diff --git a/kmymoney/dialogs/keditscheduledlg.cpp b/kmymoney/dialogs/keditscheduledlg.cpp index 60274f90e..984351b26 100644 --- a/kmymoney/dialogs/keditscheduledlg.cpp +++ b/kmymoney/dialogs/keditscheduledlg.cpp @@ -1,781 +1,781 @@ /* * Copyright 2007-2018 Thomas Baumgart * Copyright 2017-2018 Ł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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "keditscheduledlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_keditscheduledlg.h" #include "tabbar.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "register.h" #include "transactionform.h" #include "transaction.h" #include "selectedtransactions.h" #include "transactioneditor.h" #include "kmymoneylineedit.h" #include "kmymoneydateinput.h" #include "kmymoneymvccombo.h" #include "kguiutils.h" #include "kmymoneyutils.h" #include "knewaccountdlg.h" #include "knewinvestmentwizard.h" #include "keditloanwizard.h" #include "kmymoneysettings.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace eMyMoney; class KEditScheduleDlgPrivate { Q_DISABLE_COPY(KEditScheduleDlgPrivate) Q_DECLARE_PUBLIC(KEditScheduleDlg) public: explicit KEditScheduleDlgPrivate(KEditScheduleDlg *qq) : q_ptr(qq), ui(new Ui::KEditScheduleDlg), m_item(nullptr), m_editor(nullptr), m_requiredFields(nullptr) { } ~KEditScheduleDlgPrivate() { delete ui; } void init() { Q_Q(KEditScheduleDlg); ui->setupUi(q); m_requiredFields = new KMandatoryFieldGroup(q); m_requiredFields->setOkButton(ui->buttonBox->button(QDialogButtonBox::Ok)); // button to be enabled when all fields present // make sure, we have a tabbar with the form // insert it after the horizontal line ui->m_paymentInformationLayout->insertWidget(2, ui->m_form->getTabBar(ui->m_form->parentWidget())); // we never need to see the register ui->m_register->hide(); // ... setup the form ... ui->m_form->setupForm(m_schedule.account()); // ... and the register ... ui->m_register->clear(); // ... now add the transaction to register and form ... auto t = transaction(); if (m_schedule.transaction().splits().isEmpty()) m_item = KMyMoneyRegister::Register::transactionFactory(ui->m_register, t, MyMoneySplit(), 0); else m_item = KMyMoneyRegister::Register::transactionFactory(ui->m_register, t, m_schedule.transaction().splits().isEmpty() ? MyMoneySplit() : m_schedule.transaction().splits().front(), 0); ui->m_register->selectItem(m_item); // show the account row m_item->setShowRowInForm(0, true); ui->m_form->slotSetTransaction(m_item); // setup widget contents ui->m_nameEdit->setText(m_schedule.name()); - ui->m_frequencyEdit->setCurrentItem((int)m_schedule.occurrencePeriod()); + ui->m_frequencyEdit->setCurrentItem((int)m_schedule.occurrence()); if (ui->m_frequencyEdit->currentItem() == Schedule::Occurrence::Any) ui->m_frequencyEdit->setCurrentItem((int)Schedule::Occurrence::Monthly); q->slotFrequencyChanged((int)ui->m_frequencyEdit->currentItem()); ui->m_frequencyNoEdit->setValue(m_schedule.occurrenceMultiplier()); // load option widgets ui->m_paymentMethodEdit->insertItem(i18n("Direct deposit"), (int)Schedule::PaymentType::DirectDeposit); ui->m_paymentMethodEdit->insertItem(i18n("Manual deposit"), (int)Schedule::PaymentType::ManualDeposit); ui->m_paymentMethodEdit->insertItem(i18n("Direct debit"), (int)Schedule::PaymentType::DirectDebit); ui->m_paymentMethodEdit->insertItem(i18n("Standing order"), (int)Schedule::PaymentType::StandingOrder); ui->m_paymentMethodEdit->insertItem(i18n("Bank transfer"), (int)Schedule::PaymentType::BankTransfer); ui->m_paymentMethodEdit->insertItem(i18n("Write check"), (int)Schedule::PaymentType::WriteChecque); ui->m_paymentMethodEdit->insertItem(i18nc("Other payment method", "Other"), (int)Schedule::PaymentType::Other); auto method = m_schedule.paymentType(); if (method == Schedule::PaymentType::Any) method = Schedule::PaymentType::Other; ui->m_paymentMethodEdit->setCurrentItem((int)method); switch (m_schedule.weekendOption()) { case Schedule::WeekendOption::MoveNothing: ui->m_weekendOptionEdit->setCurrentIndex(0); break; case Schedule::WeekendOption::MoveBefore: ui->m_weekendOptionEdit->setCurrentIndex(1); break; case Schedule::WeekendOption::MoveAfter: ui->m_weekendOptionEdit->setCurrentIndex(2); break; } ui->m_estimateEdit->setChecked(!m_schedule.isFixed()); ui->m_lastDayInMonthEdit->setChecked(m_schedule.lastDayInMonth()); ui->m_autoEnterEdit->setChecked(m_schedule.autoEnter()); ui->m_endSeriesEdit->setChecked(m_schedule.willEnd()); ui->m_endOptionsFrame->setEnabled(m_schedule.willEnd()); if (m_schedule.willEnd()) { ui->m_RemainingEdit->setValue(m_schedule.transactionsRemaining()); ui->m_FinalPaymentEdit->setDate(m_schedule.endDate()); } q->connect(ui->m_RemainingEdit, static_cast(&QSpinBox::valueChanged), q, &KEditScheduleDlg::slotRemainingChanged); q->connect(ui->m_FinalPaymentEdit, &KMyMoneyDateInput::dateChanged, q, &KEditScheduleDlg::slotEndDateChanged); q->connect(ui->m_frequencyEdit, &KMyMoneyGeneralCombo::itemSelected, q, &KEditScheduleDlg::slotFrequencyChanged); q->connect(ui->m_frequencyNoEdit, static_cast(&QSpinBox::valueChanged), q, &KEditScheduleDlg::slotOccurrenceMultiplierChanged); q->connect(ui->buttonBox, &QDialogButtonBox::helpRequested, q, &KEditScheduleDlg::slotShowHelp); q->setModal(true); // force the initial height to be as small as possible QTimer::singleShot(0, q, SLOT(slotSetupSize())); // we just hide the variation field for now and enable the logic // once we have a respective member in the MyMoneySchedule object ui->m_variation->hide(); } /** * Helper method to recalculate and update Transactions Remaining * when other values are changed */ void updateTransactionsRemaining() { auto remain = m_schedule.transactionsRemaining(); if (remain != ui->m_RemainingEdit->value()) { ui->m_RemainingEdit->blockSignals(true); ui->m_RemainingEdit->setValue(remain); ui->m_RemainingEdit->blockSignals(false); } } MyMoneyTransaction transaction() const { auto t = m_schedule.transaction(); if (m_editor) { m_editor->createTransaction(t, m_schedule.transaction(), m_schedule.transaction().splits().isEmpty() ? MyMoneySplit() : m_schedule.transaction().splits().front(), false); } t.clearId(); t.setEntryDate(QDate()); return t; } KEditScheduleDlg *q_ptr; Ui::KEditScheduleDlg *ui; MyMoneySchedule m_schedule; KMyMoneyRegister::Transaction* m_item; QWidgetList m_tabOrderWidgets; TransactionEditor* m_editor; KMandatoryFieldGroup* m_requiredFields; }; KEditScheduleDlg::KEditScheduleDlg(const MyMoneySchedule& schedule, QWidget *parent) : QDialog(parent), d_ptr(new KEditScheduleDlgPrivate(this)) { Q_D(KEditScheduleDlg); d->m_schedule = schedule; d->m_editor = 0; d->init(); } KEditScheduleDlg::~KEditScheduleDlg() { Q_D(KEditScheduleDlg); delete d; } void KEditScheduleDlg::slotSetupSize() { resize(width(), minimumSizeHint().height()); } TransactionEditor* KEditScheduleDlg::startEdit() { Q_D(KEditScheduleDlg); KMyMoneyRegister::SelectedTransactions list(d->ui->m_register); TransactionEditor* editor = d->m_item->createEditor(d->ui->m_form, list, QDate()); // 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 && !d->m_schedule.account().id().isEmpty()) { if (!editor->fixTransactionCommodity(d->m_schedule.account())) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { editor->setScheduleInfo(d->ui->m_nameEdit->text()); connect(editor, &TransactionEditor::transactionDataSufficient, d->ui->buttonBox->button(QDialogButtonBox::Ok), &QWidget::setEnabled); connect(editor, &TransactionEditor::escapePressed, d->ui->buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::animateClick); connect(editor, &TransactionEditor::returnPressed, d->ui->buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::animateClick); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // connect(editor, SIGNAL(finishEdit(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotLeaveEditMode(KMyMoneyRegister::SelectedTransactions))); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); eWidgets::eRegister::Action action = eWidgets::eRegister::Action::Withdrawal; switch (d->m_schedule.type()) { case Schedule::Type::Deposit: action = eWidgets::eRegister::Action::Deposit; break; case Schedule::Type::Bill: action = eWidgets::eRegister::Action::Withdrawal; editor->setPaymentMethod(d->m_schedule.paymentType()); break; case Schedule::Type::Transfer: action = eWidgets::eRegister::Action::Transfer; break; default: // if we end up here, we don't have a known schedule type (yet). in this case, we just glimpse // into the transaction and determine the type. in case we don't have a transaction with splits // we stick with the default action already set up if (d->m_schedule.transaction().splits().count() > 0) { auto isDeposit = false; auto isTransfer = false; auto splits = d->m_schedule.transaction().splits(); foreach (const auto split, splits) { if (split.accountId() == d->m_schedule.account().id()) { isDeposit = !(split.shares().isNegative()); } else { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.isAssetLiability() && d->m_schedule.transaction().splits().count() == 2) { isTransfer = true; } } } if (isTransfer) action = eWidgets::eRegister::Action::Transfer; else if (isDeposit) action = eWidgets::eRegister::Action::Deposit; } break; } editor->setup(d->m_tabOrderWidgets, d->m_schedule.account(), action); // if it's not a check, then we need to clear // a possibly assigned check number if (d->m_schedule.paymentType() != Schedule::PaymentType::WriteChecque) { QWidget* w = editor->haveWidget("number"); if (w) { if (auto numberWidget = dynamic_cast(w)) { numberWidget->loadText(QString()); } } } Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); d->m_tabOrderWidgets.push_front(d->ui->m_paymentMethodEdit); // editor->setup() leaves the tabbar as the last widget in the stack, but we // need it as first here. So we move it around. QWidget* w = editor->haveWidget("tabbar"); if (w) { int idx = d->m_tabOrderWidgets.indexOf(w); if (idx != -1) { d->m_tabOrderWidgets.removeAt(idx); d->m_tabOrderWidgets.push_front(w); } } // don't forget our three buttons and additional widgets // make sure to use the correct order d->m_tabOrderWidgets.push_front(d->ui->m_frequencyEdit); d->m_tabOrderWidgets.push_front(d->ui->m_frequencyNoEdit); d->m_tabOrderWidgets.push_front(d->ui->m_nameEdit); d->m_tabOrderWidgets.append(d->ui->m_weekendOptionEdit); d->m_tabOrderWidgets.append(d->ui->m_estimateEdit); d->m_tabOrderWidgets.append(d->ui->m_variation); d->m_tabOrderWidgets.append(d->ui->m_lastDayInMonthEdit); d->m_tabOrderWidgets.append(d->ui->m_autoEnterEdit); d->m_tabOrderWidgets.append(d->ui->m_endSeriesEdit); d->m_tabOrderWidgets.append(d->ui->m_RemainingEdit); d->m_tabOrderWidgets.append(d->ui->m_FinalPaymentEdit); d->m_tabOrderWidgets.append(d->ui->buttonBox->button(QDialogButtonBox::Ok)); d->m_tabOrderWidgets.append(d->ui->buttonBox->button(QDialogButtonBox::Cancel)); d->m_tabOrderWidgets.append(d->ui->buttonBox->button(QDialogButtonBox::Help)); for (auto i = 0; i < d->m_tabOrderWidgets.size(); ++i) { w = d->m_tabOrderWidgets.at(i); if (w) { w->installEventFilter(this); w->installEventFilter(editor); } } // connect the postdate modification signal to our update routine if (auto dateEdit = dynamic_cast(editor->haveWidget("postdate"))) connect(dateEdit, &KMyMoneyDateInput::dateChanged, this, &KEditScheduleDlg::slotPostDateChanged); d->ui->m_nameEdit->setFocus(); // add the required fields to the mandatory group d->m_requiredFields->add(d->ui->m_nameEdit); d->m_requiredFields->add(editor->haveWidget("account")); d->m_requiredFields->add(editor->haveWidget("category")); // fix labels if (auto label = dynamic_cast(editor->haveWidget("date-label"))) label->setText(i18n("Next due date")); d->m_editor = editor; slotSetPaymentMethod((int)d->m_schedule.paymentType()); connect(d->ui->m_paymentMethodEdit, &KMyMoneyGeneralCombo::itemSelected, this, &KEditScheduleDlg::slotSetPaymentMethod); connect(editor, &TransactionEditor::operationTypeChanged, this, &KEditScheduleDlg::slotFilterPaymentType); } return editor; } void KEditScheduleDlg::accept() { Q_D(KEditScheduleDlg); // Force the focus to be on the OK button. This will trigger creation // of any unknown objects (payees, categories etc.) d->ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus(); // only accept if the button is really still enabled. We could end // up here, if the user filled all fields, the focus is on the category // field, but the category is not yet existent. When the user presses the // OK button in this context, he will be asked if he wants to create // the category or not. In case he decides no, we end up here with no // category filled in, so we don't run through the final acceptance. if (d->ui->buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) QDialog::accept(); } const MyMoneySchedule& KEditScheduleDlg::schedule() { Q_D(KEditScheduleDlg); if (d->m_editor) { auto t = d->transaction(); if (d->m_schedule.nextDueDate() != t.postDate()) { d->m_schedule.setNextDueDate(t.postDate()); d->m_schedule.setStartDate(t.postDate()); } d->m_schedule.setTransaction(t); d->m_schedule.setName(d->ui->m_nameEdit->text()); d->m_schedule.setFixed(!d->ui->m_estimateEdit->isChecked()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); switch (d->ui->m_weekendOptionEdit->currentIndex()) { case 0: d->m_schedule.setWeekendOption(Schedule::WeekendOption::MoveNothing); break; case 1: d->m_schedule.setWeekendOption(Schedule::WeekendOption::MoveBefore); break; case 2: d->m_schedule.setWeekendOption(Schedule::WeekendOption::MoveAfter); break; } d->m_schedule.setType(Schedule::Type::Bill); if (auto tabbar = dynamic_cast(d->m_editor->haveWidget("tabbar"))) { switch (static_cast(tabbar->currentIndex())) { case eWidgets::eRegister::Action::Deposit: d->m_schedule.setType(Schedule::Type::Deposit); break; default: case eWidgets::eRegister::Action::Withdrawal: d->m_schedule.setType(Schedule::Type::Bill); break; case eWidgets::eRegister::Action::Transfer: d->m_schedule.setType(Schedule::Type::Transfer); break; } } else { qDebug("No tabbar found in KEditScheduleDlg::schedule(). Defaulting type to BILL"); } if(d->ui->m_lastDayInMonthEdit->isEnabled()) d->m_schedule.setLastDayInMonth(d->ui->m_lastDayInMonthEdit->isChecked()); else d->m_schedule.setLastDayInMonth(false); d->m_schedule.setAutoEnter(d->ui->m_autoEnterEdit->isChecked()); d->m_schedule.setPaymentType(static_cast(d->ui->m_paymentMethodEdit->currentItem())); if (d->ui->m_endSeriesEdit->isEnabled() && d->ui->m_endSeriesEdit->isChecked()) { d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); } else { d->m_schedule.setEndDate(QDate()); } } return d->m_schedule; } void KEditScheduleDlg::newSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence 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 != eMyMoney::Schedule::Occurrence::Once) schedule.setNextDueDate(schedule.nextPayment(t.postDate())); } bool committed; do { committed = true; QPointer dlg = new KEditScheduleDlg(schedule, nullptr); QPointer transactionEditor = dlg->startEdit(); 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(nullptr, i18n("Unable to add scheduled transaction: %1", QString::fromLatin1(e.what())), i18n("Add scheduled transaction")); committed = false; } } delete transactionEditor; delete dlg; } while(!committed); } void KEditScheduleDlg::editSchedule(const MyMoneySchedule& inputSchedule) { try { auto schedule = MyMoneyFile::instance()->schedule(inputSchedule.id()); KEditScheduleDlg* sched_dlg = nullptr; KEditLoanWizard* loan_wiz = nullptr; switch (schedule.type()) { case eMyMoney::Schedule::Type::Bill: case eMyMoney::Schedule::Type::Deposit: case eMyMoney::Schedule::Type::Transfer: { sched_dlg = new KEditScheduleDlg(schedule, nullptr); QPointer transactionEditor = sched_dlg->startEdit(); if (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 auto& next = sched.nextDueDate(); const auto& 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(nullptr, 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)), 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. delete transactionEditor; ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to modify scheduled transaction '%1'", inputSchedule.name()), QString::fromLatin1(e.what())); } } delete transactionEditor; } delete sched_dlg; break; } case eMyMoney::Schedule::Type::LoanPayment: { loan_wiz = new KEditLoanWizard(schedule.account(2)); 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(nullptr, i18n("Unable to modify scheduled transaction '%1'", inputSchedule.name()), QString::fromLatin1(e.what())); } } delete loan_wiz; break; } case eMyMoney::Schedule::Type::Any: break; } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to modify scheduled transaction '%1'", inputSchedule.name()), QString::fromLatin1(e.what())); } } bool KEditScheduleDlg::focusNextPrevChild(bool next) { Q_D(KEditScheduleDlg); auto rc = false; auto w = qApp->focusWidget(); auto currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", qPrintable(w->objectName())); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { do { // if(w) qDebug("tab order is at '%s (%d/%d)'", qPrintable(w->objectName()), currentWidgetIndex, d->m_tabOrderWidgets.size()); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' as focus", qPrintable(w->objectName())); w->setFocus(); rc = true; } } while (rc == false); } return rc; } void KEditScheduleDlg::resizeEvent(QResizeEvent* ev) { Q_D(KEditScheduleDlg); d->ui->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->ui->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); QDialog::resizeEvent(ev); } void KEditScheduleDlg::slotRemainingChanged(int value) { Q_D(KEditScheduleDlg); // Make sure the required fields are set if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); if (d->m_schedule.transactionsRemaining() != value) { d->ui->m_FinalPaymentEdit->blockSignals(true); d->ui->m_FinalPaymentEdit->setDate(d->m_schedule.dateAfter(value)); d->ui->m_FinalPaymentEdit->blockSignals(false); } } void KEditScheduleDlg::slotEndDateChanged(const QDate& date) { Q_D(KEditScheduleDlg); // Make sure the required fields are set if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); if (d->m_schedule.endDate() != date) { d->m_schedule.setEndDate(date); d->updateTransactionsRemaining(); } } void KEditScheduleDlg::slotPostDateChanged(const QDate& date) { Q_D(KEditScheduleDlg); if (d->m_schedule.nextDueDate() != date) { if (d->ui->m_endOptionsFrame->isEnabled()) { d->m_schedule.setNextDueDate(date); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); d->updateTransactionsRemaining(); } } } void KEditScheduleDlg::slotSetPaymentMethod(int item) { Q_D(KEditScheduleDlg); const bool isWriteCheck = item == (int)Schedule::PaymentType::WriteChecque; if (auto numberEdit = dynamic_cast(d->m_editor->haveWidget("number"))) { numberEdit->setVisible(isWriteCheck); // hiding the label does not work, because the label underneath will shine // through. So we either write the label or a blank if (auto label = dynamic_cast(d->m_editor->haveWidget("number-label"))) label->setText(isWriteCheck ? i18n("Number") : QStringLiteral(" ")); } } void KEditScheduleDlg::slotFrequencyChanged(int item) { Q_D(KEditScheduleDlg); d->ui->m_endSeriesEdit->setEnabled(item != (int)Schedule::Occurrence::Once); bool isEndSeries = d->ui->m_endSeriesEdit->isChecked(); if (isEndSeries) d->ui->m_endOptionsFrame->setEnabled(item != (int)Schedule::Occurrence::Once); switch (item) { case (int)Schedule::Occurrence::Daily: case (int)Schedule::Occurrence::Weekly: d->ui->m_frequencyNoEdit->setEnabled(true); d->ui->m_lastDayInMonthEdit->setEnabled(false); break; case (int)Schedule::Occurrence::EveryHalfMonth: case (int)Schedule::Occurrence::Monthly: case (int)Schedule::Occurrence::Yearly: // Supports Frequency Number d->ui->m_frequencyNoEdit->setEnabled(true); d->ui->m_lastDayInMonthEdit->setEnabled(true); break; default: // Multiplier is always 1 d->ui->m_frequencyNoEdit->setEnabled(false); d->ui->m_frequencyNoEdit->setValue(1); d->ui->m_lastDayInMonthEdit->setEnabled(true); break; } if (isEndSeries && (item != (int)Schedule::Occurrence::Once)) { // Changing the frequency changes the number // of remaining transactions if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); d->m_schedule.setOccurrencePeriod(static_cast(item)); d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); d->updateTransactionsRemaining(); } } void KEditScheduleDlg::slotOccurrenceMultiplierChanged(int multiplier) { Q_D(KEditScheduleDlg); // Make sure the required fields are set auto oldOccurrenceMultiplier = d->m_schedule.occurrenceMultiplier(); if (multiplier != oldOccurrenceMultiplier) { if (d->ui->m_endOptionsFrame->isEnabled()) { if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrenceMultiplier(multiplier); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); d->updateTransactionsRemaining(); } } } void KEditScheduleDlg::slotShowHelp() { KHelpClient::invokeHelp("details.schedules.intro"); } void KEditScheduleDlg::slotFilterPaymentType(int index) { Q_D(KEditScheduleDlg); //save selected item to reload if possible auto selectedId = d->ui->m_paymentMethodEdit->itemData(d->ui->m_paymentMethodEdit->currentIndex(), Qt::UserRole).toInt(); //clear and reload the widget with the correct items d->ui->m_paymentMethodEdit->clear(); // load option widgets eWidgets::eRegister::Action action = static_cast(index); if (action != eWidgets::eRegister::Action::Withdrawal) { d->ui->m_paymentMethodEdit->insertItem(i18n("Direct deposit"), (int)Schedule::PaymentType::DirectDeposit); d->ui->m_paymentMethodEdit->insertItem(i18n("Manual deposit"), (int)Schedule::PaymentType::ManualDeposit); } if (action != eWidgets::eRegister::Action::Deposit) { d->ui->m_paymentMethodEdit->insertItem(i18n("Direct debit"), (int)Schedule::PaymentType::DirectDebit); d->ui->m_paymentMethodEdit->insertItem(i18n("Write check"), (int)Schedule::PaymentType::WriteChecque); } d->ui->m_paymentMethodEdit->insertItem(i18n("Standing order"), (int)Schedule::PaymentType::StandingOrder); d->ui->m_paymentMethodEdit->insertItem(i18n("Bank transfer"), (int)Schedule::PaymentType::BankTransfer); d->ui->m_paymentMethodEdit->insertItem(i18nc("Other payment method", "Other"), (int)Schedule::PaymentType::Other); auto newIndex = d->ui->m_paymentMethodEdit->findData(QVariant(selectedId), Qt::UserRole, Qt::MatchExactly); if (newIndex > -1) { d->ui->m_paymentMethodEdit->setCurrentIndex(newIndex); } else { d->ui->m_paymentMethodEdit->setCurrentIndex(0); } } diff --git a/kmymoney/mymoney/mymoneyforecast.cpp b/kmymoney/mymoney/mymoneyforecast.cpp index a291a5b81..ced2e2da2 100644 --- a/kmymoney/mymoney/mymoneyforecast.cpp +++ b/kmymoney/mymoney/mymoneyforecast.cpp @@ -1,1710 +1,1710 @@ /* * Copyright 2007-2010 Alvaro Soliverez * Copyright 2017-2018 Ł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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyforecast.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneybudget.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyfinancialcalculator.h" #include "mymoneyexception.h" #include "mymoneyenums.h" enum class eForecastMethod {Scheduled = 0, Historic = 1 }; /** * daily balances of an account */ typedef QMap dailyBalances; /** * map of trends of an account */ typedef QMap trendBalances; class MyMoneyForecastPrivate { Q_DECLARE_PUBLIC(MyMoneyForecast) public: explicit MyMoneyForecastPrivate(MyMoneyForecast *qq) : q_ptr(qq), m_accountsCycle(30), m_forecastCycles(3), m_forecastDays(90), m_beginForecastDay(0), m_forecastMethod(eForecastMethod::Scheduled), m_historyMethod(1), m_skipOpeningDate(true), m_includeUnusedAccounts(false), m_forecastDone(false), m_includeFutureTransactions(true), m_includeScheduledTransactions(true) { } eForecastMethod forecastMethod() const { return m_forecastMethod; } /** * Returns the list of accounts to create a budget. Only Income and Expenses are returned. */ QList budgetAccountList() { auto file = MyMoneyFile::instance(); QList accList; QStringList emptyStringList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList, emptyStringList, false); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (acc.isClosed() //check the account is not closed || (!acc.isIncomeExpense())) { //remove the account if it is not of the correct type accList_t = accList.erase(accList_t); } else { ++accList_t; } } return accList; } /** * calculate daily forecast balance based on historic transactions */ void calculateHistoricDailyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); calculateAccountTrendList(); //Calculate account daily balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); switch (q->historyMethod()) { case 0: case 1: { for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][t_day]; //trend for that day //balance of the day is the balance of the day before multiplied by the trend for the day m_accountList[acc.id()][f_day] = balanceDayBefore; m_accountList[acc.id()][f_day] += accountDailyTrend; //movement trend for that particular day m_accountList[acc.id()][f_day] = m_accountList[acc.id()][f_day].convert(acc.fraction()); //m_accountList[acc.id()][f_day] += m_accountListPast[acc.id()][f_day.addDays(-q->historyDays())]; f_day = f_day.addDays(1); } } } break; case 2: { QDate baseDate = QDate::currentDate().addDays(-q->accountsCycle()); for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) { auto f_day = 1; QDate fDate = baseDate.addDays(q->accountsCycle() + 1); while (fDate <= q->forecastEndDate()) { //the calculation is based on the balance for the last month, that is then multiplied by the trend m_accountList[acc.id()][fDate] = m_accountListPast[acc.id()][baseDate] + (m_accountTrendList[acc.id()][t_day] * MyMoneyMoney(f_day, 1)); m_accountList[acc.id()][fDate] = m_accountList[acc.id()][fDate].convert(acc.fraction()); ++f_day; fDate = baseDate.addDays(q->accountsCycle() * f_day); } baseDate = baseDate.addDays(1); } } } } } /** * calculate monthly budget balance based on historic transactions */ void calculateHistoricMonthlyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account monthly balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate();) { for (auto f_day = 1; f_day <= q->accountsCycle() && f_date <= q->forecastEndDate(); ++f_day) { MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][f_day]; //trend for that day //check for leap year if (f_date.month() == 2 && f_date.day() == 29) f_date = f_date.addDays(1); //skip 1 day m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyTrend; //movement trend for that particular day f_date = f_date.addDays(1); } } } } /** * calculate monthly budget balance based on historic transactions */ void calculateScheduledMonthlyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account monthly balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate(); f_date = f_date.addDays(1)) { //get the trend for the day MyMoneyMoney accountDailyBalance = m_accountList[acc.id()][f_date]; //do not add if it is the beginning of the month //otherwise we end up with duplicated values as reported by Marko Käning if (f_date != QDate(f_date.year(), f_date.month(), 1)) m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyBalance; } } } /** * calculate forecast based on future and scheduled transactions */ void doFutureScheduledForecast() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); if (q->isIncludingFutureTransactions()) addFutureTransactions(); if (q->isIncludingScheduledTransactions()) addScheduledTransactions(); //do not show accounts with no transactions if (!q->isIncludingUnusedAccounts()) purgeForecastAccountsList(m_accountList); //adjust value of investments to deep currency QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); if (acc.isInvest()) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; for (QDate it_day = QDate::currentDate(); it_day <= q->forecastEndDate();) { //get the price for the tradingCurrency that day const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_day); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } //value is the amount of shares multiplied by the rate of the deep currency m_accountList[acc.id()][it_day] = m_accountList[acc.id()][it_day] * rate; it_day = it_day.addDays(1); } } } } } /** * add future transactions to forecast */ void addFutureTransactions() { Q_Q(MyMoneyForecast); MyMoneyTransactionFilter filter; auto file = MyMoneyFile::instance(); // collect and process all transactions that have already been entered but // are located in the future. filter.setDateFilter(q->forecastStartDate(), q->forecastEndDate()); filter.setReportAllSplits(false); foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto acc = file->account(split.accountId()); if (q->isForecastAccount(acc)) { dailyBalances balance; balance = m_accountList[acc.id()]; //if it is income, the balance is stored as negative number if (acc.accountType() == eMyMoney::Account::Type::Income) { balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[transaction.postDate()] += split.shares(); } m_accountList[acc.id()] = balance; } } } } #if 0 QFile trcFile("forecast.csv"); trcFile.open(QIODevice::WriteOnly); QTextStream s(&trcFile); { s << "Already present transactions\n"; QMap::Iterator it_a; QSet::ConstIterator it_n; for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { auto acc = file->account(*it_n); it_a = m_accountList.find(*it_n); s << "\"" << acc.name() << "\","; for (auto i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } /** * add scheduled transactions to forecast */ void addScheduledTransactions() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); // now process all the schedules that may have an impact QList schedule; schedule = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), q->forecastEndDate(), false); if (schedule.count() > 0) { QList::Iterator it; do { qSort(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.erase(it); continue; } QDate nextDate = (*it).adjustedNextPayment((*it).adjustedDate((*it).lastPayment(), (*it).weekendOption())); if (nextDate > q->forecastEndDate()) { // We're done with this schedule, let's move on to the next schedule.erase(it); continue; } // found the next schedule. process it auto acc = (*it).account(); if (!acc.id().isEmpty()) { try { if (acc.accountType() != eMyMoney::Account::Type::Investment) { auto 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 QMap balanceMap; foreach (const auto split, t.splits()) { auto accountFromSplit = file->account(split.accountId()); if (q->isForecastAccount(accountFromSplit)) { // collect all overdues on the first day QDate forecastDate = nextDate; if (QDate::currentDate() >= nextDate) forecastDate = QDate::currentDate().addDays(1); dailyBalances balance; balance = m_accountList[accountFromSplit.id()]; for (QDate f_day = QDate::currentDate(); f_day < forecastDate;) { balanceMap[accountFromSplit.id()] += m_accountList[accountFromSplit.id()][f_day]; f_day = f_day.addDays(1); } } } // take care of the autoCalc stuff q->calculateAutoLoan(*it, t, balanceMap); // now add the splits to the balances foreach (const auto split, t.splits()) { auto accountFromSplit = file->account(split.accountId()); if (q->isForecastAccount(accountFromSplit)) { dailyBalances balance; balance = m_accountList[accountFromSplit.id()]; //auto 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 (accountFromSplit.accountType() == eMyMoney::Account::Type::Income) { balance[forecastDate] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[forecastDate] += split.shares(); } m_accountList[accountFromSplit.id()] = balance; } } } } (*it).setLastPayment(date); } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e.what(); schedule.erase(it); } } else { // remove schedule from list schedule.erase(it); } } while (1); } #if 0 { s << "\n\nAdded scheduled transactions\n"; QMap::Iterator it_a; QSet::ConstIterator it_n; for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { auto acc = file->account(*it_n); it_a = m_accountList.find(*it_n); s << "\"" << acc.name() << "\","; for (auto i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } /** * calculate daily forecast balance based on future and scheduled transactions */ void calculateScheduledDailyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account daily balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before m_accountList[acc.id()][f_day] += balanceDayBefore; //running sum f_day = f_day.addDays(1); } } } /** * set the starting balance for an accounts */ void setStartingBalance(const MyMoneyAccount& acc) { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Get current account balance if (acc.isInvest()) { //investments require special treatment //get the security id of that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; //get te const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), QDate::currentDate()); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()) * rate; } } else { m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()); } //if the method is linear regression, we have to add the opening balance to m_accountListPast if (forecastMethod() == eForecastMethod::Historic && q->historyMethod() == 2) { //FIXME workaround for stock opening dates QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } //add opening balance only if it opened after the history start if (openingDate >= q->historyStartDate()) { MyMoneyMoney openingBalance; openingBalance = file->balance(acc.id(), openingDate); //calculate running sum for (QDate it_date = openingDate; it_date <= q->historyEndDate(); it_date = it_date.addDays(1)) { //investments require special treatment if (acc.isInvest()) { //get the security id of that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; //get the rate for that specific date const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } m_accountListPast[acc.id()][it_date] += openingBalance * rate; } } else { m_accountListPast[acc.id()][it_date] += openingBalance; } } } } } /** * Returns the day moving average for the account @a acc based on the daily balances of a given number of @p forecastTerms * It returns the moving average for a given @p trendDay of the forecastTerm * With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred * at that day and the day before,for the last 3 months */ MyMoneyMoney accountMovingAverage(const MyMoneyAccount& acc, const qint64 trendDay, const int forecastTerms) { Q_Q(MyMoneyForecast); //Calculate a daily trend for the account based on the accounts of a given number of terms //With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred at that day and the day before, //for the last 3 months MyMoneyMoney balanceVariation; for (auto it_terms = 0; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; balanceVariation += (balanceAfter - balanceBefore); //add the balance variation between days } //calculate average of the variations return (balanceVariation / MyMoneyMoney(forecastTerms, 1)).convert(10000); } /** * Returns the weighted moving average for a given @p trendDay */ MyMoneyMoney accountWeightedMovingAverage(const MyMoneyAccount& acc, const qint64 trendDay, const int totalWeight) { Q_Q(MyMoneyForecast); MyMoneyMoney balanceVariation; for (auto it_terms = 0, weight = 1; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++weight) { //sum for each term multiplied by weight MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; balanceVariation += ((balanceAfter - balanceBefore) * MyMoneyMoney(weight, 1)); //add the balance variation between days multiplied by its weight } //calculate average of the variations return (balanceVariation / MyMoneyMoney(totalWeight, 1)).convert(10000); } /** * Returns the linear regression for a given @p trendDay */ MyMoneyMoney accountLinearRegression(const MyMoneyAccount &acc, const qint64 trendDay, const qint64 actualTerms, const MyMoneyMoney& meanTerms) { Q_Q(MyMoneyForecast); MyMoneyMoney meanBalance, totalBalance, totalTerms; totalTerms = MyMoneyMoney(actualTerms, 1); //calculate mean balance for (auto it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term totalBalance += m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; } meanBalance = totalBalance / MyMoneyMoney(actualTerms, 1); meanBalance = meanBalance.convert(10000); //calculate b1 //first calculate x - mean x multiplied by y - mean y MyMoneyMoney totalXY, totalSqX; auto term = 1; for (auto it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++term) { //sum for each term MyMoneyMoney balance = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; MyMoneyMoney balMeanBal = balance - meanBalance; MyMoneyMoney termMeanTerm = (MyMoneyMoney(term, 1) - meanTerms); totalXY += (balMeanBal * termMeanTerm).convert(10000); totalSqX += (termMeanTerm * termMeanTerm).convert(10000); } totalXY = (totalXY / MyMoneyMoney(actualTerms, 1)).convert(10000); totalSqX = (totalSqX / MyMoneyMoney(actualTerms, 1)).convert(10000); //check zero if (totalSqX.isZero()) return MyMoneyMoney(); MyMoneyMoney linReg = (totalXY / totalSqX).convert(10000); return linReg; } /** * calculate daily forecast trend based on historic transactions */ void calculateAccountTrendList() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); qint64 auxForecastTerms; qint64 totalWeight = 0; //Calculate account trends QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); m_accountTrendList[acc.id()][0] = MyMoneyMoney(); // for today, the trend is 0 auxForecastTerms = q->forecastCycles(); if (q->skipOpeningDate()) { QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if (openingDate > q->historyStartDate()) { //if acc opened after forecast period auxForecastTerms = 1 + ((openingDate.daysTo(q->historyEndDate()) + 1) / q->accountsCycle()); // set forecastTerms to a lower value, to calculate only based on how long this account was opened } } switch (q->historyMethod()) { //moving average case 0: { for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) m_accountTrendList[acc.id()][t_day] = accountMovingAverage(acc, t_day, auxForecastTerms); //moving average break; } //weighted moving average case 1: { //calculate total weight for moving average if (auxForecastTerms == q->forecastCycles()) { totalWeight = (auxForecastTerms * (auxForecastTerms + 1)) / 2; //totalWeight is the triangular number of auxForecastTerms } else { //if only taking a few periods, totalWeight is the sum of the weight for most recent periods auto i = 1; for (qint64 w = q->forecastCycles(); i <= auxForecastTerms; ++i, --w) totalWeight += w; } for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) m_accountTrendList[acc.id()][t_day] = accountWeightedMovingAverage(acc, t_day, totalWeight); break; } case 2: { //calculate mean term MyMoneyMoney meanTerms = MyMoneyMoney((auxForecastTerms * (auxForecastTerms + 1)) / 2, 1) / MyMoneyMoney(auxForecastTerms, 1); for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) m_accountTrendList[acc.id()][t_day] = accountLinearRegression(acc, t_day, auxForecastTerms, meanTerms); break; } default: break; } } } /** * set the internal list of accounts to be forecast */ void setForecastAccountList() { Q_Q(MyMoneyForecast); //get forecast accounts QList accList; accList = q->forecastAccountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { m_forecastAccounts.insert((*accList_t).id()); } } /** * set the internal list of accounts to create a budget */ void setBudgetAccountList() { //get budget accounts QList accList; accList = budgetAccountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { m_forecastAccounts.insert((*accList_t).id()); } } /** * get past transactions for the accounts to be forecast */ void pastTransactions() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setDateFilter(q->historyStartDate(), q->historyEndDate()); filter.setReportAllSplits(false); //Check past transactions foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto acc = file->account(split.accountId()); //workaround for stock accounts which have faulty opening dates QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if (q->isForecastAccount(acc) //If it is one of the accounts we are checking, add the amount of the transaction && ((openingDate < transaction.postDate() && q->skipOpeningDate()) || !q->skipOpeningDate())) { //don't take the opening day of the account to calculate balance dailyBalances balance; //FIXME deal with leap years balance = m_accountListPast[acc.id()]; if (acc.accountType() == eMyMoney::Account::Type::Income) {//if it is income, the balance is stored as negative number balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[transaction.postDate()] += split.shares(); } // check if this is a new account for us m_accountListPast[acc.id()] = balance; } } } } //purge those accounts with no transactions on the period if (q->isIncludingUnusedAccounts() == false) purgeForecastAccountsList(m_accountListPast); //calculate running sum QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); m_accountListPast[acc.id()][q->historyStartDate().addDays(-1)] = file->balance(acc.id(), q->historyStartDate().addDays(-1)); for (QDate it_date = q->historyStartDate(); it_date <= q->historyEndDate();) { m_accountListPast[acc.id()][it_date] += m_accountListPast[acc.id()][it_date.addDays(-1)]; //Running sum it_date = it_date.addDays(1); } } //adjust value of investments to deep currency for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); if (acc.isInvest()) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); if (! undersecurity.isCurrency()) { //only do it if the security is not an actual currency MyMoneyMoney rate = MyMoneyMoney::ONE; //set the default value for (QDate it_date = q->historyStartDate().addDays(-1) ; it_date <= q->historyEndDate();) { //get the price for the tradingCurrency that day const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } //value is the amount of shares multiplied by the rate of the deep currency m_accountListPast[acc.id()][it_date] = m_accountListPast[acc.id()][it_date] * rate; it_date = it_date.addDays(1); } } } } } /** * calculate the day to start forecast and sets the begin date * The quantity of forecast days will be counted from this date * Depends on the values of begin day and accounts cycle * The rules to calculate begin day are as follows: * - if beginDay is 0, begin date is current date * - if the day of the month set by beginDay has not passed, that will be used * - if adding an account cycle to beginDay, will not go past the beginDay of next month, * that date will be used, otherwise it will add account cycle to beginDay until it is past current date * It returns the total amount of Forecast Days from current date. */ qint64 calculateBeginForecastDay() { Q_Q(MyMoneyForecast); auto fDays = q->forecastDays(); auto beginDay = q->beginForecastDay(); auto accCycle = q->accountsCycle(); QDate beginDate; //if 0, beginDate is current date and forecastDays remains unchanged if (beginDay == 0) { q->setBeginForecastDate(QDate::currentDate()); return fDays; } //adjust if beginDay more than days of current month if (QDate::currentDate().daysInMonth() < beginDay) beginDay = QDate::currentDate().daysInMonth(); //if beginDay still to come, calculate and return if (QDate::currentDate().day() <= beginDay) { beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); fDays += QDate::currentDate().daysTo(beginDate); q->setBeginForecastDate(beginDate); return fDays; } //adjust beginDay for next month if (QDate::currentDate().addMonths(1).daysInMonth() < beginDay) beginDay = QDate::currentDate().addMonths(1).daysInMonth(); //if beginDay of next month comes before 1 interval, use beginDay next month if (QDate::currentDate().addDays(accCycle) >= (QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1))) { beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1); fDays += QDate::currentDate().daysTo(beginDate); } else { //add intervals to current beginDay and take the first after current date beginDay = ((((QDate::currentDate().day() - beginDay) / accCycle) + 1) * accCycle) + beginDay; beginDate = QDate::currentDate().addDays(beginDay - QDate::currentDate().day()); fDays += QDate::currentDate().daysTo(beginDate); } q->setBeginForecastDate(beginDate); return fDays; } /** * remove accounts from the list if the accounts has no transactions in the forecast timeframe. * Used for scheduled-forecast method. */ void purgeForecastAccountsList(QMap& accountList) { m_forecastAccounts.intersect(accountList.keys().toSet()); } MyMoneyForecast *q_ptr; /** * daily forecast balance of accounts */ QMap m_accountList; /** * daily past balance of accounts */ QMap m_accountListPast; /** * daily forecast trends of accounts */ QMap m_accountTrendList; /** * list of forecast account ids. */ QSet m_forecastAccounts; /** * cycle of accounts in days */ qint64 m_accountsCycle; /** * number of cycles to use in forecast */ qint64 m_forecastCycles; /** * number of days to forecast */ qint64 m_forecastDays; /** * date to start forecast */ QDate m_beginForecastDate; /** * day to start forecast */ qint64 m_beginForecastDay; /** * forecast method */ eForecastMethod m_forecastMethod; /** * history method */ int m_historyMethod; /** * start date of history */ QDate m_historyStartDate; /** * end date of history */ QDate m_historyEndDate; /** * start date of forecast */ QDate m_forecastStartDate; /** * end date of forecast */ QDate m_forecastEndDate; /** * skip opening date when fetching transactions of an account */ bool m_skipOpeningDate; /** * include accounts with no transactions in the forecast timeframe. default is false. */ bool m_includeUnusedAccounts; /** * forecast already done */ bool m_forecastDone; /** * include future transactions when doing a scheduled-based forecast */ bool m_includeFutureTransactions; /** * include scheduled transactions when doing a scheduled-based forecast */ bool m_includeScheduledTransactions; }; MyMoneyForecast::MyMoneyForecast() : d_ptr(new MyMoneyForecastPrivate(this)) { setHistoryStartDate(QDate::currentDate().addDays(-forecastCycles()*accountsCycle())); setHistoryEndDate(QDate::currentDate().addDays(-1)); } MyMoneyForecast::MyMoneyForecast(const MyMoneyForecast& other) : d_ptr(new MyMoneyForecastPrivate(*other.d_func())) { this->d_ptr->q_ptr = this; } void swap(MyMoneyForecast& first, MyMoneyForecast& second) { using std::swap; swap(first.d_ptr, second.d_ptr); swap(first.d_ptr->q_ptr, second.d_ptr->q_ptr); } MyMoneyForecast::MyMoneyForecast(MyMoneyForecast && other) : MyMoneyForecast() { swap(*this, other); } MyMoneyForecast & MyMoneyForecast::operator=(MyMoneyForecast other) { swap(*this, other); return *this; } MyMoneyForecast::~MyMoneyForecast() { Q_D(MyMoneyForecast); delete d; } void MyMoneyForecast::doForecast() { Q_D(MyMoneyForecast); auto fDays = d->calculateBeginForecastDay(); auto fMethod = d->forecastMethod(); auto fAccCycle = accountsCycle(); auto fCycles = forecastCycles(); //validate settings if (fAccCycle < 1 || fCycles < 1 || fDays < 1) { throw MYMONEYEXCEPTION_CSTRING("Illegal settings when calling doForecast. Settings must be higher than 0"); } //initialize global variables setForecastDays(fDays); setForecastStartDate(QDate::currentDate().addDays(1)); setForecastEndDate(QDate::currentDate().addDays(fDays)); setAccountsCycle(fAccCycle); setForecastCycles(fCycles); setHistoryStartDate(forecastCycles() * accountsCycle()); setHistoryEndDate(QDate::currentDate().addDays(-1)); //yesterday //clear all data before calculating d->m_accountListPast.clear(); d->m_accountList.clear(); d->m_accountTrendList.clear(); //set forecast accounts d->setForecastAccountList(); switch (fMethod) { case eForecastMethod::Scheduled: d->doFutureScheduledForecast(); d->calculateScheduledDailyBalances(); break; case eForecastMethod::Historic: d->pastTransactions(); d->calculateHistoricDailyBalances(); break; default: break; } //flag the forecast as done d->m_forecastDone = true; } bool MyMoneyForecast::isForecastAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); if (d->m_forecastAccounts.isEmpty()) { d->setForecastAccountList(); } return d->m_forecastAccounts.contains(acc.id()); } QList MyMoneyForecast::accountList() { auto file = MyMoneyFile::instance(); QList accList; QStringList emptyStringList; //Get all accounts from the file and check if they are present file->accountList(accList, emptyStringList, false); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (!isForecastAccount(acc)) { accList_t = accList.erase(accList_t); //remove the account } else { ++accList_t; } } return accList; } MyMoneyMoney MyMoneyForecast::calculateAccountTrend(const MyMoneyAccount& acc, qint64 trendDays) { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; MyMoneyMoney netIncome; QDate startDate; QDate openingDate = acc.openingDate(); //validate arguments if (trendDays < 1) { throw MYMONEYEXCEPTION_CSTRING("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0"); } //If it is a new account, we don't take into account the first day //because it is usually a weird one and it would mess up the trend if (openingDate.daysTo(QDate::currentDate()) < trendDays) { startDate = (acc.openingDate()).addDays(1); } else { startDate = QDate::currentDate().addDays(-trendDays); } //get all transactions for the period filter.setDateFilter(startDate, QDate::currentDate()); if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) { filter.addCategory(acc.id()); } else { filter.addAccount(acc.id()); } filter.setReportAllSplits(false); //add all transactions for that account foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { if (acc.id() == split.accountId()) netIncome += split.value(); } } } //calculate trend of the account in the past period MyMoneyMoney accTrend; //don't take into account the first day of the account if (openingDate.daysTo(QDate::currentDate()) < trendDays) { accTrend = netIncome / MyMoneyMoney(openingDate.daysTo(QDate::currentDate()) - 1, 1); } else { accTrend = netIncome / MyMoneyMoney(trendDays, 1); } return accTrend; } MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, const QDate &forecastDate) { Q_D(MyMoneyForecast); dailyBalances balance; MyMoneyMoney MM_amount = MyMoneyMoney(); //Check if acc is not a forecast account, return 0 if (!isForecastAccount(acc)) { return MM_amount; } if (d->m_accountList.contains(acc.id())) { balance = d->m_accountList.value(acc.id()); } if (balance.contains(forecastDate)) { //if the date is not in the forecast, it returns 0 MM_amount = balance.value(forecastDate); } return MM_amount; } /** * Returns the forecast balance trend for account @a acc for offset @p int * offset is days from current date, inside forecast days. * Returns 0 if offset not in range of forecast days. */ MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, qint64 offset) { QDate forecastDate = QDate::currentDate().addDays(offset); return forecastBalance(acc, forecastDate); } qint64 MyMoneyForecast::daysToMinimumBalance(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); dailyBalances balance; //Check if acc is not a forecast account, return -1 if (!isForecastAccount(acc)) { return -1; } balance = d->m_accountList[acc.id()]; for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (minBalance > balance[it_day]) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } return -1; } qint64 MyMoneyForecast::daysToZeroBalance(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); dailyBalances balance; //Check if acc is not a forecast account, return -1 if (!isForecastAccount(acc)) { return -2; } balance = d->m_accountList[acc.id()]; if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (balance[it_day] < MyMoneyMoney()) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } else if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (balance[it_day] > MyMoneyMoney()) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } return -1; } MyMoneyMoney MyMoneyForecast::accountCycleVariation(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); MyMoneyMoney cycleVariation; if (d->forecastMethod() == eForecastMethod::Historic) { switch (historyMethod()) { case 0: case 1: { for (auto t_day = 1; t_day <= accountsCycle() ; ++t_day) { cycleVariation += d->m_accountTrendList[acc.id()][t_day]; } } break; case 2: { cycleVariation = d->m_accountList[acc.id()][QDate::currentDate().addDays(accountsCycle())] - d->m_accountList[acc.id()][QDate::currentDate()]; break; } } } return cycleVariation; } MyMoneyMoney MyMoneyForecast::accountTotalVariation(const MyMoneyAccount& acc) { MyMoneyMoney totalVariation; totalVariation = forecastBalance(acc, forecastEndDate()) - forecastBalance(acc, QDate::currentDate()); return totalVariation; } QList MyMoneyForecast::accountMinimumBalanceDateList(const MyMoneyAccount& acc) { QList minBalanceList; qint64 daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (auto t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney minBalance = forecastBalance(acc, (t_cycle * accountsCycle() + daysToBeginDay)); QDate minDate = QDate::currentDate().addDays(t_cycle * accountsCycle() + daysToBeginDay); for (auto t_day = 1; t_day <= accountsCycle() ; ++t_day) { if (minBalance > forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { minBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); minDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } minBalanceList.append(minDate); } return minBalanceList; } QList MyMoneyForecast::accountMaximumBalanceDateList(const MyMoneyAccount& acc) { QList maxBalanceList; qint64 daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (auto t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney maxBalance = forecastBalance(acc, ((t_cycle * accountsCycle()) + daysToBeginDay)); QDate maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay); for (auto t_day = 0; t_day < accountsCycle() ; ++t_day) { if (maxBalance < forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { maxBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } maxBalanceList.append(maxDate); } return maxBalanceList; } MyMoneyMoney MyMoneyForecast::accountAverageBalance(const MyMoneyAccount& acc) { MyMoneyMoney totalBalance; for (auto f_day = 1; f_day <= forecastDays() ; ++f_day) { totalBalance += forecastBalance(acc, f_day); } return totalBalance / MyMoneyMoney(forecastDays(), 1); } void MyMoneyForecast::createBudget(MyMoneyBudget& budget, QDate historyStart, QDate historyEnd, QDate budgetStart, QDate budgetEnd, const bool returnBudget) { Q_D(MyMoneyForecast); // clear all data except the id and name QString name = budget.name(); budget = MyMoneyBudget(budget.id(), MyMoneyBudget()); budget.setName(name); //check parameters if (historyStart > historyEnd || budgetStart > budgetEnd || budgetStart <= historyEnd) { throw MYMONEYEXCEPTION_CSTRING("Illegal parameters when trying to create budget"); } //get forecast method auto fMethod = d->forecastMethod(); //set start date to 1st of month and end dates to last day of month, since we deal with full months in budget historyStart = QDate(historyStart.year(), historyStart.month(), 1); historyEnd = QDate(historyEnd.year(), historyEnd.month(), historyEnd.daysInMonth()); budgetStart = QDate(budgetStart.year(), budgetStart.month(), 1); budgetEnd = QDate(budgetEnd.year(), budgetEnd.month(), budgetEnd.daysInMonth()); //set forecast parameters setHistoryStartDate(historyStart); setHistoryEndDate(historyEnd); setForecastStartDate(budgetStart); setForecastEndDate(budgetEnd); setForecastDays(budgetStart.daysTo(budgetEnd) + 1); if (budgetStart.daysTo(budgetEnd) > historyStart.daysTo(historyEnd)) { //if history period is shorter than budget, use that one as the trend length setAccountsCycle(historyStart.daysTo(historyEnd)); //we set the accountsCycle to the base timeframe we will use to calculate the average (eg. 180 days, 365, etc) } else { //if one timeframe is larger than the other, but not enough to be 1 time larger, we take the lowest value setAccountsCycle(budgetStart.daysTo(budgetEnd)); } setForecastCycles((historyStart.daysTo(historyEnd) / accountsCycle())); if (forecastCycles() == 0) //the cycles must be at least 1 setForecastCycles(1); //do not skip opening date setSkipOpeningDate(false); //clear and set accounts list we are going to use. Categories, in this case d->m_forecastAccounts.clear(); d->setBudgetAccountList(); //calculate budget according to forecast method switch (fMethod) { case eForecastMethod::Scheduled: d->doFutureScheduledForecast(); d->calculateScheduledMonthlyBalances(); break; case eForecastMethod::Historic: d->pastTransactions(); //get all transactions for history period d->calculateAccountTrendList(); d->calculateHistoricMonthlyBalances(); //add all balances of each month and put at the 1st day of each month break; default: break; } //flag the forecast as done d->m_forecastDone = true; //only fill the budget if it is going to be used if (returnBudget) { //setup the budget itself auto file = MyMoneyFile::instance(); budget.setBudgetStart(budgetStart); //go through all the accounts and add them to budget for (auto it_nc = d->m_forecastAccounts.constBegin(); it_nc != d->m_forecastAccounts.constEnd(); ++it_nc) { auto acc = file->account(*it_nc); MyMoneyBudget::AccountGroup budgetAcc; budgetAcc.setId(acc.id()); budgetAcc.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); for (QDate f_date = forecastStartDate(); f_date <= forecastEndDate();) { MyMoneyBudget::PeriodGroup period; //add period to budget account period.setStartDate(f_date); period.setAmount(forecastBalance(acc, f_date)); budgetAcc.addPeriod(f_date, period); //next month f_date = f_date.addMonths(1); } //add budget account to budget budget.setAccount(budgetAcc, acc.id()); } } } qint64 MyMoneyForecast::historyDays() const { Q_D(const MyMoneyForecast); return (d->m_historyStartDate.daysTo(d->m_historyEndDate) + 1); } void MyMoneyForecast::setAccountsCycle(qint64 accountsCycle) { Q_D(MyMoneyForecast); d->m_accountsCycle = accountsCycle; } void MyMoneyForecast::setForecastCycles(qint64 forecastCycles) { Q_D(MyMoneyForecast); d->m_forecastCycles = forecastCycles; } void MyMoneyForecast::setForecastDays(qint64 forecastDays) { Q_D(MyMoneyForecast); d->m_forecastDays = forecastDays; } void MyMoneyForecast::setBeginForecastDate(const QDate &beginForecastDate) { Q_D(MyMoneyForecast); d->m_beginForecastDate = beginForecastDate; } void MyMoneyForecast::setBeginForecastDay(qint64 beginDay) { Q_D(MyMoneyForecast); d->m_beginForecastDay = beginDay; } void MyMoneyForecast::setForecastMethod(qint64 forecastMethod) { Q_D(MyMoneyForecast); d->m_forecastMethod = static_cast(forecastMethod); } void MyMoneyForecast::setHistoryStartDate(const QDate &historyStartDate) { Q_D(MyMoneyForecast); d->m_historyStartDate = historyStartDate; } void MyMoneyForecast::setHistoryEndDate(const QDate &historyEndDate) { Q_D(MyMoneyForecast); d->m_historyEndDate = historyEndDate; } void MyMoneyForecast::setHistoryStartDate(qint64 daysToStartDate) { setHistoryStartDate(QDate::currentDate().addDays(-daysToStartDate)); } void MyMoneyForecast::setHistoryEndDate(qint64 daysToEndDate) { setHistoryEndDate(QDate::currentDate().addDays(-daysToEndDate)); } void MyMoneyForecast::setForecastStartDate(const QDate &_startDate) { Q_D(MyMoneyForecast); d->m_forecastStartDate = _startDate; } void MyMoneyForecast::setForecastEndDate(const QDate &_endDate) { Q_D(MyMoneyForecast); d->m_forecastEndDate = _endDate; } void MyMoneyForecast::setSkipOpeningDate(bool _skip) { Q_D(MyMoneyForecast); d->m_skipOpeningDate = _skip; } void MyMoneyForecast::setHistoryMethod(int historyMethod) { Q_D(MyMoneyForecast); d->m_historyMethod = historyMethod; } void MyMoneyForecast::setIncludeUnusedAccounts(bool _bool) { Q_D(MyMoneyForecast); d->m_includeUnusedAccounts = _bool; } void MyMoneyForecast::setForecastDone(bool _bool) { Q_D(MyMoneyForecast); d->m_forecastDone = _bool; } void MyMoneyForecast::setIncludeFutureTransactions(bool _bool) { Q_D(MyMoneyForecast); d->m_includeFutureTransactions = _bool; } void MyMoneyForecast::setIncludeScheduledTransactions(bool _bool) { Q_D(MyMoneyForecast); d->m_includeScheduledTransactions = _bool; } qint64 MyMoneyForecast::accountsCycle() const { Q_D(const MyMoneyForecast); return d->m_accountsCycle; } qint64 MyMoneyForecast::forecastCycles() const { Q_D(const MyMoneyForecast); return d->m_forecastCycles; } qint64 MyMoneyForecast::forecastDays() const { Q_D(const MyMoneyForecast); return d->m_forecastDays; } QDate MyMoneyForecast::beginForecastDate() const { Q_D(const MyMoneyForecast); return d->m_beginForecastDate; } qint64 MyMoneyForecast::beginForecastDay() const { Q_D(const MyMoneyForecast); return d->m_beginForecastDay; } QDate MyMoneyForecast::historyStartDate() const { Q_D(const MyMoneyForecast); return d->m_historyStartDate; } QDate MyMoneyForecast::historyEndDate() const { Q_D(const MyMoneyForecast); return d->m_historyEndDate; } QDate MyMoneyForecast::forecastStartDate() const { Q_D(const MyMoneyForecast); return d->m_forecastStartDate; } QDate MyMoneyForecast::forecastEndDate() const { Q_D(const MyMoneyForecast); return d->m_forecastEndDate; } bool MyMoneyForecast::skipOpeningDate() const { Q_D(const MyMoneyForecast); return d->m_skipOpeningDate; } int MyMoneyForecast::historyMethod() const { Q_D(const MyMoneyForecast); return d->m_historyMethod; } bool MyMoneyForecast::isIncludingUnusedAccounts() const { Q_D(const MyMoneyForecast); return d->m_includeUnusedAccounts; } bool MyMoneyForecast::isForecastDone() const { Q_D(const MyMoneyForecast); return d->m_forecastDone; } bool MyMoneyForecast::isIncludingFutureTransactions() const { Q_D(const MyMoneyForecast); return d->m_includeFutureTransactions; } bool MyMoneyForecast::isIncludingScheduledTransactions() const { Q_D(const MyMoneyForecast); return d->m_includeScheduledTransactions; } void MyMoneyForecast::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { //get amortization and interest autoCalc splits MyMoneySplit amortizationSplit = transaction.amortizationSplit(); MyMoneySplit interestSplit = transaction.interestSplit(); const bool interestSplitValid = !interestSplit.id().isEmpty(); if (!amortizationSplit.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()]; // 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.occurrence())); + calc.setPF(MyMoneySchedule::eventsPerYear(schedule.baseOccurrence())); eMyMoney::Schedule::Occurrence compoundingOccurrence = static_cast(acc.interestCompounding()); if (compoundingOccurrence == eMyMoney::Schedule::Occurrence::Any) - compoundingOccurrence = schedule.occurrence(); + compoundingOccurrence = schedule.baseOccurrence(); calc.setCF(MyMoneySchedule::eventsPerYear(compoundingOccurrence)); calc.setPv(balance.toDouble()); calc.setIr(acc.interestRate(dueDate).abs().toDouble()); calc.setPmt(acc.periodicPayment().toDouble()); MyMoneyMoney interest(calc.interestDue(), 100), amortization; interest = interest.abs(); // make sure it's positive for now amortization = acc.periodicPayment() - interest; if (acc.accountType() == eMyMoney::Account::Type::AssetLoan) { interest = -interest; amortization = -amortization; } amortizationSplit.setShares(amortization); if (interestSplitValid) interestSplit.setShares(interest); // FIXME: for now we only assume loans to be in the currency of the transaction amortizationSplit.setValue(amortization); if (interestSplitValid) interestSplit.setValue(interest); transaction.modifySplit(amortizationSplit); if (interestSplitValid) transaction.modifySplit(interestSplit); } } } QList MyMoneyForecast::forecastAccountList() { auto file = MyMoneyFile::instance(); QList accList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (acc.isClosed() //check the account is not closed || (!acc.isAssetLiability())) { //|| (acc.accountType() == eMyMoney::Account::Type::Investment) ) {//check that it is not an Investment account and only include Stock accounts //remove the account if it is not of the correct type accList_t = accList.erase(accList_t); } else { ++accList_t; } } return accList; } diff --git a/kmymoney/mymoney/mymoneyschedule.cpp b/kmymoney/mymoney/mymoneyschedule.cpp index 359e23389..6fff4f864 100644 --- a/kmymoney/mymoney/mymoneyschedule.cpp +++ b/kmymoney/mymoney/mymoneyschedule.cpp @@ -1,1447 +1,1447 @@ /* * Copyright 2000-2004 Michael Edwardes * Copyright 2002-2018 Thomas Baumgart * Copyright 2005 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyschedule.h" #include "mymoneyschedule_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysplit.h" #include "imymoneyprocessingcalendar.h" using namespace eMyMoney; static IMyMoneyProcessingCalendar* processingCalendarPtr = 0; MyMoneySchedule::MyMoneySchedule() : MyMoneyObject(*new MyMoneySchedulePrivate) { } MyMoneySchedule::MyMoneySchedule(const QString &id) : MyMoneyObject(*new MyMoneySchedulePrivate, id) { } MyMoneySchedule::MyMoneySchedule(const QString& name, Schedule::Type type, Schedule::Occurrence occurrence, int occurrenceMultiplier, Schedule::PaymentType paymentType, const QDate& /* startDate */, const QDate& endDate, bool fixed, bool autoEnter) : MyMoneyObject(*new MyMoneySchedulePrivate) { Q_D(MyMoneySchedule); // Set up the values possibly differeing from defaults d->m_name = name; d->m_occurrence = occurrence; d->m_occurrenceMultiplier = occurrenceMultiplier; simpleToCompoundOccurrence(d->m_occurrenceMultiplier, d->m_occurrence); d->m_type = type; d->m_paymentType = paymentType; d->m_fixed = fixed; d->m_autoEnter = autoEnter; d->m_endDate = endDate; } MyMoneySchedule::MyMoneySchedule(const MyMoneySchedule& other) : MyMoneyObject(*new MyMoneySchedulePrivate(*other.d_func()), other.id()) { } MyMoneySchedule::MyMoneySchedule(const QString& id, const MyMoneySchedule& other) : MyMoneyObject(*new MyMoneySchedulePrivate(*other.d_func()), id) { } MyMoneySchedule::~MyMoneySchedule() { } -Schedule::Occurrence MyMoneySchedule::occurrence() const +Schedule::Occurrence MyMoneySchedule::baseOccurrence() const { Q_D(const MyMoneySchedule); Schedule::Occurrence occ = d->m_occurrence; int mult = d->m_occurrenceMultiplier; compoundToSimpleOccurrence(mult, occ); return occ; } int MyMoneySchedule::occurrenceMultiplier() const { Q_D(const MyMoneySchedule); return d->m_occurrenceMultiplier; } eMyMoney::Schedule::Type MyMoneySchedule::type() const { Q_D(const MyMoneySchedule); return d->m_type; } -eMyMoney::Schedule::Occurrence MyMoneySchedule::occurrencePeriod() const +eMyMoney::Schedule::Occurrence MyMoneySchedule::occurrence() const { Q_D(const MyMoneySchedule); return d->m_occurrence; } void MyMoneySchedule::setStartDate(const QDate& date) { Q_D(MyMoneySchedule); d->m_startDate = date; } void MyMoneySchedule::setPaymentType(Schedule::PaymentType type) { Q_D(MyMoneySchedule); d->m_paymentType = type; } void MyMoneySchedule::setFixed(bool fixed) { Q_D(MyMoneySchedule); d->m_fixed = fixed; } void MyMoneySchedule::setTransaction(const MyMoneyTransaction& transaction) { setTransaction(transaction, false); } void MyMoneySchedule::setTransaction(const MyMoneyTransaction& transaction, bool noDateCheck) { auto t = transaction; Q_D(MyMoneySchedule); if (!noDateCheck) { // don't allow a transaction that has no due date // if we get something like that, then we use the // the current next due date. If that is also invalid // we can't help it. if (!t.postDate().isValid()) { t.setPostDate(d->m_transaction.postDate()); } if (!t.postDate().isValid()) return; } // make sure to clear out some unused information in scheduled transactions // we need to do this for the case that the transaction passed as argument // is a matched or imported transaction. auto firstSplit = true; foreach (const auto split, t.splits()) { MyMoneySplit s = split; // clear out the bankID if (!split.bankID().isEmpty()) { s.setBankID(QString()); t.modifySplit(s); } // only clear payees from second split onwards if (firstSplit) { firstSplit = false; continue; } if (!split.payeeId().isEmpty()) { // but only if the split references an income/expense category auto file = MyMoneyFile::instance(); // some unit tests don't have a storage attached, so we // simply skip the test // Don't check for accounts with an id of 'Phony-ID' which is used // internally for non-existing accounts (during creation of accounts) if (file->storageAttached() && s.accountId() != QString("Phony-ID")) { auto acc = file->account(s.accountId()); if (acc.isIncomeExpense()) { s.setPayeeId(QString()); t.modifySplit(s); } } } } d->m_transaction = t; // make sure that the transaction does not have an id so that we can enter // it into the engine d->m_transaction.clearId(); } void MyMoneySchedule::setEndDate(const QDate& date) { Q_D(MyMoneySchedule); d->m_endDate = date; } void MyMoneySchedule::setLastDayInMonth(bool state) { Q_D(MyMoneySchedule); d->m_lastDayInMonth = state; } void MyMoneySchedule::setAutoEnter(bool autoenter) { Q_D(MyMoneySchedule); d->m_autoEnter = autoenter; } QDate MyMoneySchedule::startDate() const { Q_D(const MyMoneySchedule); if (d->m_startDate.isValid()) return d->m_startDate; return nextDueDate(); } eMyMoney::Schedule::PaymentType MyMoneySchedule::paymentType() const { Q_D(const MyMoneySchedule); return d->m_paymentType; } /** * Simple get method that returns true if the schedule is fixed. * * @return bool To indicate whether the instance is fixed. */ bool MyMoneySchedule::isFixed() const { Q_D(const MyMoneySchedule); return d->m_fixed; } /** * Simple get method that returns true if the schedule will end * at some time. * * @return bool Indicates whether the instance will end. */ bool MyMoneySchedule::willEnd() const { Q_D(const MyMoneySchedule); return d->m_endDate.isValid(); } QDate MyMoneySchedule::nextDueDate() const { Q_D(const MyMoneySchedule); return d->m_transaction.postDate(); } QDate MyMoneySchedule::adjustedNextDueDate() const { if (isFinished()) return QDate(); if (lastDayInMonth()) { QDate date = nextDueDate(); return adjustedDate(QDate(date.year(), date.month(), date.daysInMonth()), weekendOption()); } return adjustedDate(nextDueDate(), weekendOption()); } QDate MyMoneySchedule::adjustedDate(QDate date, Schedule::WeekendOption option) const { if (!date.isValid() || option == Schedule::WeekendOption::MoveNothing || isProcessingDate(date)) return date; int step = 1; if (option == Schedule::WeekendOption::MoveBefore) step = -1; while (!isProcessingDate(date)) date = date.addDays(step); return date; } void MyMoneySchedule::setNextDueDate(const QDate& date) { Q_D(MyMoneySchedule); if (date.isValid()) { d->m_transaction.setPostDate(date); // m_startDate = date; } } void MyMoneySchedule::setLastPayment(const QDate& date) { Q_D(MyMoneySchedule); // Delete all payments older than date QList::Iterator it; QList delList; for (it = d->m_recordedPayments.begin(); it != d->m_recordedPayments.end(); ++it) { if (*it < date || !date.isValid()) delList.append(*it); } for (it = delList.begin(); it != delList.end(); ++it) { d->m_recordedPayments.removeAll(*it); } d->m_lastPayment = date; if (!d->m_startDate.isValid()) d->m_startDate = date; } QString MyMoneySchedule::name() const { Q_D(const MyMoneySchedule); return d->m_name; } void MyMoneySchedule::setName(const QString& nm) { Q_D(MyMoneySchedule); d->m_name = nm; } eMyMoney::Schedule::WeekendOption MyMoneySchedule::weekendOption() const { Q_D(const MyMoneySchedule); return d->m_weekendOption; } void MyMoneySchedule::setOccurrence(Schedule::Occurrence occ) { auto occ2 = occ; auto mult = 1; simpleToCompoundOccurrence(mult, occ2); setOccurrencePeriod(occ2); setOccurrenceMultiplier(mult); } void MyMoneySchedule::setOccurrencePeriod(Schedule::Occurrence occ) { Q_D(MyMoneySchedule); d->m_occurrence = occ; } void MyMoneySchedule::setOccurrenceMultiplier(int occmultiplier) { Q_D(MyMoneySchedule); d->m_occurrenceMultiplier = occmultiplier < 1 ? 1 : occmultiplier; } void MyMoneySchedule::setType(Schedule::Type type) { Q_D(MyMoneySchedule); d->m_type = type; } void MyMoneySchedule::validate(bool id_check) const { /* Check the supplied instance is valid... * * To be valid it must not have the id set and have the following fields set: * * m_occurrence * m_type * m_startDate * m_paymentType * m_transaction * the transaction must contain at least one split (two is better ;-) ) */ Q_D(const MyMoneySchedule); if (id_check && !d->m_id.isEmpty()) throw MYMONEYEXCEPTION_CSTRING("ID for schedule not empty when required"); if (d->m_occurrence == Schedule::Occurrence::Any) throw MYMONEYEXCEPTION_CSTRING("Invalid occurrence type for schedule"); if (d->m_type == Schedule::Type::Any) throw MYMONEYEXCEPTION_CSTRING("Invalid type for schedule"); if (!nextDueDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("Invalid next due date for schedule"); if (d->m_paymentType == Schedule::PaymentType::Any) throw MYMONEYEXCEPTION_CSTRING("Invalid payment type for schedule"); if (d->m_transaction.splitCount() == 0) throw MYMONEYEXCEPTION_CSTRING("Scheduled transaction does not contain splits"); // Check the payment types switch (d->m_type) { case Schedule::Type::Bill: if (d->m_paymentType == Schedule::PaymentType::DirectDeposit || d->m_paymentType == Schedule::PaymentType::ManualDeposit) throw MYMONEYEXCEPTION_CSTRING("Invalid payment type for bills"); break; case Schedule::Type::Deposit: if (d->m_paymentType == Schedule::PaymentType::DirectDebit || d->m_paymentType == Schedule::PaymentType::WriteChecque) throw MYMONEYEXCEPTION_CSTRING("Invalid payment type for deposits"); break; case Schedule::Type::Any: throw MYMONEYEXCEPTION_CSTRING("Invalid type ANY"); break; case Schedule::Type::Transfer: // if (m_paymentType == DirectDeposit || m_paymentType == ManualDeposit) // return false; break; case Schedule::Type::LoanPayment: break; } } QDate MyMoneySchedule::adjustedNextPayment(const QDate& refDate) const { return nextPaymentDate(true, refDate); } QDate MyMoneySchedule::adjustedNextPayment() const { return adjustedNextPayment(QDate::currentDate()); } QDate MyMoneySchedule::nextPayment(const QDate& refDate) const { return nextPaymentDate(false, refDate); } QDate MyMoneySchedule::nextPayment() const { return nextPayment(QDate::currentDate()); } QDate MyMoneySchedule::nextPaymentDate(const bool& adjust, const QDate& refDate) const { Schedule::WeekendOption option(adjust ? weekendOption() : Schedule::WeekendOption::MoveNothing); Q_D(const MyMoneySchedule); QDate adjEndDate(adjustedDate(d->m_endDate, option)); // if the enddate is valid and it is before the reference date, // then there will be no more payments. if (adjEndDate.isValid() && adjEndDate < refDate) { return QDate(); } QDate dueDate(nextDueDate()); QDate paymentDate(adjustedDate(dueDate, option)); if (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))) { switch (d->m_occurrence) { case Schedule::Occurrence::Once: // If the lastPayment is already set or the payment should have been // prior to the reference date then invalidate the payment date. if (d->m_lastPayment.isValid() || paymentDate <= refDate) paymentDate = QDate(); break; case Schedule::Occurrence::Daily: { int step = d->m_occurrenceMultiplier; do { dueDate = dueDate.addDays(step); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); } break; case Schedule::Occurrence::Weekly: { int step = 7 * d->m_occurrenceMultiplier; do { dueDate = dueDate.addDays(step); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); } break; case Schedule::Occurrence::EveryHalfMonth: do { dueDate = addHalfMonths(dueDate, d->m_occurrenceMultiplier); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); break; case Schedule::Occurrence::Monthly: do { dueDate = dueDate.addMonths(d->m_occurrenceMultiplier); fixDate(dueDate); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); break; case Schedule::Occurrence::Yearly: do { dueDate = dueDate.addYears(d->m_occurrenceMultiplier); fixDate(dueDate); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); break; case Schedule::Occurrence::Any: default: paymentDate = QDate(); break; } } if (paymentDate.isValid() && adjEndDate.isValid() && paymentDate > adjEndDate) paymentDate = QDate(); return paymentDate; } QDate MyMoneySchedule::nextPaymentDate(const bool& adjust) const { return nextPaymentDate(adjust, QDate::currentDate()); } QList MyMoneySchedule::paymentDates(const QDate& _startDate, const QDate& _endDate) const { QDate paymentDate(nextDueDate()); QList theDates; Schedule::WeekendOption option(weekendOption()); Q_D(const MyMoneySchedule); QDate endDate(_endDate); if (willEnd() && d->m_endDate < endDate) { // consider the adjusted end date instead of the plain end date endDate = adjustedDate(d->m_endDate, option); } QDate start_date(adjustedDate(startDate(), option)); // if the period specified by the parameters and the adjusted period // defined for this schedule don't overlap, then the list remains empty if ((willEnd() && adjustedDate(d->m_endDate, option) < _startDate) || start_date > endDate) return theDates; QDate date(adjustedDate(paymentDate, option)); switch (d->m_occurrence) { case Schedule::Occurrence::Once: if (start_date >= _startDate && start_date <= endDate) theDates.append(start_date); break; case Schedule::Occurrence::Daily: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addDays(d->m_occurrenceMultiplier); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Weekly: { int step = 7 * d->m_occurrenceMultiplier; while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addDays(step); date = adjustedDate(paymentDate, option); } } break; case Schedule::Occurrence::EveryHalfMonth: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = addHalfMonths(paymentDate, d->m_occurrenceMultiplier); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Monthly: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addMonths(d->m_occurrenceMultiplier); fixDate(paymentDate); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Yearly: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addYears(d->m_occurrenceMultiplier); fixDate(paymentDate); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Any: default: break; } return theDates; } bool MyMoneySchedule::operator <(const MyMoneySchedule& right) const { return adjustedNextDueDate() < right.adjustedNextDueDate(); } bool MyMoneySchedule::operator ==(const MyMoneySchedule& right) const { Q_D(const MyMoneySchedule); auto d2 = static_cast(right.d_func()); if (MyMoneyObject::operator==(right) && d->m_occurrence == d2->m_occurrence && d->m_occurrenceMultiplier == d2->m_occurrenceMultiplier && d->m_type == d2->m_type && d->m_startDate == d2->m_startDate && d->m_paymentType == d2->m_paymentType && d->m_fixed == d2->m_fixed && d->m_transaction == d2->m_transaction && d->m_endDate == d2->m_endDate && d->m_lastDayInMonth == d2->m_lastDayInMonth && d->m_autoEnter == d2->m_autoEnter && d->m_lastPayment == d2->m_lastPayment && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name))) return true; return false; } bool MyMoneySchedule::operator !=(const MyMoneySchedule& right) const { return ! operator==(right); } int MyMoneySchedule::transactionsRemaining() const { Q_D(const MyMoneySchedule); return transactionsRemainingUntil(adjustedDate(d->m_endDate, weekendOption())); } int MyMoneySchedule::transactionsRemainingUntil(const QDate& endDate) const { auto counter = 0; Q_D(const MyMoneySchedule); QDate startDate = d->m_lastPayment.isValid() ? d->m_lastPayment : d->m_startDate; if (startDate.isValid() && endDate.isValid()) { QList dates = paymentDates(startDate, endDate); counter = dates.count(); } return counter; } QDate MyMoneySchedule::endDate() const { Q_D(const MyMoneySchedule); return d->m_endDate; } bool MyMoneySchedule::autoEnter() const { Q_D(const MyMoneySchedule); return d->m_autoEnter; } bool MyMoneySchedule::lastDayInMonth() const { Q_D(const MyMoneySchedule); return d->m_lastDayInMonth; } MyMoneyTransaction MyMoneySchedule::transaction() const { Q_D(const MyMoneySchedule); return d->m_transaction; } QDate MyMoneySchedule::lastPayment() const { Q_D(const MyMoneySchedule); return d->m_lastPayment; } MyMoneyAccount MyMoneySchedule::account(int cnt) const { Q_D(const MyMoneySchedule); QList splits = d->m_transaction.splits(); QList::ConstIterator it; auto file = MyMoneyFile::instance(); MyMoneyAccount acc; // search the first asset or liability account for (it = splits.constBegin(); it != splits.constEnd() && (acc.id().isEmpty() || cnt); ++it) { try { acc = file->account((*it).accountId()); if (acc.isAssetLiability()) --cnt; if (!cnt) return acc; } catch (const MyMoneyException &) { qWarning("Schedule '%s' references unknown account '%s'", qPrintable(id()), qPrintable((*it).accountId())); return MyMoneyAccount(); } } return MyMoneyAccount(); } MyMoneyAccount MyMoneySchedule::transferAccount() const { return account(2); } QDate MyMoneySchedule::dateAfter(int transactions) const { auto counter = 1; QDate paymentDate(startDate()); if (transactions <= 0) return paymentDate; Q_D(const MyMoneySchedule); switch (d->m_occurrence) { case Schedule::Occurrence::Once: break; case Schedule::Occurrence::Daily: while (counter++ < transactions) paymentDate = paymentDate.addDays(d->m_occurrenceMultiplier); break; case Schedule::Occurrence::Weekly: { int step = 7 * d->m_occurrenceMultiplier; while (counter++ < transactions) paymentDate = paymentDate.addDays(step); } break; case Schedule::Occurrence::EveryHalfMonth: paymentDate = addHalfMonths(paymentDate, d->m_occurrenceMultiplier * (transactions - 1)); break; case Schedule::Occurrence::Monthly: while (counter++ < transactions) paymentDate = paymentDate.addMonths(d->m_occurrenceMultiplier); break; case Schedule::Occurrence::Yearly: while (counter++ < transactions) paymentDate = paymentDate.addYears(d->m_occurrenceMultiplier); break; case Schedule::Occurrence::Any: default: break; } return paymentDate; } bool MyMoneySchedule::isOverdue() const { if (isFinished()) return false; if (adjustedNextDueDate() >= QDate::currentDate()) return false; return true; } bool MyMoneySchedule::isFinished() const { Q_D(const MyMoneySchedule); if (!d->m_lastPayment.isValid()) return false; if (d->m_endDate.isValid()) { if (d->m_lastPayment >= d->m_endDate || !nextDueDate().isValid() || nextDueDate() > d->m_endDate) return true; } // Check to see if its a once off payment if (d->m_occurrence == Schedule::Occurrence::Once) return true; return false; } bool MyMoneySchedule::hasRecordedPayment(const QDate& date) const { Q_D(const MyMoneySchedule); // m_lastPayment should always be > recordedPayments() if (d->m_lastPayment.isValid() && d->m_lastPayment >= date) return true; if (d->m_recordedPayments.contains(date)) return true; return false; } void MyMoneySchedule::recordPayment(const QDate& date) { Q_D(MyMoneySchedule); d->m_recordedPayments.append(date); } QList MyMoneySchedule::recordedPayments() const { Q_D(const MyMoneySchedule); return d->m_recordedPayments; } void MyMoneySchedule::setWeekendOption(const Schedule::WeekendOption option) { Q_D(MyMoneySchedule); // make sure only valid values are used. Invalid defaults to MoveNothing. switch (option) { case Schedule::WeekendOption::MoveBefore: case Schedule::WeekendOption::MoveAfter: d->m_weekendOption = option; break; default: d->m_weekendOption = Schedule::WeekendOption::MoveNothing; break; } } void MyMoneySchedule::fixDate(QDate& date) const { Q_D(const MyMoneySchedule); QDate fixDate(d->m_startDate); if (fixDate.isValid() && date.day() != fixDate.day() && QDate::isValid(date.year(), date.month(), fixDate.day())) { date = QDate(date.year(), date.month(), fixDate.day()); } } bool MyMoneySchedule::hasReferenceTo(const QString& id) const { Q_D(const MyMoneySchedule); return d->m_transaction.hasReferenceTo(id); } QString MyMoneySchedule::occurrenceToString() const { - return occurrenceToString(occurrenceMultiplier(), occurrencePeriod()); + return occurrenceToString(occurrenceMultiplier(), occurrence()); } QString MyMoneySchedule::occurrenceToString(Schedule::Occurrence occurrence) { QString occurrenceString = I18N_NOOP2("Frequency of schedule", "Any"); if (occurrence == Schedule::Occurrence::Once) occurrenceString = I18N_NOOP2("Frequency of schedule", "Once"); else if (occurrence == Schedule::Occurrence::Daily) occurrenceString = I18N_NOOP2("Frequency of schedule", "Daily"); else if (occurrence == Schedule::Occurrence::Weekly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Weekly"); else if (occurrence == Schedule::Occurrence::Fortnightly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Fortnightly"); else if (occurrence == Schedule::Occurrence::EveryOtherWeek) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other week"); else if (occurrence == Schedule::Occurrence::EveryHalfMonth) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every half month"); else if (occurrence == Schedule::Occurrence::EveryThreeWeeks) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three weeks"); else if (occurrence == Schedule::Occurrence::EveryFourWeeks) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four weeks"); else if (occurrence == Schedule::Occurrence::EveryThirtyDays) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every thirty days"); else if (occurrence == Schedule::Occurrence::Monthly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Monthly"); else if (occurrence == Schedule::Occurrence::EveryEightWeeks) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every eight weeks"); else if (occurrence == Schedule::Occurrence::EveryOtherMonth) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every two months"); else if (occurrence == Schedule::Occurrence::EveryThreeMonths) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three months"); else if (occurrence == Schedule::Occurrence::Quarterly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Quarterly"); else if (occurrence == Schedule::Occurrence::EveryFourMonths) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four months"); else if (occurrence == Schedule::Occurrence::TwiceYearly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Twice yearly"); else if (occurrence == Schedule::Occurrence::Yearly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Yearly"); else if (occurrence == Schedule::Occurrence::EveryOtherYear) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other year"); return occurrenceString; } QString MyMoneySchedule::occurrenceToString(int mult, Schedule::Occurrence type) { QString occurrenceString = I18N_NOOP2("Frequency of schedule", "Any"); if (type == Schedule::Occurrence::Once) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Once"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("%1 times").arg(mult)); } else if (type == Schedule::Occurrence::Daily) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Daily"); break; case 30: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every thirty days"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 days").arg(mult)); } else if (type == Schedule::Occurrence::Weekly) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Weekly"); break; case 2: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other week"); break; case 3: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three weeks"); break; case 4: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four weeks"); break; case 8: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every eight weeks"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 weeks").arg(mult)); } else if (type == Schedule::Occurrence::EveryHalfMonth) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every half month"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 half months").arg(mult)); } else if (type == Schedule::Occurrence::Monthly) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Monthly"); break; case 2: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every two months"); break; case 3: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three months"); break; case 4: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four months"); break; case 6: occurrenceString = I18N_NOOP2("Frequency of schedule", "Twice yearly"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 months").arg(mult)); } else if (type == Schedule::Occurrence::Yearly) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Yearly"); break; case 2: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other year"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 years").arg(mult)); } return occurrenceString; } QString MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence type) { QString occurrenceString = I18N_NOOP2("Schedule occurrence period", "Any"); if (type == Schedule::Occurrence::Once) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Once"); else if (type == Schedule::Occurrence::Daily) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Day"); else if (type == Schedule::Occurrence::Weekly) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Week"); else if (type == Schedule::Occurrence::EveryHalfMonth) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Half-month"); else if (type == Schedule::Occurrence::Monthly) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Month"); else if (type == Schedule::Occurrence::Yearly) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Year"); return occurrenceString; } QString MyMoneySchedule::scheduleTypeToString(Schedule::Type type) { QString text; switch (type) { case Schedule::Type::Bill: text = I18N_NOOP2("Scheduled transaction type", "Bill"); break; case Schedule::Type::Deposit: text = I18N_NOOP2("Scheduled transaction type", "Deposit"); break; case Schedule::Type::Transfer: text = I18N_NOOP2("Scheduled transaction type", "Transfer"); break; case Schedule::Type::LoanPayment: text = I18N_NOOP2("Scheduled transaction type", "Loan payment"); break; case Schedule::Type::Any: default: text = I18N_NOOP2("Scheduled transaction type", "Unknown"); } return text; } QString MyMoneySchedule::paymentMethodToString(Schedule::PaymentType paymentType) { QString text; switch (paymentType) { case Schedule::PaymentType::DirectDebit: text = I18N_NOOP2("Scheduled Transaction payment type", "Direct debit"); break; case Schedule::PaymentType::DirectDeposit: text = I18N_NOOP2("Scheduled Transaction payment type", "Direct deposit"); break; case Schedule::PaymentType::ManualDeposit: text = I18N_NOOP2("Scheduled Transaction payment type", "Manual deposit"); break; case Schedule::PaymentType::Other: text = I18N_NOOP2("Scheduled Transaction payment type", "Other"); break; case Schedule::PaymentType::WriteChecque: text = I18N_NOOP2("Scheduled Transaction payment type", "Write check"); break; case Schedule::PaymentType::StandingOrder: text = I18N_NOOP2("Scheduled Transaction payment type", "Standing order"); break; case Schedule::PaymentType::BankTransfer: text = I18N_NOOP2("Scheduled Transaction payment type", "Bank transfer"); break; case Schedule::PaymentType::Any: text = I18N_NOOP2("Scheduled Transaction payment type", "Any (Error)"); break; } return text; } QString MyMoneySchedule::weekendOptionToString(Schedule::WeekendOption weekendOption) { QString text; switch (weekendOption) { case Schedule::WeekendOption::MoveBefore: text = I18N_NOOP("Change the date to the previous processing day"); break; case Schedule::WeekendOption::MoveAfter: text = I18N_NOOP("Change the date to the next processing day"); break; case Schedule::WeekendOption::MoveNothing: text = I18N_NOOP("Do not change the date"); break; } return text; } // until we don't have the means to store the value // of the variation, we default to 10% in case this // scheduled transaction is marked 'not fixed'. // // ipwizard 2009-04-18 int MyMoneySchedule::variation() const { int rc = 0; if (!isFixed()) { rc = 10; #if 0 QString var = value("kmm-variation"); if (!var.isEmpty()) rc = var.toInt(); #endif } return rc; } void MyMoneySchedule::setVariation(int var) { Q_UNUSED(var) #if 0 deletePair("kmm-variation"); if (var != 0) setValue("kmm-variation", QString("%1").arg(var)); #endif } int MyMoneySchedule::eventsPerYear(Schedule::Occurrence occurrence) { int rc = 0; switch (occurrence) { case Schedule::Occurrence::Daily: rc = 365; break; case Schedule::Occurrence::Weekly: rc = 52; break; case Schedule::Occurrence::Fortnightly: rc = 26; break; case Schedule::Occurrence::EveryOtherWeek: rc = 26; break; case Schedule::Occurrence::EveryHalfMonth: rc = 24; break; case Schedule::Occurrence::EveryThreeWeeks: rc = 17; break; case Schedule::Occurrence::EveryFourWeeks: rc = 13; break; case Schedule::Occurrence::Monthly: case Schedule::Occurrence::EveryThirtyDays: rc = 12; break; case Schedule::Occurrence::EveryEightWeeks: rc = 6; break; case Schedule::Occurrence::EveryOtherMonth: rc = 6; break; case Schedule::Occurrence::EveryThreeMonths: case Schedule::Occurrence::Quarterly: rc = 4; break; case Schedule::Occurrence::EveryFourMonths: rc = 3; break; case Schedule::Occurrence::TwiceYearly: rc = 2; break; case Schedule::Occurrence::Yearly: rc = 1; break; default: qWarning("Occurrence not supported by financial calculator"); } return rc; } int MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence occurrence) { int rc = 0; switch (occurrence) { case Schedule::Occurrence::Daily: rc = 1; break; case Schedule::Occurrence::Weekly: rc = 7; break; case Schedule::Occurrence::Fortnightly: rc = 14; break; case Schedule::Occurrence::EveryOtherWeek: rc = 14; break; case Schedule::Occurrence::EveryHalfMonth: rc = 15; break; case Schedule::Occurrence::EveryThreeWeeks: rc = 21; break; case Schedule::Occurrence::EveryFourWeeks: rc = 28; break; case Schedule::Occurrence::EveryThirtyDays: rc = 30; break; case Schedule::Occurrence::Monthly: rc = 30; break; case Schedule::Occurrence::EveryEightWeeks: rc = 56; break; case Schedule::Occurrence::EveryOtherMonth: rc = 60; break; case Schedule::Occurrence::EveryThreeMonths: case Schedule::Occurrence::Quarterly: rc = 90; break; case Schedule::Occurrence::EveryFourMonths: rc = 120; break; case Schedule::Occurrence::TwiceYearly: rc = 180; break; case Schedule::Occurrence::Yearly: rc = 360; break; default: qWarning("Occurrence not supported by financial calculator"); } return rc; } QDate MyMoneySchedule::addHalfMonths(QDate date, int mult) const { QDate newdate = date; int d, dm; if (mult > 0) { d = newdate.day(); if (d <= 12) { if (mult % 2 == 0) newdate = newdate.addMonths(mult >> 1); else newdate = newdate.addMonths(mult >> 1).addDays(15); } else for (int i = 0; i < mult; i++) { if (d <= 13) newdate = newdate.addDays(15); else { dm = newdate.daysInMonth(); if (d == 14) newdate = newdate.addDays((dm < 30) ? dm - d : 15); else if (d == 15) newdate = newdate.addDays(dm - d); else if (d == dm) newdate = newdate.addDays(15 - d).addMonths(1); else newdate = newdate.addDays(-15).addMonths(1); } d = newdate.day(); } } else if (mult < 0) // Go backwards for (int i = 0; i > mult; i--) { d = newdate.day(); dm = newdate.daysInMonth(); if (d > 15) { dm = newdate.daysInMonth(); newdate = newdate.addDays((d == dm) ? 15 - dm : -15); } else if (d <= 13) newdate = newdate.addMonths(-1).addDays(15); else if (d == 15) newdate = newdate.addDays(-15); else { // 14 newdate = newdate.addMonths(-1); dm = newdate.daysInMonth(); newdate = newdate.addDays((dm < 30) ? dm - d : 15); } } return newdate; } /** * Helper method to convert simple occurrence to compound occurrence + multiplier * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ void MyMoneySchedule::simpleToCompoundOccurrence(int& multiplier, Schedule::Occurrence& occurrence) { Schedule::Occurrence newOcc = occurrence; int newMulti = 1; if (occurrence == Schedule::Occurrence::Once || occurrence == Schedule::Occurrence::Daily || occurrence == Schedule::Occurrence::Weekly || occurrence == Schedule::Occurrence::EveryHalfMonth || occurrence == Schedule::Occurrence::Monthly || occurrence == Schedule::Occurrence::Yearly) { // Already a base occurrence and multiplier } else if (occurrence == Schedule::Occurrence::Fortnightly || occurrence == Schedule::Occurrence::EveryOtherWeek) { newOcc = Schedule::Occurrence::Weekly; newMulti = 2; } else if (occurrence == Schedule::Occurrence::EveryThreeWeeks) { newOcc = Schedule::Occurrence::Weekly; newMulti = 3; } else if (occurrence == Schedule::Occurrence::EveryFourWeeks) { newOcc = Schedule::Occurrence::Weekly; newMulti = 4; } else if (occurrence == Schedule::Occurrence::EveryThirtyDays) { newOcc = Schedule::Occurrence::Daily; newMulti = 30; } else if (occurrence == Schedule::Occurrence::EveryEightWeeks) { newOcc = Schedule::Occurrence::Weekly; newMulti = 8; } else if (occurrence == Schedule::Occurrence::EveryOtherMonth) { newOcc = Schedule::Occurrence::Monthly; newMulti = 2; } else if (occurrence == Schedule::Occurrence::EveryThreeMonths || occurrence == Schedule::Occurrence::Quarterly) { newOcc = Schedule::Occurrence::Monthly; newMulti = 3; } else if (occurrence == Schedule::Occurrence::EveryFourMonths) { newOcc = Schedule::Occurrence::Monthly; newMulti = 4; } else if (occurrence == Schedule::Occurrence::TwiceYearly) { newOcc = Schedule::Occurrence::Monthly; newMulti = 6; } else if (occurrence == Schedule::Occurrence::EveryOtherYear) { newOcc = Schedule::Occurrence::Yearly; newMulti = 2; } else { // Unknown newOcc = Schedule::Occurrence::Any; newMulti = 1; } if (newOcc != occurrence) { occurrence = newOcc; multiplier = newMulti == 1 ? multiplier : newMulti * multiplier; } } /** * Helper method to convert compound occurrence + multiplier to simple occurrence * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ void MyMoneySchedule::compoundToSimpleOccurrence(int& multiplier, Schedule::Occurrence& occurrence) { Schedule::Occurrence newOcc = occurrence; if (occurrence == Schedule::Occurrence::Once) { // Nothing to do } else if (occurrence == Schedule::Occurrence::Daily) { switch (multiplier) { case 1: break; case 30: newOcc = Schedule::Occurrence::EveryThirtyDays; break; } } else if (newOcc == Schedule::Occurrence::Weekly) { switch (multiplier) { case 1: break; case 2: newOcc = Schedule::Occurrence::EveryOtherWeek; break; case 3: newOcc = Schedule::Occurrence::EveryThreeWeeks; break; case 4: newOcc = Schedule::Occurrence::EveryFourWeeks; break; case 8: newOcc = Schedule::Occurrence::EveryEightWeeks; break; } } else if (occurrence == Schedule::Occurrence::Monthly) switch (multiplier) { case 1: break; case 2: newOcc = Schedule::Occurrence::EveryOtherMonth; break; case 3: newOcc = Schedule::Occurrence::EveryThreeMonths; break; case 4: newOcc = Schedule::Occurrence::EveryFourMonths; break; case 6: newOcc = Schedule::Occurrence::TwiceYearly; break; } else if (occurrence == Schedule::Occurrence::EveryHalfMonth) switch (multiplier) { case 1: break; } else if (occurrence == Schedule::Occurrence::Yearly) { switch (multiplier) { case 1: break; case 2: newOcc = Schedule::Occurrence::EveryOtherYear; break; } } if (occurrence != newOcc) { // Changed to derived type occurrence = newOcc; multiplier = 1; } } void MyMoneySchedule::setProcessingCalendar(IMyMoneyProcessingCalendar* pc) { processingCalendarPtr = pc; } bool MyMoneySchedule::isProcessingDate(const QDate& date) const { if (processingCalendarPtr) return processingCalendarPtr->isProcessingDate(date); /// @todo test against m_processingDays instead? (currently only for tests) return date.dayOfWeek() < Qt::Saturday; } IMyMoneyProcessingCalendar* MyMoneySchedule::processingCalendar() const { return processingCalendarPtr; } bool MyMoneySchedule::replaceId(const QString& newId, const QString& oldId) { Q_D(MyMoneySchedule); return d->m_transaction.replaceId(newId, oldId); } diff --git a/kmymoney/mymoney/mymoneyschedule.h b/kmymoney/mymoney/mymoneyschedule.h index 25f0d04e6..2e5fc18b8 100644 --- a/kmymoney/mymoney/mymoneyschedule.h +++ b/kmymoney/mymoney/mymoneyschedule.h @@ -1,730 +1,731 @@ /* * Copyright 2000-2004 Michael Edwardes * Copyright 2002-2018 Thomas Baumgart * Copyright 2005 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MYMONEYSCHEDULE_H #define MYMONEYSCHEDULE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" #include "mymoneyobject.h" class QString; class QDate; class IMyMoneyProcessingCalendar; class MyMoneyAccount; class MyMoneyTransaction; namespace eMyMoney { namespace Schedule { enum class Type; enum class Occurrence; enum class PaymentType; enum class WeekendOption; } } template class QList; /** * @author Michael Edwardes */ /** * This class represents a schedule. (A series of bills, deposits or * transfers). * * @short A class to represent a schedule. * @see MyMoneyScheduled */ class MyMoneySchedulePrivate; class KMM_MYMONEY_EXPORT MyMoneySchedule : public MyMoneyObject { Q_DECLARE_PRIVATE(MyMoneySchedule) friend class MyMoneyStorageANON; KMM_MYMONEY_UNIT_TESTABLE public: /** * Standard constructor */ MyMoneySchedule(); explicit MyMoneySchedule(const QString &id); /** * Constructor for initialising the object. * * Please note that the optional fields are not set and the transaction * MUST be set before it can be used. * * @a startDate is not used anymore and internally set to QDate() */ explicit MyMoneySchedule(const QString& name, eMyMoney::Schedule::Type type, eMyMoney::Schedule::Occurrence occurrence, int occurrenceMultiplier, eMyMoney::Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, bool fixed, bool autoEnter); MyMoneySchedule(const QString& id, const MyMoneySchedule& other); MyMoneySchedule(const MyMoneySchedule & other); MyMoneySchedule(MyMoneySchedule && other); MyMoneySchedule & operator=(MyMoneySchedule other); friend void swap(MyMoneySchedule& first, MyMoneySchedule& second); /** * Standard destructor */ ~MyMoneySchedule(); /** - * Simple get method that returns the occurrence frequency. + * Simple get method that returns the base occurrence frequency. * - * @return eMyMoney::Schedule::Occurrence The instance frequency. + * @return eMyMoney::Schedule::Occurrence The instance frequency + * reduced to the simple units. */ - eMyMoney::Schedule::Occurrence occurrence() const; + eMyMoney::Schedule::Occurrence baseOccurrence() const; /** * Simple get method that returns the occurrence period * multiplier and occurrence * * @return eMyMoney::Schedule::Occurrence The instance period * */ - eMyMoney::Schedule::Occurrence occurrencePeriod() const; + eMyMoney::Schedule::Occurrence occurrence() const; /** * Simple get method that returns the occurrence period multiplier. * * @return int The frequency multiplier */ int occurrenceMultiplier() const; /** * Simple get method that returns the schedule type. * * @return eMyMoney::Schedule::Type The instance type. */ eMyMoney::Schedule::Type type() const; /** * Simple get method that returns the schedule startDate. If * the schedule has been executed once, the date of the first * execution is returned. Otherwise, the next due date is * returned. * * @return reference to QDate containing the start date. */ QDate startDate() const; /** * Simple get method that returns the schedule paymentType. * * @return eMyMoney::Schedule::PaymentType The instance paymentType. */ eMyMoney::Schedule::PaymentType paymentType() const; /** * Simple get method that returns true if the schedule is fixed. * * @return bool To indicate whether the instance is fixed. */ bool isFixed() const; /** * Simple get method that returns true if the schedule will end * at some time. * * @return bool Indicates whether the instance will end. */ bool willEnd() const; /** * Simple get method that returns the number of transactions remaining. * * @return int The number of transactions remaining for the instance. */ int transactionsRemaining() const; /** * Simple method that returns the number of transactions remaining * until a given date. * * @param endDate Date to count transactions to. * @return int The number of transactions remaining for the instance. */ int transactionsRemainingUntil(const QDate& endDate) const; /** * Simple get method that returns the schedule end date. * * @return QDate The end date for the instance. */ QDate endDate() const; /** * Get the state if the schedule should be processed at the last day * of a month * * @return state of the flag */ bool lastDayInMonth() const; /** * Simple get method that returns true if the transaction should be * automatically entered into the register. * * @return bool Indicates whether the instance will be automatically entered. */ bool autoEnter() const; /** * Simple get method that returns the transaction data for the schedule. * * @return MyMoneyTransaction The transaction data for the instance. */ MyMoneyTransaction transaction() const; /** * Simple method that sets the transaction for the schedule. * The transaction must have a valid postDate set, otherwise * it will not be accepted. This test is bypassed, if @a noDateCheck * is set to true * * @param transaction The new transaction. * @param noDateCheck if @a true, the date check is bypassed * @return none */ void setTransaction(const MyMoneyTransaction& transaction, bool noDateCheck); /** * Simple method that returns the schedules last payment. If the * schedule has never been executed, QDate() will be returned. * * @return QDate The last payment for the schedule. */ QDate lastPayment() const; /** * Simple method that returns the next due date for the schedule. * * @return reference to QDate containing the next due date. * * @note The date returned can represent a value that is past * a possible end of the schedule. Make sure to consider * the return value of isFinished() when using the value returned. */ QDate nextDueDate() const; /** * This method returns the next due date adjusted * according to the rules specified by the schedule's weekend option. * * @return QDate containing the adjusted next due date. If the * schedule is finished (@sa isFinished()) then the method * returns an invalid QDate. * * @sa weekendOption() * @sa adjustedDate() */ QDate adjustedNextDueDate() const; /** * This method adjusts returns the date adjusted according to the * rules specified by the schedule's weekend option. * * @return QDate containing the adjusted date. */ QDate adjustedDate(QDate date, eMyMoney::Schedule::WeekendOption option) const; /** * Get the weekendOption that determines how the schedule check code * will enter transactions that occur on a non-processing day (usually * a weekend). * * This not used by MyMoneySchedule but by the support code. **/ eMyMoney::Schedule::WeekendOption weekendOption() const; /** * Simple method that sets the frequency for the schedule. * * @param occ The new occurrence (frequency). * @return none */ void setOccurrence(eMyMoney::Schedule::Occurrence occ); /** * Simple method that sets the schedule period * * @param occ The new occurrence period (frequency) * @return none */ void setOccurrencePeriod(eMyMoney::Schedule::Occurrence occ); /** * Simple method that sets the frequency multiplier for the schedule. * * @param occmultiplier The new occurrence (frequency) multiplier. * @return none */ void setOccurrenceMultiplier(int occmultiplier); /** * Simple method that sets the type for the schedule. * * @param type The new type. * @return none */ void setType(eMyMoney::Schedule::Type type); /** * Simple method that sets the start date for the schedule. * * @param date The new start date. * @return none */ void setStartDate(const QDate& date); /** * Simple method that sets the payment type for the schedule. * * @param type The new payment type. * @return none */ void setPaymentType(eMyMoney::Schedule::PaymentType type); /** * Simple method to set whether the schedule is fixed or not. * * @param fixed boolean to indicate whether the instance is fixed. * @return none */ void setFixed(bool fixed); /** * Simple method that sets the transaction for the schedule. * The transaction must have a valid postDate set, otherwise * it will not be accepted. * * @param transaction The new transaction. * @return none */ void setTransaction(const MyMoneyTransaction& transaction); /** * Simple set method to set the end date for the schedule. * * @param date The new end date. * @return none */ void setEndDate(const QDate& date); /** * Simple method to set whether the schedule should be performed at * the last day of a month. * * @param state boolean The state to set * @return none */ void setLastDayInMonth(bool state); /** * Simple set method to set whether this transaction should be automatically * entered into the journal whenever it is due. * * @param autoenter boolean to indicate whether we need to automatically * enter the transaction. * @return none */ void setAutoEnter(bool autoenter); /** * Simple set method to set the schedule's next payment date. * * @param date The next payment date. * @return none */ void setNextDueDate(const QDate& date); /** * Simple set method to set the schedule's last payment. If * this method is called for the first time on the object, * the @a m_startDate member will be set to @a date as well. * * This method should be called whenever a schedule is entered or skipped. * * @param date The last payment date. * @return none */ void setLastPayment(const QDate& date); /** * Set the weekendOption that determines how the schedule check code * will enter transactions that occur on a non-processing day (usually * a weekend). The following values * are valid: * * - MoveNothing: don't modify date * - MoveBefore: modify the date to the previous processing day * - MoveAfter: modify the date to the next processing day * * If an invalid option is given, the option is set to MoveNothing. * * @param option See list in description * @return none * * @note This not used by MyMoneySchedule but by the support code. **/ void setWeekendOption(const eMyMoney::Schedule::WeekendOption option); /** * Validates the schedule instance. * * Makes sure the paymentType matches the type and that the required * fields have been set. * * @param id_check if @p true, the method will check for an empty id. * if @p false, this check is skipped. Default is @p true. * * @return If this method returns, all checks are passed. Otherwise, * it will throw a MyMoneyException object. * * @exception MyMoneyException with detailed error information is thrown * in case of failure of any check. */ void validate(bool id_check = true) const; /** * Calculates the date of the next payment adjusted according to the * rules specified by the schedule's weekend option. * * @param refDate The reference date from which the next payment * date will be calculated (defaults to current date) * * @return QDate The adjusted date the next payment is due. This date is * always past @a refDate. In case of an error or if there * are no more payments then an empty/invalid QDate() will * be returned. */ QDate adjustedNextPayment(const QDate& refDate) const; QDate adjustedNextPayment() const; /** * Calculates the date of the next payment. * * @param refDate The reference date from which the next payment * date will be calculated (defaults to current date) * * @return QDate The date the next payment is due. This date is * always past @a refDate. In case of an error or * if there are no more payments then an empty/invalid QDate() * will be returned. */ QDate nextPayment(const QDate& refDate) const; QDate nextPayment() const; /** * Calculates the date of the next payment and adjusts if asked. * * @param adjust Whether to adjust the calculated date according to the * rules specified by the schedule's weekend option. * @param refDate The reference date from which the next payment * date will be calculated (defaults to current date) * * @return QDate The date the next payment is due. This date is * always past @a refDate. In case of an error or * if there is no more payments then an empty/invalid QDate() * will be returned. */ QDate nextPaymentDate(const bool& adjust, const QDate& refDate) const; QDate nextPaymentDate(const bool& adjust) const; /** * Calculates the dates of the payment over a certain period of time. * * An empty list is returned for no payments or error. * * @param startDate The start date for the range calculations * @param endDate The end date for the range calculations. * @return QList The dates on which the payments are due. */ QList paymentDates(const QDate& startDate, const QDate& endDate) const; /** * Returns the instances name * * @return The name */ QString name() const; /** * Changes the instance name * * @param nm The new name * @return none */ void setName(const QString& nm); bool operator ==(const MyMoneySchedule& right) const; bool operator !=(const MyMoneySchedule& right) const; bool operator <(const MyMoneySchedule& right) const; MyMoneyAccount account(int cnt = 1) const; MyMoneyAccount transferAccount() const; QDate dateAfter(int transactions) const; bool isOverdue() const; bool isFinished() const; bool hasRecordedPayment(const QDate&) const; void recordPayment(const QDate&); QList recordedPayments() const; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ virtual bool hasReferenceTo(const QString& id) const final override; /** * This method replaces all occurrences of id @a oldId with * @a newId. All other ids are not changed. * * @return true if any change has been performed * @return false if nothing has been modified */ bool replaceId(const QString& newId, const QString& oldId); /** * Returns the human-readable format of Schedule's occurrence * * @return QString representing the human readable format */ QString occurrenceToString() const; /** * This method is used to convert the occurrence type from its * internal representation into a human readable format. * * @param type numerical representation of the MyMoneySchedule * occurrence type * * @return QString representing the human readable format */ static QString occurrenceToString(eMyMoney::Schedule::Occurrence type); /** * This method is used to convert a multiplier and base occurrence type * from its internal representation into a human readable format. * When multiplier * occurrence is equivalent to a simple occurrence * the method returns the same as occurrenceToString of the simple occurrence * * @param mult occurrence multiplier * @param type occurrence period * * @return QString representing the human readable format */ static QString occurrenceToString(int mult, eMyMoney::Schedule::Occurrence type); /** * This method is used to convert an occurrence period from * its internal representation into a human-readable format. * * @param type numerical representation of the MyMoneySchedule * occurrence type * * @return QString representing the human readable format */ static QString occurrencePeriodToString(eMyMoney::Schedule::Occurrence type); /** * This method is used to convert the payment type from its * internal representation into a human readable format. * * @param paymentType numerical representation of the MyMoneySchedule * payment type * * @return QString representing the human readable format */ static QString paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType); /** * This method is used to convert the schedule weekend option from its * internal representation into a human readable format. * * @param weekendOption numerical representation of the MyMoneySchedule * weekend option * * @return QString representing the human readable format */ static QString weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption); /** * This method is used to convert the schedule type from its * internal representation into a human readable format. * * @param type numerical representation of the MyMoneySchedule * schedule type * * @return QString representing the human readable format */ static QString scheduleTypeToString(eMyMoney::Schedule::Type type); int variation() const; void setVariation(int var); /** * * Convert an occurrence to the maximum number of events possible during a single * calendar year. * A fortnight is treated as 15 days. * * @param occurrence The occurrence * * @return int Number of days between events */ static int eventsPerYear(eMyMoney::Schedule::Occurrence occurrence); /** * * Convert an occurrence to the number of days between events * Treats a month as 30 days. * Treats a fortnight as 15 days. * * @param occurrence The occurrence * * @return int Number of days between events */ static int daysBetweenEvents(eMyMoney::Schedule::Occurrence occurrence); /** * Helper method to convert simple occurrence to compound occurrence + multiplier * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ static void simpleToCompoundOccurrence(int& multiplier, eMyMoney::Schedule::Occurrence& occurrence); /** * Helper method to convert compound occurrence + multiplier to simple occurrence * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ static void compoundToSimpleOccurrence(int& multiplier, eMyMoney::Schedule::Occurrence& occurrence); /** * This method is used to set the static point to relevant * IMyMoneyProcessingCalendar. */ static void setProcessingCalendar(IMyMoneyProcessingCalendar* pc); private: /** * This method returns a pointer to the processing calendar object. * * @return const pointer to the current attached processing calendar object. * If no object is attached, returns 0. */ IMyMoneyProcessingCalendar* processingCalendar() const; /** * This method forces the day of the passed @p date to * be the day of the start date of this schedule kept * in m_startDate. It is internally used when calculating * the payment dates over several periods. * * @param date reference to QDate object to be checked and adjusted */ void fixDate(QDate& date) const; /** * This method adds a number of Half Months to the given Date. * This is used for EveryHalfMonth occurrences. * The addition uses the following rules to add a half month: * Day 1-13: add 15 days * Day 14: add 15 days (except February: the last day of the month) * Day 15: last day of the month * Day 16-29 (not last day in February): subtract 15 days and add 1 month * 30 and last day: 15th of next month * * This calculation pairs days 1 to 12 with 16 to 27. * Day 15 is paired with the last day of every month. * Repeated addition has issues in the following cases: * - Days 13 to 14 are paired with 28 to 29 until addition hits the last day of February * after which the (15,last) pair will be used. * - Addition from Day 30 leads immediately to the (15th,last) day pair. * * @param date The date * @param mult The number of half months to add. Default is 1. * * @return QDate date with mult half months added */ QDate addHalfMonths(QDate date, int mult = 1) const; /** * Checks if a given date should be considered a processing day * based on a calendar. See @a IMyMoneyProcessingCalendar and * setProcessingCalendar(). If no processingCalendar has been * setup using setProcessingCalendar it returns @c true on Mon..Fri * and @c false on Sat..Sun. */ bool isProcessingDate(const QDate& date) const; }; inline void swap(MyMoneySchedule& first, MyMoneySchedule& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneySchedule::MyMoneySchedule(MyMoneySchedule && other) : MyMoneySchedule() // krazy:exclude=inline { swap(*this, other); } inline MyMoneySchedule & MyMoneySchedule::operator=(MyMoneySchedule other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneySchedule objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneySchedule) #endif diff --git a/kmymoney/mymoney/storage/mymoneystoragemgr.cpp b/kmymoney/mymoney/storage/mymoneystoragemgr.cpp index 763f49694..b35dbb530 100644 --- a/kmymoney/mymoney/storage/mymoneystoragemgr.cpp +++ b/kmymoney/mymoney/storage/mymoneystoragemgr.cpp @@ -1,1899 +1,1899 @@ /* * Copyright 2002-2003 Michael Edwardes * Copyright 2002-2018 Thomas Baumgart * Copyright 2004 Kevin Tambascio * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Ł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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneystoragemgr_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyprice.h" MyMoneyStorageMgr::MyMoneyStorageMgr() : d_ptr(new MyMoneyStorageMgrPrivate(this)) { Q_D(MyMoneyStorageMgr); d->init(); } MyMoneyStorageMgr::~MyMoneyStorageMgr() { Q_D(MyMoneyStorageMgr); delete d; } MyMoneyPayee MyMoneyStorageMgr::user() const { Q_D(const MyMoneyStorageMgr); return d->m_user; } QDate MyMoneyStorageMgr::creationDate() const { Q_D(const MyMoneyStorageMgr); return d->m_creationDate; } QDate MyMoneyStorageMgr::lastModificationDate() const { Q_D(const MyMoneyStorageMgr); return d->m_lastModificationDate; } uint MyMoneyStorageMgr::currentFixVersion() const { Q_D(const MyMoneyStorageMgr); return d->m_currentFixVersion; } uint MyMoneyStorageMgr::fileFixVersion() const { Q_D(const MyMoneyStorageMgr); return d->m_fileFixVersion; } void MyMoneyStorageMgr::setUser(const MyMoneyPayee& user) { Q_D(MyMoneyStorageMgr); d->m_user = user; d->touch(); } void MyMoneyStorageMgr::setCreationDate(const QDate& val) { Q_D(MyMoneyStorageMgr); d->m_creationDate = val; d->touch(); } void MyMoneyStorageMgr::setLastModificationDate(const QDate& val) { Q_D(MyMoneyStorageMgr); d->m_lastModificationDate = val; d->m_dirty = false; } void MyMoneyStorageMgr::setFileFixVersion(uint v) { Q_D(MyMoneyStorageMgr); d->m_fileFixVersion = v; } bool MyMoneyStorageMgr::isStandardAccount(const QString& id) const { return id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity); } void MyMoneyStorageMgr::setAccountName(const QString& id, const QString& name) { Q_D(MyMoneyStorageMgr); if (!isStandardAccount(id)) throw MYMONEYEXCEPTION_CSTRING("Only standard accounts can be modified using setAccountName()"); auto acc = d->m_accountList[id]; acc.setName(name); d->m_accountList.modify(acc.id(), acc); } MyMoneyAccount MyMoneyStorageMgr::account(const QString& id) const { Q_D(const MyMoneyStorageMgr); // locate the account and if present, return it's data if (d->m_accountList.find(id) != d->m_accountList.end()) { auto acc = d->m_accountList[id]; // is that needed at all? if (acc.fraction() == -1) { const auto& sec = security(acc.currencyId()); acc.fraction(sec); } return acc; } // throw an exception, if it does not exist throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown account id '%1'").arg(id)); } MyMoneyAccount MyMoneyStorageMgr::accountByName(const QString& name) const { Q_D(const MyMoneyStorageMgr); if (name.isEmpty()) return MyMoneyAccount(); QMap::ConstIterator it_a; for (it_a = d->m_accountList.begin(); it_a != d->m_accountList.end(); ++it_a) { if ((*it_a).name() == name) { return *it_a; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown account '%1'").arg(name)); } void MyMoneyStorageMgr::accountList(QList& list) const { Q_D(const MyMoneyStorageMgr); foreach(const QString& accountId, d->m_accountList.keys()) { if (!isStandardAccount(accountId)) { list.append(account(accountId)); } } } void MyMoneyStorageMgr::addAccount(MyMoneyAccount& account) { Q_D(MyMoneyStorageMgr); // create the account. MyMoneyAccount newAccount(d->nextAccountID(), account); d->m_accountList.insert(newAccount.id(), newAccount); account = newAccount; } void MyMoneyStorageMgr::addPayee(MyMoneyPayee& payee) { Q_D(MyMoneyStorageMgr); // create the payee MyMoneyPayee newPayee(d->nextPayeeID(), payee); d->m_payeeList.insert(newPayee.id(), newPayee); payee = newPayee; } /** * @brief Add onlineJob to storage * @param job caller stays owner of the object, but id will be set */ void MyMoneyStorageMgr::addOnlineJob(onlineJob &job) { Q_D(MyMoneyStorageMgr); onlineJob newJob = onlineJob(d->nextOnlineJobID(), job); d->m_onlineJobList.insert(newJob.id(), newJob); job = newJob; } void MyMoneyStorageMgr::removeOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageMgr); if (!d->m_onlineJobList.contains(job.id())) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown onlineJob '%1' should be removed.").arg(job.id())); d->m_onlineJobList.remove(job.id()); } void MyMoneyStorageMgr::modifyOnlineJob(const onlineJob &job) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator iter = d->m_onlineJobList.find(job.id()); if (iter == d->m_onlineJobList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Got unknown onlineJob '%1' for modifying").arg(job.id())); onlineJob oldJob = iter.value(); d->m_onlineJobList.modify((*iter).id(), job); } onlineJob MyMoneyStorageMgr::getOnlineJob(const QString& id) const { Q_D(const MyMoneyStorageMgr); if (d->m_onlineJobList.contains(id)) { return d->m_onlineJobList[id]; } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown online Job '%1'").arg(id)); } ulong MyMoneyStorageMgr::onlineJobId() const { return 1; } MyMoneyPayee MyMoneyStorageMgr::payee(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_payeeList.find(id); if (it == d->m_payeeList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(id)); return *it; } MyMoneyPayee MyMoneyStorageMgr::payeeByName(const QString& payee) const { Q_D(const MyMoneyStorageMgr); if (payee.isEmpty()) return MyMoneyPayee::null; QMap::ConstIterator it_p; for (it_p = d->m_payeeList.begin(); it_p != d->m_payeeList.end(); ++it_p) { if ((*it_p).name() == payee) { return *it_p; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(payee)); } void MyMoneyStorageMgr::modifyPayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_payeeList.find(payee.id()); if (it == d->m_payeeList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(payee.id())); d->m_payeeList.modify((*it).id(), payee); } void MyMoneyStorageMgr::removePayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it_t; QMap::ConstIterator it_s; QMap::ConstIterator it_p; it_p = d->m_payeeList.find(payee.id()); if (it_p == d->m_payeeList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(payee.id())); // scan all transactions to check if the payee is still referenced for (it_t = d->m_transactionList.begin(); it_t != d->m_transactionList.end(); ++it_t) { if ((*it_t).hasReferenceTo(payee.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove payee that is still referenced to a %1").arg("transaction")); } } // check referential integrity in schedules for (it_s = d->m_scheduleList.begin(); it_s != d->m_scheduleList.end(); ++it_s) { if ((*it_s).hasReferenceTo(payee.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove payee that is still referenced to a %1").arg("schedule")); } } // remove any reference to report and/or budget d->removeReferences(payee.id()); d->m_payeeList.remove((*it_p).id()); } QList MyMoneyStorageMgr::payeeList() const { Q_D(const MyMoneyStorageMgr); return d->m_payeeList.values(); } void MyMoneyStorageMgr::addTag(MyMoneyTag& tag) { Q_D(MyMoneyStorageMgr); // create the tag MyMoneyTag newTag(d->nextTagID(), tag); d->m_tagList.insert(newTag.id(), newTag); tag = newTag; } MyMoneyTag MyMoneyStorageMgr::tag(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_tagList.find(id); if (it == d->m_tagList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(id)); return *it; } MyMoneyTag MyMoneyStorageMgr::tagByName(const QString& tag) const { Q_D(const MyMoneyStorageMgr); if (tag.isEmpty()) return MyMoneyTag::null; QMap::ConstIterator it_ta; for (it_ta = d->m_tagList.begin(); it_ta != d->m_tagList.end(); ++it_ta) { if ((*it_ta).name() == tag) { return *it_ta; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(tag)); } void MyMoneyStorageMgr::modifyTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_tagList.find(tag.id()); if (it == d->m_tagList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(tag.id())); d->m_tagList.modify((*it).id(), tag); } void MyMoneyStorageMgr::removeTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it_t; QMap::ConstIterator it_s; QMap::ConstIterator it_ta; it_ta = d->m_tagList.find(tag.id()); if (it_ta == d->m_tagList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(tag.id())); // scan all transactions to check if the tag is still referenced for (it_t = d->m_transactionList.begin(); it_t != d->m_transactionList.end(); ++it_t) { if ((*it_t).hasReferenceTo(tag.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove tag that is still referenced to a %1").arg("transaction")); } } // check referential integrity in schedules for (it_s = d->m_scheduleList.begin(); it_s != d->m_scheduleList.end(); ++it_s) { if ((*it_s).hasReferenceTo(tag.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove tag that is still referenced to a %1").arg("schedule")); } } // remove any reference to report and/or budget d->removeReferences(tag.id()); d->m_tagList.remove((*it_ta).id()); } QList MyMoneyStorageMgr::tagList() const { Q_D(const MyMoneyStorageMgr); return d->m_tagList.values(); } void MyMoneyStorageMgr::addAccount(MyMoneyAccount& parent, MyMoneyAccount& account) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator theParent; QMap::ConstIterator theChild; theParent = d->m_accountList.find(parent.id()); if (theParent == d->m_accountList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown parent account '%1'").arg(parent.id())); theChild = d->m_accountList.find(account.id()); if (theChild == d->m_accountList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown child account '%1'").arg(account.id())); auto acc = *theParent; acc.addAccountId(account.id()); d->m_accountList.modify(acc.id(), acc); parent = acc; acc = *theChild; acc.setParentAccountId(parent.id()); d->m_accountList.modify(acc.id(), acc); account = acc; } void MyMoneyStorageMgr::addInstitution(MyMoneyInstitution& institution) { Q_D(MyMoneyStorageMgr); MyMoneyInstitution newInstitution(d->nextInstitutionID(), institution); d->m_institutionList.insert(newInstitution.id(), newInstitution); // return new data institution = newInstitution; } uint MyMoneyStorageMgr::transactionCount(const QString& account) const { Q_D(const MyMoneyStorageMgr); uint cnt = 0; if (account.length() == 0) { cnt = d->m_transactionList.count(); } else { // scan all transactions foreach (const auto transaction, d->m_transactionList) { // scan all splits of this transaction auto found = false; foreach (const auto split, transaction.splits()) { // is it a split in our account? if (split.accountId() == account) { // since a transaction can only have one split referencing // each account, we're done with the splits here! found = true; break; } } // if no split contains the account id, continue with the // next transaction if (!found) continue; // otherwise count it ++cnt; } } return cnt; } QMap MyMoneyStorageMgr::transactionCountMap() const { Q_D(const MyMoneyStorageMgr); QMap map; // scan all transactions foreach (const auto transaction, d->m_transactionList) { // scan all splits of this transaction foreach (const auto split, transaction.splits()) { map[split.accountId()]++; } } return map; } uint MyMoneyStorageMgr::institutionCount() const { Q_D(const MyMoneyStorageMgr); return d->m_institutionList.count(); } uint MyMoneyStorageMgr::accountCount() const { Q_D(const MyMoneyStorageMgr); return d->m_accountList.count(); } void MyMoneyStorageMgr::addTransaction(MyMoneyTransaction& transaction, bool skipAccountUpdate) { Q_D(MyMoneyStorageMgr); // 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_CSTRING("transaction already contains an id"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("invalid post date"); // now check the splits foreach (const auto split, transaction.splits()) { // the following lines will throw an exception if the // account or payee do not exist account(split.accountId()); if (!split.payeeId().isEmpty()) payee(split.payeeId()); } MyMoneyTransaction newTransaction(d->nextTransactionID(), transaction); QString key = newTransaction.uniqueSortKey(); d->m_transactionList.insert(key, newTransaction); d->m_transactionKeys.insert(newTransaction.id(), key); transaction = newTransaction; // adjust the balance of all affected accounts foreach (const auto split, transaction.splits()) { auto acc = d->m_accountList[split.accountId()]; d->adjustBalance(acc, split, false); if (!skipAccountUpdate) { acc.touch(); } d->m_accountList.modify(acc.id(), acc); } } bool MyMoneyStorageMgr::hasActiveSplits(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it; for (it = d->m_transactionList.begin(); it != d->m_transactionList.end(); ++it) { if ((*it).accountReferenced(id)) { return true; } } return false; } MyMoneyInstitution MyMoneyStorageMgr::institution(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator pos; pos = d->m_institutionList.find(id); if (pos != d->m_institutionList.end()) return *pos; throw MYMONEYEXCEPTION_CSTRING("unknown institution"); } bool MyMoneyStorageMgr::dirty() const { Q_D(const MyMoneyStorageMgr); return d->m_dirty; } void MyMoneyStorageMgr::setDirty() { Q_D(MyMoneyStorageMgr); d->m_dirty = true; } QList MyMoneyStorageMgr::institutionList() const { Q_D(const MyMoneyStorageMgr); return d->m_institutionList.values(); } void MyMoneyStorageMgr::modifyAccount(const MyMoneyAccount& account, bool skipCheck) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator pos; // locate the account in the file global pool pos = d->m_accountList.find(account.id()); if (pos != d->m_accountList.end()) { // check if the new info is based on the old one. // this is the case, when the file and the id // as well as the type are equal. if (((*pos).parentAccountId() == account.parentAccountId() && ((*pos).accountType() == account.accountType() || ((*pos).isLiquidAsset() && account.isLiquidAsset()))) || skipCheck == true) { // make sure that all the referenced objects exist if (!account.institutionId().isEmpty()) institution(account.institutionId()); foreach (const auto sAccount, account.accountList()) this->account(sAccount); // update information in account list d->m_accountList.modify(account.id(), account); } else throw MYMONEYEXCEPTION_CSTRING("Invalid information for update"); } else throw MYMONEYEXCEPTION_CSTRING("Unknown account id"); } void MyMoneyStorageMgr::modifyInstitution(const MyMoneyInstitution& institution) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator pos; // locate the institution in the file global pool pos = d->m_institutionList.find(institution.id()); if (pos != d->m_institutionList.end()) { d->m_institutionList.modify(institution.id(), institution); } else throw MYMONEYEXCEPTION_CSTRING("unknown institution"); } void MyMoneyStorageMgr::modifyTransaction(const MyMoneyTransaction& transaction) { Q_D(MyMoneyStorageMgr); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * ids are assigned // * the pointer to the MyMoneyFile object is not 0 // * the date valid (must not be empty) // * the splits must have valid account ids // first perform all the checks if (transaction.id().isEmpty() // || transaction.file() != this || !transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction to be modified"); // now check the splits foreach (const auto split, transaction.splits()) { // the following lines will throw an exception if the // account or payee do not exist account(split.accountId()); if (!split.payeeId().isEmpty()) payee(split.payeeId()); foreach (const auto tagId, split.tagIdList()) { if (!tagId.isEmpty()) tag(tagId); } } // new data seems to be ok. find old version of transaction // in our pool. Throw exception if unknown. if (!d->m_transactionKeys.contains(transaction.id())) throw MYMONEYEXCEPTION_CSTRING("invalid transaction id"); QString oldKey = d->m_transactionKeys[transaction.id()]; if (!d->m_transactionList.contains(oldKey)) throw MYMONEYEXCEPTION_CSTRING("invalid transaction key"); QMap::ConstIterator it_t; it_t = d->m_transactionList.find(oldKey); if (it_t == d->m_transactionList.end()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction key"); foreach (const auto split, (*it_t).splits()) { auto acc = d->m_accountList[split.accountId()]; // we only need to adjust non-investment accounts here // as for investment accounts the balance will be recalculated // after the transaction has been added. if (!acc.isInvest()) { d->adjustBalance(acc, split, true); acc.touch(); d->m_accountList.modify(acc.id(), acc); } } // remove old transaction from lists d->m_transactionList.remove(oldKey); // add new transaction to lists QString newKey = transaction.uniqueSortKey(); d->m_transactionList.insert(newKey, transaction); d->m_transactionKeys.modify(transaction.id(), newKey); // adjust account balances foreach (const auto split, transaction.splits()) { auto acc = d->m_accountList[split.accountId()]; d->adjustBalance(acc, split, false); acc.touch(); d->m_accountList.modify(acc.id(), acc); } } void MyMoneyStorageMgr::reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent) { Q_D(MyMoneyStorageMgr); d->reparentAccount(account, parent, true); } void MyMoneyStorageMgr::close() { } void MyMoneyStorageMgr::removeTransaction(const MyMoneyTransaction& transaction) { Q_D(MyMoneyStorageMgr); // first perform all the checks if (transaction.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction to be deleted"); QMap::ConstIterator it_k; QMap::ConstIterator it_t; it_k = d->m_transactionKeys.find(transaction.id()); if (it_k == d->m_transactionKeys.end()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction to be deleted"); it_t = d->m_transactionList.find(*it_k); if (it_t == d->m_transactionList.end()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction key"); // keep a copy so that we still have the data after removal MyMoneyTransaction t(*it_t); // FIXME: check if any split is frozen and throw exception // remove the transaction from the two lists d->m_transactionList.remove(*it_k); d->m_transactionKeys.remove(transaction.id()); // scan the splits and collect all accounts that need // to be updated after the removal of this transaction foreach (const auto split, t.splits()) { auto acc = d->m_accountList[split.accountId()]; d->adjustBalance(acc, split, true); acc.touch(); d->m_accountList.modify(acc.id(), acc); } } void MyMoneyStorageMgr::removeAccount(const MyMoneyAccount& account) { Q_D(MyMoneyStorageMgr); MyMoneyAccount parent; // check that the account and it's parent exist // this will throw an exception if the id is unknown MyMoneyStorageMgr::account(account.id()); parent = MyMoneyStorageMgr::account(account.parentAccountId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits"); } // re-parent all sub-ordinate accounts to the parent of the account // to be deleted. First round check that all accounts exist, second // round do the re-parenting. foreach (const auto accountID, account.accountList()) MyMoneyStorageMgr::account(accountID); // if one of the accounts did not exist, an exception had been // thrown and we would not make it until here. QMap::ConstIterator it_a; QMap::ConstIterator it_p; // locate the account in the file global pool it_a = d->m_accountList.find(account.id()); if (it_a == d->m_accountList.end()) throw MYMONEYEXCEPTION_CSTRING("Internal error: account not found in list"); it_p = d->m_accountList.find(parent.id()); if (it_p == d->m_accountList.end()) throw MYMONEYEXCEPTION_CSTRING("Internal error: parent account not found in list"); if (!account.institutionId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot remove account still attached to an institution"); d->removeReferences(account.id()); // FIXME: check referential integrity for the account to be removed // check if the new info is based on the old one. // this is the case, when the file and the id // as well as the type are equal. if ((*it_a).id() == account.id() && (*it_a).accountType() == account.accountType()) { // second round over sub-ordinate accounts: do re-parenting // but only if the list contains at least one entry // FIXME: move this logic to MyMoneyFile foreach (const auto accountID, (*it_a).accountList()) { MyMoneyAccount acc(MyMoneyStorageMgr::account(accountID)); d->reparentAccount(acc, parent, false); } // remove account from parent's list parent.removeAccountId(account.id()); d->m_accountList.modify(parent.id(), parent); // remove account from the global account pool d->m_accountList.remove(account.id()); } } void MyMoneyStorageMgr::removeInstitution(const MyMoneyInstitution& institution) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it_i; it_i = d->m_institutionList.find(institution.id()); if (it_i != d->m_institutionList.end()) { d->m_institutionList.remove(institution.id()); } else throw MYMONEYEXCEPTION_CSTRING("invalid institution"); } void MyMoneyStorageMgr::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { Q_D(const MyMoneyStorageMgr); list.clear(); const auto& transactions = d->m_transactionList; for (const auto& transaction : transactions) { // This code is used now. It adds the transaction to the list for // each matching split exactly once. This allows to show information // about different splits in the same register view (e.g. search result) // // I have no idea, if this has some impact on the functionality. So far, // I could not see it. (ipwizard 9/5/2003) const auto cnt = filter.matchingSplitsCount(transaction); for (uint i = 0; i < cnt; ++i) list.append(transaction); } } void MyMoneyStorageMgr::transactionList(QList< QPair >& list, MyMoneyTransactionFilter& filter) const { Q_D(const MyMoneyStorageMgr); list.clear(); for (const auto& transaction : d->m_transactionList) { const auto& splits = filter.matchingSplits(transaction); for (const auto& split : splits) list.append(qMakePair(transaction, split)); } } QList MyMoneyStorageMgr::transactionList(MyMoneyTransactionFilter& filter) const { QList list; transactionList(list, filter); return list; } QList MyMoneyStorageMgr::onlineJobList() const { Q_D(const MyMoneyStorageMgr); return d->m_onlineJobList.values(); } QList< MyMoneyCostCenter > MyMoneyStorageMgr::costCenterList() const { Q_D(const MyMoneyStorageMgr); return d->m_costCenterList.values(); } MyMoneyCostCenter MyMoneyStorageMgr::costCenter(const QString& id) const { Q_D(const MyMoneyStorageMgr); if (!d->m_costCenterList.contains(id)) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid cost center id '%1'").arg(id)); return d->m_costCenterList[id]; } bool MyMoneyStorageMgr::isDuplicateTransaction(const QString& id) const { Q_D(const MyMoneyStorageMgr); return d->m_transactionKeys.contains(id); } MyMoneyTransaction MyMoneyStorageMgr::transaction(const QString& id) const { Q_D(const MyMoneyStorageMgr); // get the full key of this transaction, throw exception // if it's invalid (unknown) if (!d->m_transactionKeys.contains(id)) { throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid transaction id '%1'").arg(id)); } // check if this key is in the list, throw exception if not QString key = d->m_transactionKeys[id]; if (!d->m_transactionList.contains(key)) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid transaction key '%1'").arg(key)); return d->m_transactionList[key]; } MyMoneyTransaction MyMoneyStorageMgr::transaction(const QString& account, const int idx) const { Q_D(const MyMoneyStorageMgr); /* removed with MyMoneyAccount::Transaction QMap::ConstIterator acc; // find account object in list, throw exception if unknown acc = m_accountList.find(account); if(acc == m_accountList.end()) throw MYMONEYEXCEPTION_CSTRING("unknown account id"); // get the transaction info from the account MyMoneyAccount::Transaction t = (*acc).transaction(idx); // return the transaction, throw exception if not found return transaction(t.transactionID()); */ // new implementation if the above code does not work anymore auto acc = d->m_accountList[account]; MyMoneyTransactionFilter filter; if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) filter.addCategory(account); else filter.addAccount(account); const auto list = transactionList(filter); if (idx < 0 || idx >= static_cast(list.count())) throw MYMONEYEXCEPTION_CSTRING("Unknown idx for transaction"); return transaction(list[idx].id()); } MyMoneyMoney MyMoneyStorageMgr::balance(const QString& id, const QDate& date) const { Q_D(const MyMoneyStorageMgr); if (!d->m_accountList.contains(id)) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown account id '%1'").arg(id)); // the balance of all transactions for this account has // been requested. no need to calculate anything as we // have this number with the account object already. if (!date.isValid()) return d->m_accountList[id].balance(); else return d->calculateBalance(id, date); } MyMoneyMoney MyMoneyStorageMgr::totalBalance(const QString& id, const QDate& date) const { MyMoneyMoney result(balance(id, date)); foreach (const auto sAccount, account(id).accountList()) result += totalBalance(sAccount, date); return result; } MyMoneyAccount MyMoneyStorageMgr::liability() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability)); } MyMoneyAccount MyMoneyStorageMgr::asset() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset)); } MyMoneyAccount MyMoneyStorageMgr::expense() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)); } MyMoneyAccount MyMoneyStorageMgr::income() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income)); } MyMoneyAccount MyMoneyStorageMgr::equity() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity)); } void MyMoneyStorageMgr::loadAccounts(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_accountList = map; // scan the map to identify the last used id d->m_nextAccountID = 0; const QRegularExpression idExp("A(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { if (!isStandardAccount((*iter).id())) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextAccountID) { d->m_nextAccountID = id; } } } } void MyMoneyStorageMgr::loadTransactions(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_transactionList = map; // now fill the key map and // identify the last used id QMap keys; d->m_nextTransactionID = 0; const QRegularExpression idExp("T(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { keys[(*iter).id()] = iter.key(); const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextTransactionID) { d->m_nextTransactionID = id; } } d->m_transactionKeys = keys; } void MyMoneyStorageMgr::loadInstitutions(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_institutionList = map; // scan the map to identify the last used id d->m_nextInstitutionID = 0; const QRegularExpression idExp("I(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextInstitutionID) { d->m_nextInstitutionID = id; } } } void MyMoneyStorageMgr::loadPayees(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_payeeList = map; // scan the map to identify the last used id d->m_nextPayeeID = 0; const QRegularExpression idExp("P(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextPayeeID) { d->m_nextPayeeID = id; } } } void MyMoneyStorageMgr::loadTags(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_tagList = map; // scan the map to identify the last used id d->m_nextTagID = 0; const QRegularExpression idExp("G(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextTagID) { d->m_nextTagID = id; } } } void MyMoneyStorageMgr::loadSecurities(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_securitiesList = map; // scan the map to identify the last used id d->m_nextSecurityID = 0; const QRegularExpression idExp("E(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextSecurityID) { d->m_nextSecurityID = id; } } } void MyMoneyStorageMgr::loadCurrencies(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_currencyList = map; } void MyMoneyStorageMgr::loadPrices(const MyMoneyPriceList& list) { Q_D(MyMoneyStorageMgr); d->m_priceList = list; } void MyMoneyStorageMgr::loadOnlineJobs(const QMap< QString, onlineJob >& onlineJobs) { Q_D(MyMoneyStorageMgr); d->m_onlineJobList = onlineJobs; d->m_nextOnlineJobID = 0; const QRegularExpression idExp("O(\\d+)$"); auto end = onlineJobs.constEnd(); for (auto iter = onlineJobs.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextOnlineJobID) { d->m_nextOnlineJobID = id; } } } void MyMoneyStorageMgr::loadCostCenters(const QMap< QString, MyMoneyCostCenter >& costCenters) { Q_D(MyMoneyStorageMgr); d->m_costCenterList = costCenters; // scan the map to identify the last used id d->m_nextCostCenterID = 0; const QRegularExpression idExp("C(\\d+)$"); auto end = costCenters.constEnd(); for (auto iter = costCenters.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextCostCenterID) { d->m_nextCostCenterID = id; } } } void MyMoneyStorageMgr::setValue(const QString& key, const QString& val) { Q_D(MyMoneyStorageMgr); MyMoneyKeyValueContainer::setValue(key, val); d->touch(); } void MyMoneyStorageMgr::deletePair(const QString& key) { Q_D(MyMoneyStorageMgr); MyMoneyKeyValueContainer::deletePair(key); d->touch(); } void MyMoneyStorageMgr::setPairs(const QMap& list) { Q_D(MyMoneyStorageMgr); MyMoneyKeyValueContainer::setPairs(list); d->touch(); } void MyMoneyStorageMgr::addSchedule(MyMoneySchedule& sched) { Q_D(MyMoneyStorageMgr); // first perform all the checks if (!sched.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("schedule already contains an id"); // The following will throw an exception when it fails sched.validate(false); // it is expected in mymoneygenericstorage-test const auto splits = sched.transaction().splits(); for (const auto& split : splits) if (!d->m_accountList.contains(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("bad account id"); MyMoneySchedule newSched(d->nextScheduleID(), sched); d->m_scheduleList.insert(newSched.id(), newSched); sched = newSched; } void MyMoneyStorageMgr::modifySchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_scheduleList.find(sched.id()); if (it == d->m_scheduleList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown schedule '%1'").arg(sched.id())); d->m_scheduleList.modify(sched.id(), sched); } void MyMoneyStorageMgr::removeSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_scheduleList.find(sched.id()); if (it == d->m_scheduleList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown schedule '%1'").arg(sched.id())); // FIXME: check referential integrity for loan accounts d->m_scheduleList.remove(sched.id()); } MyMoneySchedule MyMoneyStorageMgr::schedule(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator pos; // locate the schedule and if present, return it's data pos = d->m_scheduleList.find(id); if (pos != d->m_scheduleList.end()) return (*pos); // throw an exception, if it does not exist throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown schedule '%1'").arg(id)); } QList MyMoneyStorageMgr::scheduleList(const QString& accountId, eMyMoney::Schedule::Type type, eMyMoney::Schedule::Occurrence occurrence, eMyMoney::Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, bool overdue) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator pos; QList list; // qDebug("scheduleList()"); for (pos = d->m_scheduleList.begin(); pos != d->m_scheduleList.end(); ++pos) { // qDebug(" '%s'", qPrintable((*pos).id())); if (type != eMyMoney::Schedule::Type::Any) { if (type != (*pos).type()) { continue; } } if (occurrence != eMyMoney::Schedule::Occurrence::Any) { - if (occurrence != (*pos).occurrence()) { + if (occurrence != (*pos).baseOccurrence()) { continue; } } if (paymentType != eMyMoney::Schedule::PaymentType::Any) { if (paymentType != (*pos).paymentType()) { continue; } } if (!accountId.isEmpty()) { MyMoneyTransaction t = (*pos).transaction(); QList::ConstIterator it; QList splits; splits = t.splits(); for (it = splits.constBegin(); it != splits.constEnd(); ++it) { if ((*it).accountId() == accountId) break; } if (it == splits.constEnd()) { continue; } } if (startDate.isValid() && endDate.isValid()) { if ((*pos).paymentDates(startDate, endDate).count() == 0) { continue; } } if (startDate.isValid() && !endDate.isValid()) { if (!(*pos).nextPayment(startDate.addDays(-1)).isValid()) { continue; } } if (!startDate.isValid() && endDate.isValid()) { if ((*pos).startDate() > endDate) { continue; } } if (overdue) { if (!(*pos).isOverdue()) continue; } // qDebug("Adding '%s'", (*pos).name().toLatin1()); list << *pos; } return list; } void MyMoneyStorageMgr::loadSchedules(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_scheduleList = map; // scan the map to identify the last used id d->m_nextScheduleID = 0; const QRegularExpression idExp("SCH(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextScheduleID) { d->m_nextScheduleID = id; } } } QList MyMoneyStorageMgr::scheduleListEx(int scheduleTypes, int scheduleOcurrences, int schedulePaymentTypes, QDate date, const QStringList& accounts) const { Q_D(const MyMoneyStorageMgr); // qDebug("scheduleListEx"); QMap::ConstIterator pos; QList list; if (!date.isValid()) return list; for (pos = d->m_scheduleList.begin(); pos != d->m_scheduleList.end(); ++pos) { if (scheduleTypes && !(scheduleTypes & (int)(*pos).type())) continue; - if (scheduleOcurrences && !(scheduleOcurrences & (int)(*pos).occurrence())) + if (scheduleOcurrences && !(scheduleOcurrences & (int)(*pos).baseOccurrence())) continue; if (schedulePaymentTypes && !(schedulePaymentTypes & (int)(*pos).paymentType())) continue; if ((*pos).paymentDates(date, date).count() == 0) continue; if ((*pos).isFinished()) continue; if ((*pos).hasRecordedPayment(date)) continue; if (accounts.count() > 0) { if (accounts.contains((*pos).account().id())) continue; } // qDebug("\tAdding '%s'", (*pos).name().toLatin1()); list << *pos; } return list; } void MyMoneyStorageMgr::addSecurity(MyMoneySecurity& security) { Q_D(MyMoneyStorageMgr); // create the account MyMoneySecurity newSecurity(d->nextSecurityID(), security); d->m_securitiesList.insert(newSecurity.id(), newSecurity); security = newSecurity; } void MyMoneyStorageMgr::modifySecurity(const MyMoneySecurity& security) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_securitiesList.find(security.id()); if (it == d->m_securitiesList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown security '%1'").arg(security.id())); d->m_securitiesList.modify(security.id(), security); } void MyMoneyStorageMgr::removeSecurity(const MyMoneySecurity& security) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; // FIXME: check referential integrity it = d->m_securitiesList.find(security.id()); if (it == d->m_securitiesList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown security '%1'").arg(security.id())); d->m_securitiesList.remove(security.id()); } MyMoneySecurity MyMoneyStorageMgr::security(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it = d->m_securitiesList.find(id); if (it != d->m_securitiesList.end()) { return it.value(); } // FIXME: in places where a currency is needed, a currency method should be called even if the currency is in fact a security it = d->m_currencyList.find(id); if (it != d->m_currencyList.end()) { return it.value(); } return MyMoneySecurity(); } QList MyMoneyStorageMgr::securityList() const { Q_D(const MyMoneyStorageMgr); //qDebug("securityList: Security list size is %d, this=%8p", m_equitiesList.size(), (void*)this); return d->m_securitiesList.values(); } void MyMoneyStorageMgr::addCurrency(const MyMoneySecurity& currency) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_currencyList.find(currency.id()); if (it != d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot add currency with existing id %1").arg(currency.id())); d->m_currencyList.insert(currency.id(), currency); } void MyMoneyStorageMgr::modifyCurrency(const MyMoneySecurity& currency) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_currencyList.find(currency.id()); if (it == d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot modify currency with unknown id %1").arg(currency.id())); d->m_currencyList.modify(currency.id(), currency); } void MyMoneyStorageMgr::removeCurrency(const MyMoneySecurity& currency) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; // FIXME: check referential integrity it = d->m_currencyList.find(currency.id()); if (it == d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove currency with unknown id %1").arg(currency.id())); d->m_currencyList.remove(currency.id()); } MyMoneySecurity MyMoneyStorageMgr::currency(const QString& id) const { Q_D(const MyMoneyStorageMgr); if (id.isEmpty()) { } QMap::ConstIterator it; it = d->m_currencyList.find(id); if (it == d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot retrieve currency with unknown id '%1'").arg(id)); return *it; } QList MyMoneyStorageMgr::currencyList() const { Q_D(const MyMoneyStorageMgr); return d->m_currencyList.values(); } QList MyMoneyStorageMgr::reportList() const { Q_D(const MyMoneyStorageMgr); return d->m_reportList.values(); } void MyMoneyStorageMgr::addReport(MyMoneyReport& report) { Q_D(MyMoneyStorageMgr); if (!report.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("report already contains an id")); MyMoneyReport newReport(d->nextReportID(), report); d->m_reportList.insert(newReport.id(), newReport); report = newReport; } void MyMoneyStorageMgr::loadReports(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_reportList = map; // scan the map to identify the last used id d->m_nextReportID = 0; const QRegularExpression idExp("R(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextReportID) { d->m_nextReportID = id; } } } void MyMoneyStorageMgr::modifyReport(const MyMoneyReport& report) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_reportList.find(report.id()); if (it == d->m_reportList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown report '%1'").arg(report.id())); d->m_reportList.modify(report.id(), report); } uint MyMoneyStorageMgr::countReports() const { Q_D(const MyMoneyStorageMgr); return d->m_reportList.count(); } MyMoneyReport MyMoneyStorageMgr::report(const QString& _id) const { Q_D(const MyMoneyStorageMgr); return d->m_reportList[_id]; } void MyMoneyStorageMgr::removeReport(const MyMoneyReport& report) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_reportList.find(report.id()); if (it == d->m_reportList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown report '%1'").arg(report.id())); d->m_reportList.remove(report.id()); } QList MyMoneyStorageMgr::budgetList() const { Q_D(const MyMoneyStorageMgr); return d->m_budgetList.values(); } void MyMoneyStorageMgr::addBudget(MyMoneyBudget& budget) { Q_D(MyMoneyStorageMgr); MyMoneyBudget newBudget(d->nextBudgetID(), budget); d->m_budgetList.insert(newBudget.id(), newBudget); budget = newBudget; } void MyMoneyStorageMgr::loadBudgets(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_budgetList = map; // scan the map to identify the last used id d->m_nextBudgetID = 0; const QRegularExpression idExp("B(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextBudgetID) { d->m_nextBudgetID = id; } } } MyMoneyBudget MyMoneyStorageMgr::budgetByName(const QString& budget) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it_p; for (it_p = d->m_budgetList.begin(); it_p != d->m_budgetList.end(); ++it_p) { if ((*it_p).name() == budget) { return *it_p; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown budget '%1'").arg(budget)); } void MyMoneyStorageMgr::modifyBudget(const MyMoneyBudget& budget) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_budgetList.find(budget.id()); if (it == d->m_budgetList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown budget '%1'").arg(budget.id())); d->m_budgetList.modify(budget.id(), budget); } uint MyMoneyStorageMgr::countBudgets() const { Q_D(const MyMoneyStorageMgr); return d->m_budgetList.count(); } MyMoneyBudget MyMoneyStorageMgr::budget(const QString& _id) const { Q_D(const MyMoneyStorageMgr); return d->m_budgetList[_id]; } void MyMoneyStorageMgr::removeBudget(const MyMoneyBudget& budget) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_budgetList.find(budget.id()); if (it == d->m_budgetList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown budget '%1'").arg(budget.id())); d->m_budgetList.remove(budget.id()); } void MyMoneyStorageMgr::addPrice(const MyMoneyPrice& price) { Q_D(MyMoneyStorageMgr); MyMoneySecurityPair pricePair(price.from(), price.to()); QMap::ConstIterator it_m; it_m = d->m_priceList.find(pricePair); MyMoneyPriceEntries entries; if (it_m != d->m_priceList.end()) { entries = (*it_m); } // entries contains the current entries for this security pair // in case it_m points to m_priceList.end() we need to create a // new entry in the priceList, otherwise we need to modify // an existing one. MyMoneyPriceEntries::ConstIterator it; it = entries.constFind(price.date()); if (it != entries.constEnd()) { if ((*it).rate(QString()) == price.rate(QString()) && (*it).source() == price.source()) // in case the information did not change, we don't do anything return; } // store new value in local copy entries[price.date()] = price; if (it_m != d->m_priceList.end()) { d->m_priceList.modify(pricePair, entries); } else { d->m_priceList.insert(pricePair, entries); } } void MyMoneyStorageMgr::removePrice(const MyMoneyPrice& price) { Q_D(MyMoneyStorageMgr); MyMoneySecurityPair pricePair(price.from(), price.to()); QMap::ConstIterator it_m; it_m = d->m_priceList.find(pricePair); MyMoneyPriceEntries entries; if (it_m != d->m_priceList.end()) { entries = (*it_m); } // store new value in local copy entries.remove(price.date()); if (entries.count() != 0) { d->m_priceList.modify(pricePair, entries); } else { d->m_priceList.remove(pricePair); } } MyMoneyPriceList MyMoneyStorageMgr::priceList() const { Q_D(const MyMoneyStorageMgr); MyMoneyPriceList list; d->m_priceList.map(list); return list; } MyMoneyPrice MyMoneyStorageMgr::price(const QString& fromId, const QString& toId, const QDate& _date, bool exactDate) const { Q_D(const MyMoneyStorageMgr); // if the caller selected an exact entry, we can search for it using the date as the key QMap::const_iterator itm = d->m_priceList.find(qMakePair(fromId, toId)); if (itm != d->m_priceList.end()) { // if no valid date is passed, we use today's date. const QDate &date = _date.isValid() ? _date : QDate::currentDate(); const MyMoneyPriceEntries &entries = itm.value(); // regardless of the exactDate flag if the exact date is present return it's value since it's the correct value MyMoneyPriceEntries::const_iterator it = entries.find(date); if (it != entries.end()) return it.value(); // the exact date was not found look for the latest date before the requested date if the flag allows it if (!exactDate && !entries.empty()) { // if there are entries get the lower bound of the date it = entries.lowerBound(date); // since lower bound returns the first item with a larger key (we already know that key is not present) // if it's not the first item then we need to return the previous item (the map is not empty so there is one) if (it != entries.begin()) { return (--it).value(); } } } return MyMoneyPrice(); } void MyMoneyStorageMgr::rebuildAccountBalances() { Q_D(MyMoneyStorageMgr); // reset the balance of all accounts to 0 QMap map; d->m_accountList.map(map); QMap::iterator it_a; for (it_a = map.begin(); it_a != map.end(); ++it_a) { (*it_a).setBalance(MyMoneyMoney()); } // now scan over all transactions and all splits and setup the balances foreach (const auto transaction, d->m_transactionList) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { const QString& id = split.accountId(); // locate the account and if present, update data if (map.find(id) != map.end()) { map[id].adjustBalance(split); } } } } d->m_accountList = map; } bool MyMoneyStorageMgr::isReferenced(const MyMoneyObject& obj, const QBitArray& skipCheck) const { Q_D(const MyMoneyStorageMgr); Q_ASSERT(skipCheck.count() == (int)Reference::Count); // We delete all references in reports when an object // is deleted, so we don't need to check here. See // MyMoneyStorageMgr::removeReferences(). In case // you miss the report checks in the following lines ;) const auto& id = obj.id(); // FIXME optimize the list of objects we have to checks // with a bit of knowledge of the internal structure, we // could optimize the number of objects we check for references // Scan all engine objects for a reference if (!skipCheck.testBit((int)Reference::Transaction)) foreach (const auto it, d->m_transactionList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Account)) foreach (const auto it, d->m_accountList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Institution)) foreach (const auto it, d->m_institutionList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Payee)) foreach (const auto it, d->m_payeeList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Tag)) foreach (const auto it, d->m_tagList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Budget)) foreach (const auto it, d->m_budgetList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Schedule)) foreach (const auto it, d->m_scheduleList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Security)) foreach (const auto it, d->m_securitiesList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Currency)) foreach (const auto it, d->m_currencyList) if (it.hasReferenceTo(id)) return true; // within the pricelist we don't have to scan each entry. Checking the QPair // members of the MyMoneySecurityPair is enough as they are identical to the // two security ids if (!skipCheck.testBit((int)Reference::Price)) { for (auto it_pr = d->m_priceList.begin(); it_pr != d->m_priceList.end(); ++it_pr) { if ((it_pr.key().first == id) || (it_pr.key().second == id)) return true; } } return false; } void MyMoneyStorageMgr::startTransaction() { Q_D(MyMoneyStorageMgr); d->m_payeeList.startTransaction(&d->m_nextPayeeID); d->m_tagList.startTransaction(&d->m_nextTagID); d->m_institutionList.startTransaction(&d->m_nextInstitutionID); d->m_accountList.startTransaction(&d->m_nextPayeeID); d->m_transactionList.startTransaction(&d->m_nextTransactionID); d->m_transactionKeys.startTransaction(); d->m_scheduleList.startTransaction(&d->m_nextScheduleID); d->m_securitiesList.startTransaction(&d->m_nextSecurityID); d->m_currencyList.startTransaction(); d->m_reportList.startTransaction(&d->m_nextReportID); d->m_budgetList.startTransaction(&d->m_nextBudgetID); d->m_priceList.startTransaction(); d->m_onlineJobList.startTransaction(&d->m_nextOnlineJobID); } bool MyMoneyStorageMgr::commitTransaction() { Q_D(MyMoneyStorageMgr); bool rc = false; rc |= d->m_payeeList.commitTransaction(); rc |= d->m_tagList.commitTransaction(); rc |= d->m_institutionList.commitTransaction(); rc |= d->m_accountList.commitTransaction(); rc |= d->m_transactionList.commitTransaction(); rc |= d->m_transactionKeys.commitTransaction(); rc |= d->m_scheduleList.commitTransaction(); rc |= d->m_securitiesList.commitTransaction(); rc |= d->m_currencyList.commitTransaction(); rc |= d->m_reportList.commitTransaction(); rc |= d->m_budgetList.commitTransaction(); rc |= d->m_priceList.commitTransaction(); rc |= d->m_onlineJobList.commitTransaction(); // if there was a change, touch the whole storage object if (rc) d->touch(); return rc; } void MyMoneyStorageMgr::rollbackTransaction() { Q_D(MyMoneyStorageMgr); d->m_payeeList.rollbackTransaction(); d->m_tagList.rollbackTransaction(); d->m_institutionList.rollbackTransaction(); d->m_accountList.rollbackTransaction(); d->m_transactionList.rollbackTransaction(); d->m_transactionKeys.rollbackTransaction(); d->m_scheduleList.rollbackTransaction(); d->m_securitiesList.rollbackTransaction(); d->m_currencyList.rollbackTransaction(); d->m_reportList.rollbackTransaction(); d->m_budgetList.rollbackTransaction(); d->m_priceList.rollbackTransaction(); d->m_onlineJobList.rollbackTransaction(); } diff --git a/kmymoney/mymoney/tests/mymoneyschedule-test.cpp b/kmymoney/mymoney/tests/mymoneyschedule-test.cpp index 45e504229..f691517d1 100644 --- a/kmymoney/mymoney/tests/mymoneyschedule-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyschedule-test.cpp @@ -1,935 +1,935 @@ /* * Copyright 2003 Michael Edwardes * Copyright 2006 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyschedule-test.h" #include #include #include #include #define KMM_MYMONEY_UNIT_TESTABLE friend class MyMoneyScheduleTest; #include "mymoneysplit.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "mymoneyschedule_p.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneytransaction.h" #include "mymoneytransaction_p.h" #include "storage/mymoneystoragemgr.h" QTEST_GUILESS_MAIN(MyMoneyScheduleTest) using namespace eMyMoney; void MyMoneyScheduleTest::testEmptyConstructor() { MyMoneySchedule s; QCOMPARE(s.id().isEmpty(), true); - QCOMPARE(s.occurrence(), Schedule::Occurrence::Any); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Any); QCOMPARE(s.type(), Schedule::Type::Any); QCOMPARE(s.paymentType(), Schedule::PaymentType::Any); QCOMPARE(s.isFinished(), false); QCOMPARE(!s.startDate().isValid(), true); QCOMPARE(!s.endDate().isValid(), true); QCOMPARE(!s.lastPayment().isValid(), true); QCOMPARE(s.autoEnter(), false); QCOMPARE(s.name().isEmpty(), true); QCOMPARE(s.willEnd(), false); QCOMPARE(s.lastDayInMonth(), false); } void MyMoneyScheduleTest::testConstructor() { MyMoneySchedule s("A Name", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate(), QDate(), true, true); QCOMPARE(s.type(), Schedule::Type::Bill); - QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceMultiplier(), 1); QCOMPARE(s.paymentType(), Schedule::PaymentType::DirectDebit); QCOMPARE(s.startDate(), QDate()); QCOMPARE(s.willEnd(), false); QCOMPARE(s.isFixed(), true); QCOMPARE(s.autoEnter(), true); QCOMPARE(s.name(), QLatin1String("A Name")); QCOMPARE(!s.endDate().isValid(), true); QCOMPARE(!s.lastPayment().isValid(), true); } void MyMoneyScheduleTest::testSetFunctions() { MyMoneySchedule s; s.d_func()->setId("SCHED001"); QCOMPARE(s.id(), QLatin1String("SCHED001")); s.setType(Schedule::Type::Bill); QCOMPARE(s.type(), Schedule::Type::Bill); s.setEndDate(QDate::currentDate()); QCOMPARE(s.endDate(), QDate::currentDate()); QCOMPARE(s.willEnd(), true); } void MyMoneyScheduleTest::testCopyConstructor() { MyMoneySchedule s; s.d_func()->setId("SCHED001"); s.setType(Schedule::Type::Bill); MyMoneySchedule s2(s); QCOMPARE(s.id(), s2.id()); QCOMPARE(s.type(), s2.type()); } void MyMoneyScheduleTest::testAssignmentConstructor() { MyMoneySchedule s; s.d_func()->setId("SCHED001"); s.setType(Schedule::Type::Bill); MyMoneySchedule s2 = s; QCOMPARE(s.id(), s2.id()); QCOMPARE(s.type(), s2.type()); } void MyMoneyScheduleTest::testAddHalfMonths() { // addHalfMonths is private // Test a Schedule with occurrence EveryHalfMonth using nextPayment MyMoneySchedule s; s.setStartDate(QDate(2007, 1, 1)); s.setOccurrence(Schedule::Occurrence::EveryHalfMonth); s.setNextDueDate(s.startDate()); s.setLastPayment(s.startDate()); QString format("yyyy-MM-dd"); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-16")); s.setNextDueDate(QDate(2007, 1, 2)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-17")); s.setNextDueDate(QDate(2007, 1, 3)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-18")); s.setNextDueDate(QDate(2007, 1, 4)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-19")); s.setNextDueDate(QDate(2007, 1, 5)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-20")); s.setNextDueDate(QDate(2007, 1, 6)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-21")); s.setNextDueDate(QDate(2007, 1, 7)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-22")); s.setNextDueDate(QDate(2007, 1, 8)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-23")); s.setNextDueDate(QDate(2007, 1, 9)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-24")); s.setNextDueDate(QDate(2007, 1, 10)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-25")); s.setNextDueDate(QDate(2007, 1, 11)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-26")); s.setNextDueDate(QDate(2007, 1, 12)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-27")); s.setNextDueDate(QDate(2007, 1, 13)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-28")); s.setNextDueDate(QDate(2007, 1, 14)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-29")); // 15 -> Last Day s.setNextDueDate(QDate(2007, 1, 15)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-01-31")); s.setNextDueDate(QDate(2007, 1, 16)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-01")); s.setNextDueDate(QDate(2007, 1, 17)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-02")); s.setNextDueDate(QDate(2007, 1, 18)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-03")); s.setNextDueDate(QDate(2007, 1, 19)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-04")); s.setNextDueDate(QDate(2007, 1, 20)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-05")); s.setNextDueDate(QDate(2007, 1, 21)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-06")); s.setNextDueDate(QDate(2007, 1, 22)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-07")); s.setNextDueDate(QDate(2007, 1, 23)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-08")); s.setNextDueDate(QDate(2007, 1, 24)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-09")); s.setNextDueDate(QDate(2007, 1, 25)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-10")); s.setNextDueDate(QDate(2007, 1, 26)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-11")); s.setNextDueDate(QDate(2007, 1, 27)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-12")); s.setNextDueDate(QDate(2007, 1, 28)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-13")); s.setNextDueDate(QDate(2007, 1, 29)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-14")); // 30th,31st -> 15th s.setNextDueDate(QDate(2007, 1, 30)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-15")); s.setNextDueDate(QDate(2007, 1, 31)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-02-15")); // 30th (last day) s.setNextDueDate(QDate(2007, 4, 30)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2007-05-15")); // 28th of February (Last day): to 15th s.setNextDueDate(QDate(1900, 2, 28)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("1900-03-15")); // 28th of February (Leap year): to 13th s.setNextDueDate(QDate(2000, 2, 28)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2000-03-13")); // 29th of February (Leap year) s.setNextDueDate(QDate(2000, 2, 29)); QCOMPARE(s.nextPayment(s.nextDueDate()).toString(format), QLatin1String("2000-03-15")); // Add multiple transactions s.setStartDate(QDate(2007, 1, 1)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-01-16")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-01")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-02-16")); s.setStartDate(QDate(2007, 1, 12)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-01-27")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-12")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-02-27")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-12")); s.setStartDate(QDate(2007, 1, 13)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-01-28")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-13")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-02-28")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-15")); s.setStartDate(QDate(2007, 1, 14)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-01-29")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-14")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-02-28")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-15")); s.setStartDate(QDate(2007, 1, 15)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-01-31")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-15")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-02-28")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-15")); s.setStartDate(QDate(2007, 1, 16)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-02-01")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-16")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-03-01")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-16")); s.setStartDate(QDate(2007, 1, 27)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-02-12")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-27")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-03-12")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-27")); s.setStartDate(QDate(2007, 1, 28)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-02-13")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-28")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-03-15")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-31")); s.setStartDate(QDate(2007, 1, 29)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-02-14")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-28")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-03-15")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-31")); s.setStartDate(QDate(2007, 1, 30)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-02-15")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-28")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-03-15")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-31")); s.setStartDate(QDate(2007, 1, 31)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-02-15")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-02-28")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-03-15")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-03-31")); s.setStartDate(QDate(2007, 4, 29)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-05-14")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-05-29")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-06-14")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-06-29")); s.setStartDate(QDate(2007, 4, 30)); QCOMPARE(s.dateAfter(2).toString(format), QLatin1String("2007-05-15")); QCOMPARE(s.dateAfter(3).toString(format), QLatin1String("2007-05-31")); QCOMPARE(s.dateAfter(4).toString(format), QLatin1String("2007-06-15")); QCOMPARE(s.dateAfter(5).toString(format), QLatin1String("2007-06-30")); } void MyMoneyScheduleTest::testAdjustedNextDueDate() { MyMoneySchedule s; QDate dueDate(2007, 9, 3); // start on a Monday for (int i = 0; i < 7; ++i) { s.setNextDueDate(dueDate); s.setWeekendOption(Schedule::WeekendOption::MoveNothing); QCOMPARE(s.adjustedNextDueDate(), dueDate); s.setWeekendOption(Schedule::WeekendOption::MoveBefore); switch (i) { case 5: // Saturday case 6: // Sunday QCOMPARE(s.adjustedNextDueDate(), QDate(2007, 9, 7)); break; default: QCOMPARE(s.adjustedNextDueDate(), dueDate); break; } s.setWeekendOption(Schedule::WeekendOption::MoveAfter); switch (i) { case 5: // Saturday case 6: // Sunday QCOMPARE(s.adjustedNextDueDate(), QDate(2007, 9, 10)); break; default: QCOMPARE(s.adjustedNextDueDate(), dueDate); break; } dueDate = dueDate.addDays(1); } } void MyMoneyScheduleTest::testModifyNextDueDate() { MyMoneySchedule s; s.setStartDate(QDate(2007, 1, 2)); s.setOccurrence(Schedule::Occurrence::Monthly); s.setNextDueDate(s.startDate().addMonths(1)); s.setLastPayment(s.startDate()); QList dates; dates = s.paymentDates(QDate(2007, 2, 2), QDate(2007, 2, 2)); QCOMPARE(s.nextDueDate(), QDate(2007, 2, 2)); QCOMPARE(dates.count(), 1); QCOMPARE(dates[0], QDate(2007, 2, 2)); s.setNextDueDate(QDate(2007, 1, 24)); dates = s.paymentDates(QDate(2007, 2, 1), QDate(2007, 2, 1)); QCOMPARE(s.nextDueDate(), QDate(2007, 1, 24)); QCOMPARE(dates.count(), 0); dates = s.paymentDates(QDate(2007, 1, 24), QDate(2007, 1, 24)); QCOMPARE(dates.count(), 1); dates = s.paymentDates(QDate(2007, 1, 24), QDate(2007, 2, 24)); QCOMPARE(dates.count(), 2); QCOMPARE(dates[0], QDate(2007, 1, 24)); QCOMPARE(dates[1], QDate(2007, 2, 2)); } void MyMoneyScheduleTest::testDaysBetweenEvents() { QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::Once), 0); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::Daily), 1); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::Weekly), 7); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryOtherWeek), 14); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::Fortnightly), 14); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryHalfMonth), 15); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryThreeWeeks), 21); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryFourWeeks), 28); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryThirtyDays), 30); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::Monthly), 30); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryEightWeeks), 56); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryOtherMonth), 60); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryThreeMonths), 90); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::Quarterly), 90); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryFourMonths), 120); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::TwiceYearly), 180); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::Yearly), 360); QCOMPARE(MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence::EveryOtherYear), 0); } void MyMoneyScheduleTest::testEventsPerYear() { QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::Once), 0); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::Daily), 365); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::Weekly), 52); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryOtherWeek), 26); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::Fortnightly), 26); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryHalfMonth), 24); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryThreeWeeks), 17); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryFourWeeks), 13); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryThirtyDays), 12); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::Monthly), 12); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryEightWeeks), 6); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryOtherMonth), 6); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryThreeMonths), 4); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::Quarterly), 4); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryFourMonths), 3); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::TwiceYearly), 2); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::Yearly), 1); QCOMPARE(MyMoneySchedule::eventsPerYear(Schedule::Occurrence::EveryOtherYear), 0); } void MyMoneyScheduleTest::testOccurrenceToString() { // For each occurrenceE test MyMoneySchedule::occurrenceToString(occurrenceE) QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Once), QLatin1String("Once")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Daily), QLatin1String("Daily")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Weekly), QLatin1String("Weekly")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherWeek), QLatin1String("Every other week")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Fortnightly), QLatin1String("Fortnightly")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryHalfMonth), QLatin1String("Every half month")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThreeWeeks), QLatin1String("Every three weeks")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryFourWeeks), QLatin1String("Every four weeks")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThirtyDays), QLatin1String("Every thirty days")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Monthly), QLatin1String("Monthly")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryEightWeeks), QLatin1String("Every eight weeks")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherMonth), QLatin1String("Every two months")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThreeMonths), QLatin1String("Every three months")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Quarterly), QLatin1String("Quarterly")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryFourMonths), QLatin1String("Every four months")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::TwiceYearly), QLatin1String("Twice yearly")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Yearly), QLatin1String("Yearly")); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherYear), QLatin1String("Every other year")); // For each occurrenceE set occurrence and compare occurrenceToString() with oTS(occurrence()) MyMoneySchedule s; s.setStartDate(QDate(2007, 1, 1)); s.setNextDueDate(s.startDate()); s.setLastPayment(s.startDate()); s.setOccurrence(Schedule::Occurrence::Once); QCOMPARE(s.occurrenceToString(), QLatin1String("Once")); s.setOccurrence(Schedule::Occurrence::Daily); QCOMPARE(s.occurrenceToString(), QLatin1String("Daily")); s.setOccurrence(Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceToString(), QLatin1String("Weekly")); s.setOccurrence(Schedule::Occurrence::EveryOtherWeek); QCOMPARE(s.occurrenceToString(), QLatin1String("Every other week")); // Fortnightly no longer used: Every other week used instead s.setOccurrence(Schedule::Occurrence::Fortnightly); QCOMPARE(s.occurrenceToString(), QLatin1String("Every other week")); s.setOccurrence(Schedule::Occurrence::EveryHalfMonth); QCOMPARE(s.occurrenceToString(), QLatin1String("Every half month")); s.setOccurrence(Schedule::Occurrence::EveryThreeWeeks); QCOMPARE(s.occurrenceToString(), QLatin1String("Every three weeks")); s.setOccurrence(Schedule::Occurrence::EveryFourWeeks); QCOMPARE(s.occurrenceToString(), QLatin1String("Every four weeks")); s.setOccurrence(Schedule::Occurrence::EveryThirtyDays); QCOMPARE(s.occurrenceToString(), QLatin1String("Every thirty days")); s.setOccurrence(Schedule::Occurrence::Monthly); QCOMPARE(s.occurrenceToString(), QLatin1String("Monthly")); s.setOccurrence(Schedule::Occurrence::EveryEightWeeks); QCOMPARE(s.occurrenceToString(), QLatin1String("Every eight weeks")); s.setOccurrence(Schedule::Occurrence::EveryOtherMonth); QCOMPARE(s.occurrenceToString(), QLatin1String("Every two months")); s.setOccurrence(Schedule::Occurrence::EveryThreeMonths); QCOMPARE(s.occurrenceToString(), QLatin1String("Every three months")); // Quarterly no longer used. Every three months used instead s.setOccurrence(Schedule::Occurrence::Quarterly); QCOMPARE(s.occurrenceToString(), QLatin1String("Every three months")); s.setOccurrence(Schedule::Occurrence::EveryFourMonths); QCOMPARE(s.occurrenceToString(), QLatin1String("Every four months")); s.setOccurrence(Schedule::Occurrence::TwiceYearly); QCOMPARE(s.occurrenceToString(), QLatin1String("Twice yearly")); s.setOccurrence(Schedule::Occurrence::Yearly); QCOMPARE(s.occurrenceToString(), QLatin1String("Yearly")); s.setOccurrence(Schedule::Occurrence::EveryOtherYear); QCOMPARE(s.occurrenceToString(), QLatin1String("Every other year")); // Test occurrenceToString(mult,occ) // Test all pairs equivalent to simple occurrences: should return the same as occurrenceToString(simpleOcc) // TODO replace string with (mult,occ) call. QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Once), MyMoneySchedule::occurrenceToString(1, Schedule::Occurrence::Once)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Daily), MyMoneySchedule::occurrenceToString(1, Schedule::Occurrence::Daily)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Weekly), MyMoneySchedule::occurrenceToString(1, Schedule::Occurrence::Weekly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherWeek), MyMoneySchedule::occurrenceToString(2, Schedule::Occurrence::Weekly)); // Fortnightly will no longer be used: only Every Other Week QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryHalfMonth), MyMoneySchedule::occurrenceToString(1, Schedule::Occurrence::EveryHalfMonth)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThreeWeeks), MyMoneySchedule::occurrenceToString(3, Schedule::Occurrence::Weekly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryFourWeeks), MyMoneySchedule::occurrenceToString(4, Schedule::Occurrence::Weekly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Monthly), MyMoneySchedule::occurrenceToString(1, Schedule::Occurrence::Monthly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryEightWeeks), MyMoneySchedule::occurrenceToString(8, Schedule::Occurrence::Weekly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherMonth), MyMoneySchedule::occurrenceToString(2, Schedule::Occurrence::Monthly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThreeMonths), MyMoneySchedule::occurrenceToString(3, Schedule::Occurrence::Monthly)); // Quarterly will no longer be used: only Every Three Months QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryFourMonths), MyMoneySchedule::occurrenceToString(4, Schedule::Occurrence::Monthly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::TwiceYearly), MyMoneySchedule::occurrenceToString(6, Schedule::Occurrence::Monthly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Yearly), MyMoneySchedule::occurrenceToString(1, Schedule::Occurrence::Yearly)); QCOMPARE(MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherYear), MyMoneySchedule::occurrenceToString(2, Schedule::Occurrence::Yearly)); // Test additional calls with other mult,occ QCOMPARE(MyMoneySchedule::occurrenceToString(2, Schedule::Occurrence::Once), QLatin1String("2 times")); QCOMPARE(MyMoneySchedule::occurrenceToString(2, Schedule::Occurrence::Daily), QLatin1String("Every 2 days")); QCOMPARE(MyMoneySchedule::occurrenceToString(5, Schedule::Occurrence::Weekly), QLatin1String("Every 5 weeks")); QCOMPARE(MyMoneySchedule::occurrenceToString(2, Schedule::Occurrence::EveryHalfMonth), QLatin1String("Every 2 half months")); QCOMPARE(MyMoneySchedule::occurrenceToString(5, Schedule::Occurrence::Monthly), QLatin1String("Every 5 months")); QCOMPARE(MyMoneySchedule::occurrenceToString(3, Schedule::Occurrence::Yearly), QLatin1String("Every 3 years")); QCOMPARE(MyMoneySchedule::occurrenceToString(37, Schedule::Occurrence::Once), QLatin1String("37 times")); QCOMPARE(MyMoneySchedule::occurrenceToString(43, Schedule::Occurrence::Daily), QLatin1String("Every 43 days")); QCOMPARE(MyMoneySchedule::occurrenceToString(61, Schedule::Occurrence::Weekly), QLatin1String("Every 61 weeks")); QCOMPARE(MyMoneySchedule::occurrenceToString(73, Schedule::Occurrence::EveryHalfMonth), QLatin1String("Every 73 half months")); QCOMPARE(MyMoneySchedule::occurrenceToString(83, Schedule::Occurrence::Monthly), QLatin1String("Every 83 months")); QCOMPARE(MyMoneySchedule::occurrenceToString(89, Schedule::Occurrence::Yearly), QLatin1String("Every 89 years")); // Test instance-level occurrenceToString method is using occurrencePeriod and multiplier // For each base occurrence set occurrencePeriod and multiplier s.setOccurrencePeriod(Schedule::Occurrence::Once); s.setOccurrenceMultiplier(1); s.setOccurrence(Schedule::Occurrence::Once); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceToString(), QLatin1String("Once")); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceToString(), QLatin1String("2 times")); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceToString(), QLatin1String("3 times")); s.setOccurrencePeriod(Schedule::Occurrence::Daily); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceToString(), QLatin1String("Daily")); s.setOccurrenceMultiplier(30); QCOMPARE(s.occurrenceToString(), QLatin1String("Every thirty days")); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 3 days")); s.setOccurrence(Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceToString(), QLatin1String("Weekly")); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceToString(), QLatin1String("Every other week")); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceToString(), QLatin1String("Every three weeks")); s.setOccurrenceMultiplier(4); QCOMPARE(s.occurrenceToString(), QLatin1String("Every four weeks")); s.setOccurrenceMultiplier(5); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 5 weeks")); s.setOccurrenceMultiplier(7); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 7 weeks")); s.setOccurrenceMultiplier(8); QCOMPARE(s.occurrenceToString(), QLatin1String("Every eight weeks")); s.setOccurrenceMultiplier(9); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 9 weeks")); s.setOccurrence(Schedule::Occurrence::EveryHalfMonth); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceToString(), QLatin1String("Every half month")); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 2 half months")); s.setOccurrence(Schedule::Occurrence::Monthly); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceToString(), QLatin1String("Monthly")); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceToString(), QLatin1String("Every two months")); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceToString(), QLatin1String("Every three months")); s.setOccurrenceMultiplier(4); QCOMPARE(s.occurrenceToString(), QLatin1String("Every four months")); s.setOccurrenceMultiplier(5); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 5 months")); s.setOccurrenceMultiplier(6); QCOMPARE(s.occurrenceToString(), QLatin1String("Twice yearly")); s.setOccurrenceMultiplier(7); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 7 months")); s.setOccurrence(Schedule::Occurrence::Yearly); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceToString(), QLatin1String("Yearly")); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceToString(), QLatin1String("Every other year")); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceToString(), QLatin1String("Every 3 years")); } void MyMoneyScheduleTest::testOccurrencePeriodToString() { // For each occurrenceE test MyMoneySchedule::occurrencePeriodToString(occurrenceE) // Base occurrences are translated QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::Once), QLatin1String("Once")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::Daily), QLatin1String("Day")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::Weekly), QLatin1String("Week")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryHalfMonth), QLatin1String("Half-month")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::Monthly), QLatin1String("Month")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::Yearly), QLatin1String("Year")); // All others are not translated so return Any QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryOtherWeek), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::Fortnightly), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryThreeWeeks), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryFourWeeks), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryThirtyDays), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryEightWeeks), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryOtherMonth), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryThreeMonths), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::Quarterly), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryFourMonths), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::TwiceYearly), QLatin1String("Any")); QCOMPARE(MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence::EveryOtherYear), QLatin1String("Any")); } void MyMoneyScheduleTest::testOccurrencePeriod() { // Each occurrence: // Set occurrence using setOccurrencePeriod // occurrencePeriod should match what we set // occurrence depends on multiplier // TODO: // Once occurrence() and setOccurrence() are converting between compound and simple occurrences // we need to change the occurrence() check and add an occurrenceMultiplier() check MyMoneySchedule s; s.setStartDate(QDate(2007, 1, 1)); s.setNextDueDate(s.startDate()); s.setLastPayment(s.startDate()); // Set all base occurrences s.setOccurrencePeriod(Schedule::Occurrence::Once); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Once); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Once); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceMultiplier(), 1); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Once); QCOMPARE(s.occurrence(), Schedule::Occurrence::Once); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Once); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceMultiplier(), 2); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Once); QCOMPARE(s.occurrence(), Schedule::Occurrence::Once); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Once); s.setOccurrencePeriod(Schedule::Occurrence::Daily); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Daily); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Daily); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceMultiplier(), 1); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Daily); QCOMPARE(s.occurrence(), Schedule::Occurrence::Daily); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Daily); s.setOccurrenceMultiplier(30); QCOMPARE(s.occurrenceMultiplier(), 30); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Daily); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryThirtyDays); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Daily); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryThirtyDays); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceMultiplier(), 2); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Daily); QCOMPARE(s.occurrence(), Schedule::Occurrence::Daily); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Daily); s.setOccurrencePeriod(Schedule::Occurrence::Weekly); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceMultiplier(), 1); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Weekly); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceMultiplier(), 2); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryOtherWeek); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryOtherWeek); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceMultiplier(), 3); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryThreeWeeks); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryThreeWeeks); s.setOccurrenceMultiplier(4); QCOMPARE(s.occurrenceMultiplier(), 4); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryFourWeeks); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryFourWeeks); s.setOccurrenceMultiplier(5); QCOMPARE(s.occurrenceMultiplier(), 5); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Weekly); s.setOccurrenceMultiplier(8); QCOMPARE(s.occurrenceMultiplier(), 8); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryEightWeeks); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryEightWeeks); s.setOccurrencePeriod(Schedule::Occurrence::EveryHalfMonth); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::EveryHalfMonth); + QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryHalfMonth); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceMultiplier(), 1); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::EveryHalfMonth); QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryHalfMonth); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryHalfMonth); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceMultiplier(), 2); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::EveryHalfMonth); QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryHalfMonth); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryHalfMonth); s.setOccurrencePeriod(Schedule::Occurrence::Monthly); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceMultiplier(), 1); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Monthly); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceMultiplier(), 2); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryOtherMonth); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryOtherMonth); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceMultiplier(), 3); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryThreeMonths); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryThreeMonths); s.setOccurrenceMultiplier(4); QCOMPARE(s.occurrenceMultiplier(), 4); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryFourMonths); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryFourMonths); s.setOccurrenceMultiplier(5); QCOMPARE(s.occurrenceMultiplier(), 5); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Monthly); s.setOccurrenceMultiplier(6); QCOMPARE(s.occurrenceMultiplier(), 6); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::TwiceYearly); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::TwiceYearly); s.setOccurrencePeriod(Schedule::Occurrence::Yearly); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Yearly); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Yearly); s.setOccurrenceMultiplier(1); QCOMPARE(s.occurrenceMultiplier(), 1); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Yearly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Yearly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Yearly); s.setOccurrenceMultiplier(2); QCOMPARE(s.occurrenceMultiplier(), 2); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Yearly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryOtherYear); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Yearly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryOtherYear); s.setOccurrenceMultiplier(3); QCOMPARE(s.occurrenceMultiplier(), 3); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Yearly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Yearly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Yearly); // Set occurrence: check occurrence, Period and Multiplier s.setOccurrence(Schedule::Occurrence::Once); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Once); QCOMPARE(s.occurrence(), Schedule::Occurrence::Once); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Once); QCOMPARE(s.occurrenceMultiplier(), 1); s.setOccurrence(Schedule::Occurrence::Daily); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Daily); QCOMPARE(s.occurrence(), Schedule::Occurrence::Daily); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Daily); QCOMPARE(s.occurrenceMultiplier(), 1); s.setOccurrence(Schedule::Occurrence::EveryThirtyDays); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryThirtyDays); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Daily); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryThirtyDays); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Daily); QCOMPARE(s.occurrenceMultiplier(), 30); s.setOccurrence(Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceMultiplier(), 1); s.setOccurrence(Schedule::Occurrence::EveryOtherWeek); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryOtherWeek); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryOtherWeek); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceMultiplier(), 2); // Fortnightly no longer used: Every other week used instead s.setOccurrence(Schedule::Occurrence::Fortnightly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryOtherWeek); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryOtherWeek); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceMultiplier(), 2); s.setOccurrence(Schedule::Occurrence::EveryThreeWeeks); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryThreeWeeks); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryThreeWeeks); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceMultiplier(), 3); s.setOccurrence(Schedule::Occurrence::EveryFourWeeks); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryFourWeeks); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryFourWeeks); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceMultiplier(), 4); s.setOccurrence(Schedule::Occurrence::EveryEightWeeks); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryEightWeeks); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Weekly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryEightWeeks); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Weekly); QCOMPARE(s.occurrenceMultiplier(), 8); s.setOccurrence(Schedule::Occurrence::EveryHalfMonth); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryHalfMonth); QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryHalfMonth); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::EveryHalfMonth); QCOMPARE(s.occurrenceMultiplier(), 1); s.setOccurrence(Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrenceMultiplier(), 1); s.setOccurrence(Schedule::Occurrence::EveryOtherMonth); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryOtherMonth); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryOtherMonth); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrenceMultiplier(), 2); s.setOccurrence(Schedule::Occurrence::EveryThreeMonths); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryThreeMonths); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryThreeMonths); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrenceMultiplier(), 3); // Quarterly no longer used. Every three months used instead s.setOccurrence(Schedule::Occurrence::Quarterly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryThreeMonths); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryThreeMonths); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrenceMultiplier(), 3); s.setOccurrence(Schedule::Occurrence::EveryFourMonths); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryFourMonths); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryFourMonths); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrenceMultiplier(), 4); s.setOccurrence(Schedule::Occurrence::TwiceYearly); - QCOMPARE(s.occurrence(), Schedule::Occurrence::TwiceYearly); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Monthly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::TwiceYearly); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Monthly); QCOMPARE(s.occurrenceMultiplier(), 6); s.setOccurrence(Schedule::Occurrence::Yearly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::Yearly); QCOMPARE(s.occurrence(), Schedule::Occurrence::Yearly); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Yearly); QCOMPARE(s.occurrenceMultiplier(), 1); s.setOccurrence(Schedule::Occurrence::EveryOtherYear); - QCOMPARE(s.occurrence(), Schedule::Occurrence::EveryOtherYear); - QCOMPARE(s.occurrencePeriod(), Schedule::Occurrence::Yearly); + QCOMPARE(s.baseOccurrence(), Schedule::Occurrence::EveryOtherYear); + QCOMPARE(s.occurrence(), Schedule::Occurrence::Yearly); QCOMPARE(s.occurrenceMultiplier(), 2); } void MyMoneyScheduleTest::testSimpleToFromCompoundOccurrence() { // Conversion between Simple and Compound occurrences // Each simple occurrence to compound occurrence Schedule::Occurrence occ; int mult; occ = Schedule::Occurrence::Once; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Once && mult == 1); occ = Schedule::Occurrence::Daily; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Daily && mult == 1); occ = Schedule::Occurrence::Weekly; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Weekly && mult == 1); occ = Schedule::Occurrence::EveryOtherWeek; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Weekly && mult == 2); occ = Schedule::Occurrence::Fortnightly; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Weekly && mult == 2); occ = Schedule::Occurrence::EveryHalfMonth; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryHalfMonth && mult == 1); occ = Schedule::Occurrence::EveryThreeWeeks; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Weekly && mult == 3); occ = Schedule::Occurrence::EveryFourWeeks; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Weekly && mult == 4); occ = Schedule::Occurrence::EveryThirtyDays; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Daily && mult == 30); occ = Schedule::Occurrence::Monthly; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Monthly && mult == 1); occ = Schedule::Occurrence::EveryEightWeeks; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Weekly && mult == 8); occ = Schedule::Occurrence::EveryOtherMonth; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Monthly && mult == 2); occ = Schedule::Occurrence::EveryThreeMonths; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Monthly && mult == 3); occ = Schedule::Occurrence::Quarterly; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Monthly && mult == 3); occ = Schedule::Occurrence::EveryFourMonths; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Monthly && mult == 4); occ = Schedule::Occurrence::TwiceYearly; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Monthly && mult == 6); occ = Schedule::Occurrence::Yearly; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Yearly && mult == 1); occ = Schedule::Occurrence::EveryOtherYear; mult = 1; MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Yearly && mult == 2); // Compound to Simple Occurrences occ = Schedule::Occurrence::Once; mult = 1; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Once && mult == 1); occ = Schedule::Occurrence::Daily; mult = 1; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Daily && mult == 1); occ = Schedule::Occurrence::Weekly; mult = 1; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Weekly && mult == 1); occ = Schedule::Occurrence::Weekly; mult = 2; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryOtherWeek && mult == 1); // Schedule::Occurrence::Fortnightly not converted back occ = Schedule::Occurrence::EveryHalfMonth; mult = 1; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryHalfMonth && mult == 1); occ = Schedule::Occurrence::Weekly; mult = 3; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryThreeWeeks && mult == 1); occ = Schedule::Occurrence::Weekly ; mult = 4; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryFourWeeks && mult == 1); occ = Schedule::Occurrence::Daily; mult = 30; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryThirtyDays && mult == 1); occ = Schedule::Occurrence::Monthly; mult = 1; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Monthly && mult == 1); occ = Schedule::Occurrence::Weekly; mult = 8; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryEightWeeks && mult == 1); occ = Schedule::Occurrence::Monthly; mult = 2; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryOtherMonth && mult == 1); occ = Schedule::Occurrence::Monthly; mult = 3; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryThreeMonths && mult == 1); // Schedule::Occurrence::Quarterly not converted back occ = Schedule::Occurrence::Monthly; mult = 4; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryFourMonths && mult == 1); occ = Schedule::Occurrence::Monthly; mult = 6; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::TwiceYearly && mult == 1); occ = Schedule::Occurrence::Yearly; mult = 1; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::Yearly && mult == 1); occ = Schedule::Occurrence::Yearly; mult = 2; MyMoneySchedule::compoundToSimpleOccurrence(mult, occ); QVERIFY(occ == Schedule::Occurrence::EveryOtherYear && mult == 1); } void MyMoneyScheduleTest::testProcessingDates() { /// @todo extend test to cover application usage (with a processing calendar defined) ? // There should be no processing calendar defined so // make sure fall back works MyMoneySchedule s; // Check there is no processing caledar defined. QVERIFY(s.processingCalendar() == nullptr); // This should be a processing day. QCOMPARE(s.isProcessingDate(QDate(2009, 12, 31)), true); // This should be a processing day when there is no calendar. QCOMPARE(s.isProcessingDate(QDate(2010, 1, 1)), true); // This should be a non-processing day as it is on a weekend. QCOMPARE(s.isProcessingDate(QDate(2010, 1, 2)), false); } void MyMoneyScheduleTest::testAdjustedNextPayment() { MyMoneySchedule s; QDate dueDate(2010, 5, 23); QDate adjustedDueDate(2010, 5, 21); s.setNextDueDate(dueDate); s.setOccurrence(Schedule::Occurrence::Monthly); s.setWeekendOption(Schedule::WeekendOption::MoveBefore); //if adjustedNextPayment works ok with adjusted date prior to the current date, it should return 2010-06-23 QDate nextDueDate(2010, 6, 23); //this is the current behaviour, and it is wrong //QCOMPARE(s.adjustedNextPayment(adjustedDueDate), adjustedDueDate); //this is the expected behaviour QCOMPARE(s.adjustedNextPayment(s.adjustedNextDueDate()), s.adjustedDate(nextDueDate, s.weekendOption())); } void MyMoneyScheduleTest::testAdjustedWhenItWillEnd() { MyMoneySchedule s; QDate endDate(2011, 8, 13); // this is a nonprocessing day because // it's a Saturday QDate refDate(2011, 8, 10); // just some ref date before the last payment s.setStartDate(endDate.addMonths(-1)); s.setOccurrence(Schedule::Occurrence::Monthly); s.setEndDate(endDate); // the next due date is on this day but the policy is to move the // schedule to the next processing day (Monday) s.setWeekendOption(Schedule::WeekendOption::MoveAfter); s.setNextDueDate(endDate); // the payment should be found between the respective date and one month after QCOMPARE(s.paymentDates(endDate, endDate.addMonths(1)).count(), 1); // the next payment must be the final one QCOMPARE(s.nextPayment(refDate), endDate); // and since it is on a Saturday, the adjusted one must be on the // following Monday QCOMPARE(s.adjustedNextPayment(refDate), endDate.addDays(2)); // reference for Sunday is still OK QCOMPARE(s.adjustedNextPayment(QDate(2011, 8, 14)), endDate.addDays(2)); // but it is finished on Monday (as reference date) QVERIFY(!s.adjustedNextPayment(QDate(2011, 8, 15)).isValid()); // check the # of remaining transactions s.setNextDueDate(endDate.addMonths(-1)); QCOMPARE(s.transactionsRemaining(), 2); } void MyMoneyScheduleTest::testProcessLastDayInMonth() { MyMoneySchedule s; // occurrence is unrelated s.setOccurrence(Schedule::Occurrence::Any); s.setLastDayInMonth(true); s.setNextDueDate(QDate(2010, 1, 1)); QCOMPARE(s.adjustedNextDueDate(), QDate(2010,1,31)); s.setNextDueDate(QDate(2010, 2, 1)); QCOMPARE(s.adjustedNextDueDate(), QDate(2010,2,28)); s.setNextDueDate(QDate(2016, 2, 1)); QCOMPARE(s.adjustedNextDueDate(), QDate(2016,2,29)); s.setNextDueDate(QDate(2016, 4, 1)); QCOMPARE(s.adjustedNextDueDate(), QDate(2016,4,30)); s.setLastDayInMonth(false); QCOMPARE(s.adjustedNextDueDate(), QDate(2016,4,1)); } diff --git a/kmymoney/plugins/icalendar/export/schedulestoicalendar.cpp b/kmymoney/plugins/icalendar/export/schedulestoicalendar.cpp index c0853f92e..ba522c396 100644 --- a/kmymoney/plugins/icalendar/export/schedulestoicalendar.cpp +++ b/kmymoney/plugins/icalendar/export/schedulestoicalendar.cpp @@ -1,386 +1,386 @@ /*************************************************************************** * Copyright 2009 Cristian Onet onet.cristian@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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ #include "schedulestoicalendar.h" #include #include #include // KDE includes #include #include #include // libical includes #include // KMyMoney includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyutils.h" #include "mymoneyschedule.h" #include "mymoneyaccount.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneypayee.h" #include "mymoneyenums.h" // plugin includes #include "pluginsettings.h" using namespace eMyMoney; int timeUnitsInSeconds(int optionValue) { // see how the items are added in the combobox of the settings editor widget static const int minute = 0; static const int hour = 1; static const int day = 2; switch (optionValue) { case minute: return 60; case hour: return 60*60; case day: return 24*60*60; default: return 1; } } int beforeAfterToInt(int optionValue) { // see how the items are added in the combobox of the settings editor widget static const int before = 0; static const int after = 1; switch (optionValue) { case before: return -1; case after: return 1; default: return -1; } } struct icaltimetype qdateToIcalTimeType(const QDate& date) { struct icaltimetype icalDate = icaltime_null_date(); icalDate.year = date.year(); icalDate.month = date.month(); icalDate.day = date.day(); icalDate.is_date = 1; return icalDate; } struct icaltimetype qdateTimeToIcalTimeType(const QDateTime& dateTime) { struct icaltimetype icalDateTime = icaltime_null_date(); icalDateTime.year = dateTime.date().year(); icalDateTime.month = dateTime.date().month(); icalDateTime.day = dateTime.date().day(); icalDateTime.hour = dateTime.time().hour(); icalDateTime.minute = dateTime.time().minute(); icalDateTime.second = dateTime.time().second(); icalDateTime.is_date = 0; return icalDateTime; } struct icalrecurrencetype scheduleToRecurenceRule(const MyMoneySchedule& schedule) { struct icalrecurrencetype recurrence; icalrecurrencetype_clear(&recurrence); if (schedule.willEnd()) recurrence.until = qdateToIcalTimeType(schedule.endDate()); recurrence.week_start = icalrecurrencetype_day_day_of_week(QLocale().firstDayOfWeek()); int frequencyFactor = 1; // used to translate kmymoney frequency to icalendar frequency - switch (schedule.occurrence()) { + switch (schedule.baseOccurrence()) { case Schedule::Occurrence::Daily: recurrence.freq = ICAL_DAILY_RECURRENCE; break; case Schedule::Occurrence::Weekly: recurrence.freq = ICAL_WEEKLY_RECURRENCE; break; case Schedule::Occurrence::Fortnightly: recurrence.freq = ICAL_WEEKLY_RECURRENCE; frequencyFactor = 2; break; case Schedule::Occurrence::EveryOtherWeek: recurrence.freq = ICAL_WEEKLY_RECURRENCE; frequencyFactor = 2; break; case Schedule::Occurrence::EveryHalfMonth: recurrence.freq = ICAL_WEEKLY_RECURRENCE; frequencyFactor = 2; break; case Schedule::Occurrence::EveryThreeWeeks: recurrence.freq = ICAL_WEEKLY_RECURRENCE; frequencyFactor = 3; break; case Schedule::Occurrence::EveryThirtyDays: recurrence.freq = ICAL_DAILY_RECURRENCE; frequencyFactor = 30; break; case Schedule::Occurrence::Monthly: recurrence.freq = ICAL_MONTHLY_RECURRENCE; break; case Schedule::Occurrence::EveryFourWeeks: recurrence.freq = ICAL_WEEKLY_RECURRENCE; frequencyFactor = 4; break; case Schedule::Occurrence::EveryEightWeeks: recurrence.freq = ICAL_WEEKLY_RECURRENCE; frequencyFactor = 8; break; case Schedule::Occurrence::EveryOtherMonth: recurrence.freq = ICAL_MONTHLY_RECURRENCE; frequencyFactor = 2; break; case Schedule::Occurrence::EveryThreeMonths: recurrence.freq = ICAL_MONTHLY_RECURRENCE; frequencyFactor = 3; break; case Schedule::Occurrence::TwiceYearly: recurrence.freq = ICAL_MONTHLY_RECURRENCE; frequencyFactor = 6; break; case Schedule::Occurrence::EveryOtherYear: recurrence.freq = ICAL_YEARLY_RECURRENCE; frequencyFactor = 2; break; case Schedule::Occurrence::Quarterly: recurrence.freq = ICAL_MONTHLY_RECURRENCE; frequencyFactor = 3; break; case Schedule::Occurrence::EveryFourMonths: recurrence.freq = ICAL_MONTHLY_RECURRENCE; frequencyFactor = 4; break; case Schedule::Occurrence::Yearly: recurrence.freq = ICAL_YEARLY_RECURRENCE; break; case Schedule::Occurrence::Once: case Schedule::Occurrence::Any: default: qWarning() << "Once, any or unknown recurrence returned recurrence is invalid" << endl; recurrence.freq = ICAL_NO_RECURRENCE; break; } recurrence.interval = frequencyFactor*schedule.occurrenceMultiplier(); return recurrence; } QString scheduleToDescription(const MyMoneySchedule& schedule) { auto file = MyMoneyFile::instance(); const MyMoneyAccount& account = schedule.account(); const MyMoneyTransaction& transaction = schedule.transaction(); QString payeeName; MyMoneyMoney amount; QString category; bool isTransfer = false; bool isIncome = false; foreach (const auto split, transaction.splits()) { if (split.accountId() != account.id()) { if (!category.isEmpty()) category += ", "; // this is a split transaction const MyMoneyAccount& splitAccount = file->account(split.accountId()); category = splitAccount.name(); isTransfer = splitAccount.accountGroup() == Account::Type::Asset || splitAccount.accountGroup() == Account::Type::Liability; isIncome = splitAccount.accountGroup() == Account::Type::Income; } else { payeeName = file->payee(split.payeeId()).name(); // make the amount positive since the message makes it clear if this is an income or expense amount = split.shares().abs(); } } QString description = isTransfer ? i18n("Transfer from %1 to %2, Payee %3, amount %4", account.name(), category, payeeName, MyMoneyUtils::formatMoney(amount, file->currency(account.currencyId()))) : ( isIncome ? i18n("From %1 into %2, Category %3, sum of %4", payeeName, account.name(), category, MyMoneyUtils::formatMoney(amount, file->currency(account.currencyId()))) : i18n("From account %1, Pay to %2, Category %3, sum of %4", account.name(), payeeName, category, MyMoneyUtils::formatMoney(amount, file->currency(account.currencyId()))) ); if (!transaction.memo().isEmpty()) description = i18nc("The first string is the schedules details", "%1, memo %2", description, transaction.memo()); return description; } struct KMMSchedulesToiCalendar::Private { QString m_icalendarAsString; }; KMMSchedulesToiCalendar::KMMSchedulesToiCalendar() : d(new Private) { } KMMSchedulesToiCalendar::~KMMSchedulesToiCalendar() { delete d; } void KMMSchedulesToiCalendar::exportToFile(const QString& filePath, bool settingsChaged) { if (!MyMoneyFile::instance()->storageAttached()) return; QFile icsFile(filePath); icsFile.open(QIODevice::ReadOnly); QTextStream stream(&icsFile); d->m_icalendarAsString = stream.readAll(); icsFile.close(); // create the calendar bool newCalendar = false; icalcomponent* vCalendar = 0; if (d->m_icalendarAsString.isEmpty()) { newCalendar = true; vCalendar = icalcomponent_new_vcalendar(); } else { vCalendar = icalcomponent_new_from_string(d->m_icalendarAsString.toUtf8()); if (vCalendar == 0) { qDebug() << "Error parsing the following string into an icalendar:" << endl; qDebug() << d->m_icalendarAsString << endl; qDebug() << "so we will overwrite this with a new calendar" << endl; newCalendar = true; vCalendar = icalcomponent_new_vcalendar(); } } if (vCalendar == 0) { // one way or the other we must have a calendar by now qDebug() << "Unable to create vcalendar component" << endl; return; } if (newCalendar) { // set proid and version icalcomponent_add_property(vCalendar, icalproperty_new_prodid("icalendarexport"));; icalcomponent_add_property(vCalendar, icalproperty_new_version("2.0")); } // export schedules as TODOs auto file = MyMoneyFile::instance(); QList schedules = file->scheduleList(); for (QList::const_iterator itSchedule = schedules.constBegin(); itSchedule != schedules.constEnd(); ++itSchedule) { const MyMoneySchedule& myMoneySchedule = *itSchedule; if (myMoneySchedule.isFinished()) continue; // skip this schedule if it is already finished icalcomponent* schedule = 0; bool newTodo = false; if (!newCalendar) { // try to find the schedule to update it if we do not use a new calendar icalcomponent* itVTODO = icalcomponent_get_first_component(vCalendar, ICAL_VTODO_COMPONENT); for (; itVTODO != 0; itVTODO = icalcomponent_get_next_component(vCalendar, ICAL_VTODO_COMPONENT)) { if (icalcomponent_get_uid(itVTODO) == myMoneySchedule.id()) { // we found our todo stop searching schedule = itVTODO; break; } } if (schedule == 0) { schedule = icalcomponent_new_vtodo(); newTodo = true; } } else { schedule = icalcomponent_new_vtodo(); newTodo = true; } // description icalcomponent_set_description(schedule, scheduleToDescription(myMoneySchedule).toUtf8()); // summary icalcomponent_set_summary(schedule, myMoneySchedule.name().toUtf8()); // uid icalcomponent_set_uid(schedule, myMoneySchedule.id().toUtf8()); // dtstart icalcomponent_set_dtstart(schedule, qdateToIcalTimeType(myMoneySchedule.startDate())); // due icalcomponent_set_due(schedule, qdateToIcalTimeType(myMoneySchedule.nextDueDate())); if (newTodo) { // created icalcomponent_add_property(schedule, icalproperty_new_created(qdateTimeToIcalTimeType(QDateTime::currentDateTime()))); } else { // last modified icalproperty* pLastMod = icalcomponent_get_first_property(schedule, ICAL_LASTMODIFIED_PROPERTY); if (pLastMod != 0) { // set the current property icalproperty_set_lastmodified(pLastMod, qdateTimeToIcalTimeType(QDateTime::currentDateTime())); } else { // create a new property icalcomponent_add_property(schedule, icalproperty_new_lastmodified(qdateTimeToIcalTimeType(QDateTime::currentDateTime()))); } } // recurrence icalproperty* pRRule = icalcomponent_get_first_property(schedule, ICAL_RRULE_PROPERTY); if (pRRule != 0) { icalcomponent_remove_property(schedule, pRRule); } - if (myMoneySchedule.occurrence() != Schedule::Occurrence::Once && myMoneySchedule.occurrence() != Schedule::Occurrence::Any) + if (myMoneySchedule.occurrence() != Schedule::Occurrence::Once && myMoneySchedule.baseOccurrence() != Schedule::Occurrence::Any) icalcomponent_add_property(schedule, icalproperty_new_rrule(scheduleToRecurenceRule(myMoneySchedule))); icalcomponent* oldAlarm = icalcomponent_get_first_component(schedule, ICAL_VALARM_COMPONENT); if (oldAlarm && settingsChaged) icalcomponent_remove_component(schedule, oldAlarm); if (PluginSettings::createAlarm() && (!oldAlarm || settingsChaged)) { // alarm: beginning wiht one day before the todo is due every one hour icalcomponent* alarm = icalcomponent_new_valarm(); // alarm: action icalcomponent_add_property(alarm, icalproperty_new_action(ICAL_ACTION_DISPLAY)); // alarm: description icalcomponent_set_description(alarm, scheduleToDescription(myMoneySchedule).toUtf8()); // alarm: trigger int triggerInterval = beforeAfterToInt(PluginSettings::beforeAfter()) * PluginSettings::timeUnits() * timeUnitsInSeconds(PluginSettings::timeUnitInSeconds()); icalcomponent_add_property(alarm, icalproperty_new_trigger(icaltriggertype_from_int(triggerInterval))); // alarm: duration int intervalBetweenReminders = PluginSettings::intervalBetweenRemindersTimeUnits() * timeUnitsInSeconds(PluginSettings::intervalBetweenRemindersTimeUnitInSeconds()); icalcomponent_set_duration(alarm, icaldurationtype_from_int(intervalBetweenReminders)); if (PluginSettings::repeatingReminders()) { // alarm: repeat icalcomponent_add_property(alarm, icalproperty_new_repeat(PluginSettings::numberOfReminders())); } // add the alarm to the schedule icalcomponent_add_component(schedule, alarm); } // add the schedule to the caledar if (newTodo) icalcomponent_add_component(vCalendar, schedule); } icsFile.open(QIODevice::WriteOnly); d->m_icalendarAsString = QString::fromUtf8(icalcomponent_as_ical_string(vCalendar)); // reclaim some memory :) icalcomponent_free(vCalendar); // write the calendar to the file stream << d->m_icalendarAsString << endl; icsFile.close(); } diff --git a/kmymoney/plugins/sql/mymoneystoragesql.cpp b/kmymoney/plugins/sql/mymoneystoragesql.cpp index 2844901b1..c5e9d005f 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql.cpp +++ b/kmymoney/plugins/sql/mymoneystoragesql.cpp @@ -1,2903 +1,2903 @@ /*************************************************************************** mymoneystoragesql.cpp --------------------- begin : 11 November 2005 copyright : (C) 2005 by Tony Bloomfield email : tonybloom@users.sourceforge.net : Fernando Vilas : Christian Dávid (C) 2017 by Ł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 "mymoneystoragesql_p.h" // ---------------------------------------------------------------------------- // System Includes // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include "KMessageBox" // ---------------------------------------------------------------------------- // Project Includes //************************ Constructor/Destructor ***************************** MyMoneyStorageSql::MyMoneyStorageSql(MyMoneyStorageMgr *storage, const QUrl &url) : QSqlDatabase(QUrlQuery(url).queryItemValue("driver")), d_ptr(new MyMoneyStorageSqlPrivate(this)) { Q_D(MyMoneyStorageSql); d->m_storage = storage; } MyMoneyStorageSql::~MyMoneyStorageSql() { try { close(true); } catch (const MyMoneyException &e) { qDebug() << "Caught Exception in MMStorageSql dtor: " << e.what(); } Q_D(MyMoneyStorageSql); delete d; } uint MyMoneyStorageSql::currentVersion() const { Q_D(const MyMoneyStorageSql); return (d->m_db.currentVersion()); } int MyMoneyStorageSql::open(const QUrl &url, int openMode, bool clear) { Q_D(MyMoneyStorageSql); try { int rc = 0; d->m_driver = MyMoneyDbDriver::create(QUrlQuery(url).queryItemValue("driver")); //get the input options QStringList options = QUrlQuery(url).queryItemValue("options").split(','); d->m_loadAll = true; // force loading whole database into memory since unification of storages // options.contains("loadAll")/*|| m_mode == 0*/; d->m_override = options.contains("override"); // create the database connection // regarding the construction of the database name see the discussion on // https://phabricator.kde.org/D12681. In case of a local file based DB // driver we cut off the leading slash only in those cases, where we // a) have a file based DB on Windows systems and // b) have a server based DB. // so that we do not modify the absolute path on *nix based systems // in case of a DB based driver QString dbName = url.path(); if(d->m_driver->requiresExternalFile()) { #ifdef Q_OS_WIN dbName = url.path().remove(0, 1); // remove separator slash for files on Windows #endif } else { dbName = url.path().remove(0, 1); // remove separator slash for server based databases } setDatabaseName(dbName); setHostName(url.host()); setUserName(url.userName()); setPassword(url.password()); if (QUrlQuery(url).queryItemValue("driver").contains("QMYSQL")) { setConnectOptions("MYSQL_OPT_RECONNECT=1"); } QSqlQuery query(*this); switch (openMode) { case QIODevice::ReadOnly: // OpenDatabase menu entry (or open last file) case QIODevice::ReadWrite: // Save menu entry with database open // this may be a sqlite file opened from the recently used list // but which no longer exists. In that case, open will work but create an empty file. // This is not what the user's after; he may accuse KMM of deleting all his data! if (d->m_driver->requiresExternalFile()) { if (!d->fileExists(dbName)) { rc = 1; break; } } if (!QSqlDatabase::open()) { d->buildError(QSqlQuery(*this), Q_FUNC_INFO, "opening database"); rc = 1; } else { if (driverName().compare(QLatin1String("QSQLCIPHER")) == 0) { auto passphrase = password(); while (true) { if (!passphrase.isEmpty()) { query.exec(QString::fromLatin1("PRAGMA cipher_version")); if(!query.next()) throw MYMONEYEXCEPTION_CSTRING("Based on empty cipher_version, libsqlcipher is not in use."); query.exec(QString::fromLatin1("PRAGMA key = '%1'").arg(passphrase)); // SQLCipher feature to decrypt a database } query.exec(QStringLiteral("SELECT count(*) FROM sqlite_master")); // SQLCipher recommended way to check if password is correct if (query.next()) { rc = d->createTables(); // check all tables are present, create if not break; } auto ok = false; passphrase = QInputDialog::getText(nullptr, i18n("Password"), i18n("You're trying to open an encrypted database.\n" "Please provide a password in order to open it."), QLineEdit::Password, QString(), &ok); if (!ok) { QSqlDatabase::close(); throw MYMONEYEXCEPTION_CSTRING("Bad password."); } } } else { rc = d->createTables(); // check all tables are present, create if not } } break; case QIODevice::WriteOnly: // SaveAs Database - if exists, must be empty, if not will create { // Try to open the database. // If that fails, try to create the database, then try to open it again. d->m_newDatabase = true; // QSqlDatabase::open() always returns true on MS Windows // even if SQLite database doesn't exist auto isSQLiteAutocreated = false; if (driverName().compare(QLatin1String("QSQLITE")) == 0 || driverName().compare(QLatin1String("QSQLCIPHER")) == 0) { if (!QFile::exists(dbName)) isSQLiteAutocreated = true; } const auto isSuccessfullyOpened = QSqlDatabase::open(); if (!isSuccessfullyOpened || (isSQLiteAutocreated && isSuccessfullyOpened)) { if (!d->createDatabase(url)) { rc = 1; } else { if (!QSqlDatabase::open()) { d->buildError(QSqlQuery(*this), Q_FUNC_INFO, "opening new database"); rc = 1; } else { query.exec(QString::fromLatin1("PRAGMA key = '%1'").arg(password())); rc = d->createTables(); } } } else { if (driverName().compare(QLatin1String("QSQLCIPHER")) == 0 && !password().isEmpty()) { KMessageBox::information(nullptr, i18n("Overwriting an existing database with an encrypted database is not yet supported.\n" "Please save your database under a new name.")); QSqlDatabase::close(); rc = 3; return rc; } rc = d->createTables(); if (rc == 0) { if (clear) { d->clean(); } else { rc = d->isEmpty(); } } } break; } default: qWarning("%s", qPrintable(QString("%1 - unknown open mode %2").arg(Q_FUNC_INFO).arg(openMode))); } if (rc != 0) return (rc); // bypass logon check if we are creating a database if (d->m_newDatabase) return(0); // check if the database is locked, if not lock it d->readFileInfo(); if (!d->m_logonUser.isEmpty() && (!d->m_override)) { d->m_error = i18n("Database apparently in use\nOpened by %1 on %2 at %3.\nOpen anyway?", d->m_logonUser, d->m_logonAt.date().toString(Qt::ISODate), d->m_logonAt.time().toString("hh.mm.ss")); qDebug("%s", qPrintable(d->m_error)); close(false); rc = -1; // retryable error } else { d->m_logonUser = url.userName() + '@' + url.host(); d->m_logonAt = QDateTime::currentDateTime(); d->writeFileInfo(); } return(rc); } catch (const QString& s) { qDebug("%s", qPrintable(s)); return (1); } } void MyMoneyStorageSql::close(bool logoff) { Q_D(MyMoneyStorageSql); if (QSqlDatabase::isOpen()) { if (logoff) { MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->m_logonUser.clear(); d->writeFileInfo(); } QSqlDatabase::close(); QSqlDatabase::removeDatabase(connectionName()); } } ulong MyMoneyStorageSql::getRecCount(const QString& table) const { Q_D(const MyMoneyStorageSql); QSqlQuery q(*const_cast (this)); q.prepare(QString("SELECT COUNT(*) FROM %1;").arg(table)); if ((!q.exec()) || (!q.next())) { // krazy:exclude=crashy d->buildError(q, Q_FUNC_INFO, "error retrieving record count"); qFatal("Error retrieving record count"); // definitely shouldn't happen } return ((ulong) q.value(0).toULongLong()); } ////////////////////////////////////////////////////////////////// bool MyMoneyStorageSql::readFile() { Q_D(MyMoneyStorageSql); d->m_displayStatus = true; try { d->readFileInfo(); d->readInstitutions(); if (d->m_loadAll) { readPayees(); } else { QList user; user.append(QString("USER")); readPayees(user); } readTags(); d->readCurrencies(); d->readSecurities(); d->readAccounts(); if (d->m_loadAll) { d->readTransactions(); } else { if (d->m_preferred.filterSet().singleFilter.accountFilter) readTransactions(d->m_preferred); } d->readSchedules(); d->readPrices(); d->readReports(); d->readBudgets(); d->readOnlineJobs(); //FIXME - ?? if (m_mode == 0) //m_storage->rebuildAccountBalances(); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. d->m_storage->setLastModificationDate(d->m_storage->lastModificationDate()); // FIXME?? if (m_mode == 0) m_storage = NULL; // make sure the progress bar is not shown any longer d->signalProgress(-1, -1); d->m_displayStatus = false; //MyMoneySqlQuery::traceOn(); return true; } catch (const QString &) { return false; } // this seems to be nonsense, but it clears the dirty flag // as a side-effect. } // The following is called from 'SaveAsDatabase' bool MyMoneyStorageSql::writeFile() { Q_D(MyMoneyStorageSql); // initialize record counts and hi ids d->m_institutions = d->m_accounts = d->m_payees = d->m_tags = d->m_transactions = d->m_splits = d->m_securities = d->m_prices = d->m_currencies = d->m_schedules = d->m_reports = d->m_kvps = d->m_budgets = 0; d->m_hiIdInstitutions = d->m_hiIdPayees = d->m_hiIdTags = d->m_hiIdAccounts = d->m_hiIdTransactions = d->m_hiIdSchedules = d->m_hiIdSecurities = d->m_hiIdReports = d->m_hiIdBudgets = 0; d->m_onlineJobs = d->m_payeeIdentifier = 0; d->m_displayStatus = true; try { const auto driverName = this->driverName(); if (driverName.compare(QLatin1String("QSQLITE")) == 0 || driverName.compare(QLatin1String("QSQLCIPHER")) == 0) { QSqlQuery query(*this); query.exec("PRAGMA foreign_keys = ON"); // this is needed for "ON UPDATE" and "ON DELETE" to work } MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->writeInstitutions(); d->writePayees(); d->writeTags(); d->writeAccounts(); d->writeTransactions(); d->writeSchedules(); d->writeSecurities(); d->writePrices(); d->writeCurrencies(); d->writeReports(); d->writeBudgets(); d->writeOnlineJobs(); d->writeFileInfo(); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. //m_storage->setLastModificationDate(m_storage->lastModificationDate()); // FIXME?? if (m_mode == 0) m_storage = NULL; // make sure the progress bar is not shown any longer d->signalProgress(-1, -1); d->m_displayStatus = false; // this seems to be nonsense, but it clears the dirty flag // as a side-effect. d->m_storage->setLastModificationDate(d->m_storage->lastModificationDate()); return true; } catch (const QString &) { return false; } } QString MyMoneyStorageSql::lastError() const { Q_D(const MyMoneyStorageSql); return d->m_error; } // --------------- SQL Transaction (commit unit) handling ----------------------------------- void MyMoneyStorageSql::startCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); if (d->m_commitUnitStack.isEmpty()) { if (!transaction()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "starting commit unit")); } d->m_commitUnitStack.push(callingFunction); } bool MyMoneyStorageSql::endCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); // for now, we don't know if there were any changes made to the data so // we expect the data to have changed. This assumption causes some unnecessary // repaints of the UI here and there, but for now it's ok. If we can determine // that the commit() really changes the data, we can return that information // as value of this method. bool rc = true; if (d->m_commitUnitStack.isEmpty()) { throw MYMONEYEXCEPTION_CSTRING("Empty commit unit stack while trying to commit"); } if (callingFunction != d->m_commitUnitStack.top()) qDebug("%s", qPrintable(QString("%1 - %2 s/be %3").arg(Q_FUNC_INFO).arg(callingFunction).arg(d->m_commitUnitStack.top()))); d->m_commitUnitStack.pop(); if (d->m_commitUnitStack.isEmpty()) { //qDebug() << "Committing with " << QSqlQuery::refCount() << " queries"; if (!commit()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "ending commit unit")); } return rc; } void MyMoneyStorageSql::cancelCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); if (d->m_commitUnitStack.isEmpty()) return; if (callingFunction != d->m_commitUnitStack.top()) qDebug("%s", qPrintable(QString("%1 - %2 s/be %3").arg(Q_FUNC_INFO).arg(callingFunction).arg(d->m_commitUnitStack.top()))); d->m_commitUnitStack.clear(); if (!rollback()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "cancelling commit unit") + ' ' + callingFunction); } ///////////////////////////////////////////////////////////////////// void MyMoneyStorageSql::fillStorage() { Q_D(MyMoneyStorageSql); // if (!m_transactionListRead) // make sure we have loaded everything d->readTransactions(); // if (!m_payeeListRead) readPayees(); } //------------------------------ Write SQL routines ---------------------------------------- // **** Institutions **** void MyMoneyStorageSql::addInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmInstitutions"].insertString()); QList iList; iList << inst; d->writeInstitutionList(iList , q); ++d->m_institutions; d->writeFileInfo(); } void MyMoneyStorageSql::modifyInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmInstitutions"].updateString()); QVariantList kvpList; kvpList << inst.id(); d->deleteKeyValuePairs("OFXSETTINGS", kvpList); QList iList; iList << inst; d->writeInstitutionList(iList , q); d->writeFileInfo(); } void MyMoneyStorageSql::removeInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << inst.id(); d->deleteKeyValuePairs("OFXSETTINGS", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmInstitutions"].deleteString()); query.bindValue(":id", inst.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Institution")); // krazy:exclude=crashy --d->m_institutions; d->writeFileInfo(); } void MyMoneyStorageSql::addPayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayees"].insertString()); d->writePayee(payee, query); ++d->m_payees; QVariantList identIds; QList idents = payee.payeeIdentifiers(); // Store ids which have to be stored in the map table identIds.reserve(idents.count()); foreach (payeeIdentifier ident, idents) { try { // note: this changes ident addPayeeIdentifier(ident); identIds.append(ident.idString()); } catch (const payeeIdentifier::empty &) { } } if (!identIds.isEmpty()) { // Create lists for batch processing QVariantList order; QVariantList payeeIdList; order.reserve(identIds.size()); payeeIdList.reserve(identIds.size()); for (int i = 0; i < identIds.size(); ++i) { order << i; payeeIdList << payee.id(); } query.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, identifierId, userOrder) VALUES(?, ?, ?)"); query.bindValue(0, payeeIdList); query.bindValue(1, identIds); query.bindValue(2, order); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing payee's identifiers")); // krazy:exclude=crashy } d->writeFileInfo(); } void MyMoneyStorageSql::modifyPayee(MyMoneyPayee payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayees"].updateString()); d->writePayee(payee, query); // Get a list of old identifiers first query.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payee's identifiers (getting old values failed)")); // krazy:exclude=crashy QStringList oldIdentIds; oldIdentIds.reserve(query.numRowsAffected()); while (query.next()) oldIdentIds << query.value(0).toString(); // Add new and modify old payeeIdentifiers foreach (payeeIdentifier ident, payee.payeeIdentifiers()) { if (ident.idString().isEmpty()) { payeeIdentifier oldIdent(ident); addPayeeIdentifier(ident); // addPayeeIdentifier could fail (throws an exception then) only remove old // identifier if new one is stored correctly payee.removePayeeIdentifier(oldIdent); payee.addPayeeIdentifier(ident); } else { modifyPayeeIdentifier(ident); payee.modifyPayeeIdentifier(ident); oldIdentIds.removeAll(ident.idString()); } } // Remove identifiers which are not used anymore foreach (QString idToRemove, oldIdentIds) { payeeIdentifier ident(fetchPayeeIdentifier(idToRemove)); removePayeeIdentifier(ident); } // Update relation table query.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payee's identifiers (delete from mapping table)")); // krazy:exclude=crashy // Get list again because modifiyPayeeIdentifier which is used above may change the id QList idents(payee.payeeIdentifiers()); QVariantList order; QVariantList payeeIdList; QVariantList identIdList; order.reserve(idents.size()); payeeIdList.reserve(idents.size()); identIdList.reserve(idents.size()); { QList::const_iterator end = idents.constEnd(); int i = 0; for (QList::const_iterator iter = idents.constBegin(); iter != end; ++iter, ++i) { order << i; payeeIdList << payee.id(); identIdList << iter->idString(); } } query.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, userOrder, identifierId) VALUES(?, ?, ?)"); query.bindValue(0, payeeIdList); query.bindValue(1, order); query.bindValue(2, identIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing payee's identifiers during modify")); // krazy:exclude=crashy d->writeFileInfo(); } void MyMoneyStorageSql::modifyUserInfo(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmPayees"].updateString()); d->writePayee(payee, q, true); d->writeFileInfo(); } void MyMoneyStorageSql::removePayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); // Get identifiers first so we know which to delete query.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("removing payee's identifiers (getting old values failed)")); // krazy:exclude=crashy QStringList identIds; while (query.next()) identIds << query.value(0).toString(); QMap idents = fetchPayeeIdentifiers(identIds); foreach (payeeIdentifier ident, idents) { removePayeeIdentifier(ident); } // Delete entries from mapping table query.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("removing payee's identifiers (delete from mapping table)")); // krazy:exclude=crashy // Delete payee query.prepare(d->m_db.m_tables["kmmPayees"].deleteString()); query.bindValue(":id", payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Payee")); // krazy:exclude=crashy --d->m_payees; d->writeFileInfo(); } // **** Tags **** void MyMoneyStorageSql::addTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTags"].insertString()); d->writeTag(tag, q); ++d->m_tags; d->writeFileInfo(); } void MyMoneyStorageSql::modifyTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTags"].updateString()); d->writeTag(tag, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmTags"].deleteString()); query.bindValue(":id", tag.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Tag")); // krazy:exclude=crashy --d->m_tags; d->writeFileInfo(); } // **** Accounts **** void MyMoneyStorageSql::addAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmAccounts"].insertString()); QList aList; aList << acc; d->writeAccountList(aList, q); ++d->m_accounts; d->writeFileInfo(); } void MyMoneyStorageSql::modifyAccount(const MyMoneyAccount& acc) { QList aList; aList << acc; modifyAccountList(aList); } void MyMoneyStorageSql::modifyAccountList(const QList& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmAccounts"].updateString()); QVariantList kvpList; foreach (const MyMoneyAccount& a, acc) { kvpList << a.id(); } d->deleteKeyValuePairs("ACCOUNT", kvpList); d->deleteKeyValuePairs("ONLINEBANKING", kvpList); d->writeAccountList(acc, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << acc.id(); d->deleteKeyValuePairs("ACCOUNT", kvpList); d->deleteKeyValuePairs("ONLINEBANKING", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmAccounts"].deleteString()); query.bindValue(":id", acc.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Account")); // krazy:exclude=crashy --d->m_accounts; d->writeFileInfo(); } // **** Transactions and Splits **** void MyMoneyStorageSql::addTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // add the transaction and splits QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTransactions"].insertString()); d->writeTransaction(tx.id(), tx, q, "N"); ++d->m_transactions; QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); ++d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); // in the fileinfo record, update lastMod, txCount, next TxId d->writeFileInfo(); } void MyMoneyStorageSql::modifyTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // remove the splits of the old tx from the count table QSqlQuery query(*this); query.prepare("SELECT accountId FROM kmmSplits WHERE transactionId = :txId;"); query.bindValue(":txId", tx.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("retrieving old splits")); while (query.next()) { QString id = query.value(0).toString(); --d->m_transactionCountMap[id]; } // add the transaction and splits query.prepare(d->m_db.m_tables["kmmTransactions"].updateString()); d->writeTransaction(tx.id(), tx, query, "N"); QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); ++d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); //writeSplits(tx.id(), "N", tx.splits()); // in the fileinfo record, update lastMod d->writeFileInfo(); } void MyMoneyStorageSql::removeTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->deleteTransaction(tx.id()); --d->m_transactions; QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); --d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); // in the fileinfo record, update lastModDate, txCount d->writeFileInfo(); } // **** Schedules **** void MyMoneyStorageSql::addSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSchedules"].insertString()); d->writeSchedule(sched, q, true); ++d->m_schedules; d->writeFileInfo(); } void MyMoneyStorageSql::modifySchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSchedules"].updateString()); d->writeSchedule(sched, q, false); d->writeFileInfo(); } void MyMoneyStorageSql::removeSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->deleteSchedule(sched.id()); --d->m_schedules; d->writeFileInfo(); } // **** Securities **** void MyMoneyStorageSql::addSecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSecurities"].insertString()); d->writeSecurity(sec, q); ++d->m_securities; d->writeFileInfo(); } void MyMoneyStorageSql::modifySecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << sec.id(); d->deleteKeyValuePairs("SECURITY", kvpList); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSecurities"].updateString()); d->writeSecurity(sec, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeSecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << sec.id(); d->deleteKeyValuePairs("SECURITY", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmSecurities"].deleteString()); query.bindValue(":id", kvpList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Security")); --d->m_securities; d->writeFileInfo(); } // **** Prices **** void MyMoneyStorageSql::addPrice(const MyMoneyPrice& p) { Q_D(MyMoneyStorageSql); if (d->m_readingPrices) return; // the app always calls addPrice, whether or not there is already one there MyMoneyDbTransaction t(*this, Q_FUNC_INFO); bool newRecord = false; QSqlQuery query(*this); QString s = d->m_db.m_tables["kmmPrices"].selectAllString(false); s += " WHERE fromId = :fromId AND toId = :toId AND priceDate = :priceDate;"; query.prepare(s); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("finding Price")); // krazy:exclude=crashy if (query.next()) { query.prepare(d->m_db.m_tables["kmmPrices"].updateString()); } else { query.prepare(d->m_db.m_tables["kmmPrices"].insertString()); ++d->m_prices; newRecord = true; } query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); query.bindValue(":price", p.rate(QString()).toString()); const MyMoneySecurity sec = d->m_storage->security(p.to()); query.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", sec.pricePrecision())); query.bindValue(":priceSource", p.source()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing Price")); // krazy:exclude=crashy if (newRecord) d->writeFileInfo(); } void MyMoneyStorageSql::removePrice(const MyMoneyPrice& p) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPrices"].deleteString()); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Price")); // krazy:exclude=crashy --d->m_prices; d->writeFileInfo(); } // **** Currencies **** void MyMoneyStorageSql::addCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmCurrencies"].insertString()); d->writeCurrency(sec, q); ++d->m_currencies; d->writeFileInfo(); } void MyMoneyStorageSql::modifyCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmCurrencies"].updateString()); d->writeCurrency(sec, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmCurrencies"].deleteString()); query.bindValue(":ISOcode", sec.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Currency")); // krazy:exclude=crashy --d->m_currencies; d->writeFileInfo(); } void MyMoneyStorageSql::addReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmReportConfig"].insertString()); d->writeReport(rep, q); ++d->m_reports; d->writeFileInfo(); } void MyMoneyStorageSql::modifyReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmReportConfig"].updateString()); d->writeReport(rep, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("DELETE FROM kmmReportConfig WHERE id = :id"); query.bindValue(":id", rep.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Report")); // krazy:exclude=crashy --d->m_reports; d->writeFileInfo(); } void MyMoneyStorageSql::addBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmBudgetConfig"].insertString()); d->writeBudget(bud, q); ++d->m_budgets; d->writeFileInfo(); } void MyMoneyStorageSql::modifyBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmBudgetConfig"].updateString()); d->writeBudget(bud, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmBudgetConfig"].deleteString()); query.bindValue(":id", bud.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Budget")); // krazy:exclude=crashy --d->m_budgets; d->writeFileInfo(); } void MyMoneyStorageSql::addOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("INSERT INTO kmmOnlineJobs (id, type, jobSend, bankAnswerDate, state, locked) VALUES(:id, :type, :jobSend, :bankAnswerDate, :state, :locked);"); d->writeOnlineJob(job, query); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing onlineJob")); // krazy:exclude=crashy ++d->m_onlineJobs; try { // Save online task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { } } void MyMoneyStorageSql::modifyOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); Q_ASSERT(!job.id().isEmpty()); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(QLatin1String( "UPDATE kmmOnlineJobs SET " " type = :type, " " jobSend = :jobSend, " " bankAnswerDate = :bankAnswerDate, " " state = :state, " " locked = :locked " " WHERE id = :id" )); d->writeOnlineJob(job, query); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing onlineJob")); // krazy:exclude=crashy try { // Modify online task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { // If there is no task attached this is fine as well } } void MyMoneyStorageSql::removeOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // Remove onlineTask first, because it could have a contraint // which could block the removal of the onlineJob try { // Remove task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Remove, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { } QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmOnlineJobs"].deleteString()); query.bindValue(":id", job.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting onlineJob")); // krazy:exclude=crashy --d->m_onlineJobs; } void MyMoneyStorageSql::addPayeeIdentifier(payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); ident = payeeIdentifier(incrementPayeeIdentfierId(), ident); QSqlQuery q(*this); q.prepare("INSERT INTO kmmPayeeIdentifier (id, type) VALUES(:id, :type)"); d->writePayeeIdentifier(ident, q); ++d->m_payeeIdentifier; try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, ident); } catch (const payeeIdentifier::empty &) { } } void MyMoneyStorageSql::modifyPayeeIdentifier(const payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("SELECT type FROM kmmPayeeIdentifier WHERE id = ?"); query.bindValue(0, ident.idString()); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payeeIdentifier")); // krazy:exclude=crashy bool typeChanged = (query.value(0).toString() != ident.iid()); if (typeChanged) { // Delete old identifier if type changed const payeeIdentifier oldIdent(fetchPayeeIdentifier(ident.idString())); try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, oldIdent); } catch (const payeeIdentifier::empty &) { // Note: this should not happen because the ui does not offer a way to change // the type of an payeeIdentifier if it was not correctly loaded. throw MYMONEYEXCEPTION((QString::fromLatin1("Could not modify payeeIdentifier '") + ident.idString() + QLatin1String("' because type changed and could not remove identifier of old type. Maybe a plugin is missing?")) ); // krazy:exclude=crashy } } query.prepare("UPDATE kmmPayeeIdentifier SET type = :type WHERE id = :id"); d->writePayeeIdentifier(ident, query); try { if (typeChanged) d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, ident); else d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, ident); } catch (const payeeIdentifier::empty &) { } } void MyMoneyStorageSql::removePayeeIdentifier(const payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // Remove first, the table could have a contraint which prevents removal // of row in kmmPayeeIdentifier try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Remove, ident); } catch (const payeeIdentifier::empty &) { } QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayeeIdentifier"].deleteString()); query.bindValue(":id", ident.idString()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting payeeIdentifier")); // krazy:exclude=crashy --d->m_payeeIdentifier; } // **** Key/value pairs **** //******************************** read SQL routines ************************************** /*void MyMoneyStorageSql::setVersion (const QString& version) { m_dbVersion = version.section('.', 0, 0).toUInt(); m_minorVersion = version.section('.', 1, 1).toUInt(); // Okay, I made a cockup by forgetting to include a fixversion in the database // design, so we'll use the minor version as fix level (similar to VERSION // and FIXVERSION in XML file format). A second mistake was setting minor version to 1 // in the first place, so we need to subtract one on reading and add one on writing (sigh)!! m_storage->setFileFixVersion( m_minorVersion - 1); }*/ QMap MyMoneyStorageSql::fetchInstitutions(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int institutionsNb = (idList.isEmpty() ? d->m_institutions : idList.size()); d->signalProgress(0, institutionsNb, QObject::tr("Loading institutions...")); int progress = 0; QMap iList; ulong lastId = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmInstitutions"]; QSqlQuery sq(*const_cast (this)); sq.prepare("SELECT id from kmmAccounts where institutionId = :id"); QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" id = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString::fromLatin1("reading Institution"))); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int managerCol = t.fieldNumber("manager"); int routingCodeCol = t.fieldNumber("routingCode"); int addressStreetCol = t.fieldNumber("addressStreet"); int addressCityCol = t.fieldNumber("addressCity"); int addressZipcodeCol = t.fieldNumber("addressZipcode"); int telephoneCol = t.fieldNumber("telephone"); while (query.next()) { MyMoneyInstitution inst; QString iid = GETSTRING(idCol); inst.setName(GETSTRING(nameCol)); inst.setManager(GETSTRING(managerCol)); inst.setSortcode(GETSTRING(routingCodeCol)); inst.setStreet(GETSTRING(addressStreetCol)); inst.setCity(GETSTRING(addressCityCol)); inst.setPostcode(GETSTRING(addressZipcodeCol)); inst.setTelephone(GETSTRING(telephoneCol)); // get list of subaccounts sq.bindValue(":id", iid); if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Institution AccountList")); // krazy:exclude=crashy QStringList aList; while (sq.next()) aList.append(sq.value(0).toString()); foreach (const QString& it, aList) inst.addAccountId(it); iList[iid] = MyMoneyInstitution(iid, inst); ulong id = MyMoneyUtils::extractId(iid); if (id > lastId) lastId = id; d->signalProgress(++progress, 0); } return iList; } QMap MyMoneyStorageSql::fetchInstitutions() const { return fetchInstitutions(QStringList(), false); } void MyMoneyStorageSql::readPayees(const QString& id) { QList list; list.append(id); readPayees(list); } void MyMoneyStorageSql::readPayees(const QList& pid) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadPayees(fetchPayees(pid)); } catch (const MyMoneyException &) { } // if (pid.isEmpty()) m_payeeListRead = true; } void MyMoneyStorageSql::readPayees() { readPayees(QList()); } QMap MyMoneyStorageSql::fetchPayees(const QStringList& idList, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int payeesNb = (idList.isEmpty() ? d->m_payees : idList.size()); d->signalProgress(0, payeesNb, QObject::tr("Loading payees...")); } int progress = 0; QMap pList; QSqlQuery query(*const_cast (this)); QString queryString = QLatin1String("SELECT kmmPayees.id AS id, kmmPayees.name AS name, kmmPayees.reference AS reference, " " kmmPayees.email AS email, kmmPayees.addressStreet AS addressStreet, kmmPayees.addressCity AS addressCity, kmmPayees.addressZipcode AS addressZipcode, " " kmmPayees.addressState AS addressState, kmmPayees.telephone AS telephone, kmmPayees.notes AS notes, " " kmmPayees.defaultAccountId AS defaultAccountId, kmmPayees.matchData AS matchData, kmmPayees.matchIgnoreCase AS matchIgnoreCase, " " kmmPayees.matchKeys AS matchKeys, " " kmmPayeesPayeeIdentifier.identifierId AS identId " " FROM ( SELECT * FROM kmmPayees "); if (!idList.isEmpty()) { // Create WHERE clause if needed queryString += QLatin1String(" WHERE id IN ("); queryString += QString("?, ").repeated(idList.length()); queryString.chop(2); // remove ", " from end queryString += QLatin1Char(')'); } queryString += QLatin1String( " ) kmmPayees " " LEFT OUTER JOIN kmmPayeesPayeeIdentifier ON kmmPayees.Id = kmmPayeesPayeeIdentifier.payeeId " // The order is used below " ORDER BY kmmPayees.id, kmmPayeesPayeeIdentifier.userOrder;"); query.prepare(queryString); if (!idList.isEmpty()) { // Bind values QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString::fromLatin1("reading Payee"))); // krazy:exclude=crashy const QSqlRecord record = query.record(); const int idCol = record.indexOf("id"); const int nameCol = record.indexOf("name"); const int referenceCol = record.indexOf("reference"); const int emailCol = record.indexOf("email"); const int addressStreetCol = record.indexOf("addressStreet"); const int addressCityCol = record.indexOf("addressCity"); const int addressZipcodeCol = record.indexOf("addressZipcode"); const int addressStateCol = record.indexOf("addressState"); const int telephoneCol = record.indexOf("telephone"); const int notesCol = record.indexOf("notes"); const int defaultAccountIdCol = record.indexOf("defaultAccountId"); const int matchDataCol = record.indexOf("matchData"); const int matchIgnoreCaseCol = record.indexOf("matchIgnoreCase"); const int matchKeysCol = record.indexOf("matchKeys"); const int identIdCol = record.indexOf("identId"); if (query.next()) { while (query.isValid()) { QString pid; MyMoneyPayee payee; uint type; bool ignoreCase; QString matchKeys; pid = GETSTRING(idCol); payee.setName(GETSTRING(nameCol)); payee.setReference(GETSTRING(referenceCol)); payee.setEmail(GETSTRING(emailCol)); payee.setAddress(GETSTRING(addressStreetCol)); payee.setCity(GETSTRING(addressCityCol)); payee.setPostcode(GETSTRING(addressZipcodeCol)); payee.setState(GETSTRING(addressStateCol)); payee.setTelephone(GETSTRING(telephoneCol)); payee.setNotes(GETSTRING(notesCol)); payee.setDefaultAccountId(GETSTRING(defaultAccountIdCol)); type = GETINT(matchDataCol); ignoreCase = (GETSTRING(matchIgnoreCaseCol) == "Y"); matchKeys = GETSTRING(matchKeysCol); payee.setMatchData(static_cast(type), ignoreCase, matchKeys); // Get payeeIdentifier ids QStringList identifierIds; do { identifierIds.append(GETSTRING(identIdCol)); } while (query.next() && GETSTRING(idCol) == pid); // as long as the payeeId is unchanged // Fetch and save payeeIdentifier if (!identifierIds.isEmpty()) { QList< ::payeeIdentifier > identifier = fetchPayeeIdentifiers(identifierIds).values(); payee.resetPayeeIdentifiers(identifier); } if (pid == "USER") d->m_storage->setUser(payee); else pList[pid] = MyMoneyPayee(pid, payee); if (d->m_displayStatus) d->signalProgress(++progress, 0); } } return pList; } QMap MyMoneyStorageSql::fetchPayees() const { return fetchPayees(QStringList(), false); } void MyMoneyStorageSql::readTags(const QString& id) { QList list; list.append(id); readTags(list); } void MyMoneyStorageSql::readTags(const QList& pid) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadTags(fetchTags(pid)); } catch (const MyMoneyException &) { } } void MyMoneyStorageSql::readTags() { readTags(QList()); } QMap MyMoneyStorageSql::fetchOnlineJobs(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); Q_UNUSED(forUpdate); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) d->signalProgress(0, idList.isEmpty() ? d->m_onlineJobs : idList.size(), QObject::tr("Loading online banking data...")); // Create query QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare("SELECT id, type, jobSend, bankAnswerDate, state, locked FROM kmmOnlineJobs;"); } else { QString queryIdSet = QString("?, ").repeated(idList.length()); queryIdSet.chop(2); query.prepare(QLatin1String("SELECT id, type, jobSend, bankAnswerDate, state, locked FROM kmmOnlineJobs WHERE id IN (") + queryIdSet + QLatin1String(");")); QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading onlineJobs")); // krazy:exclude=crashy // Create onlineJobs int progress = 0; QMap jobList; while (query.next()) { const QString& id = query.value(0).toString(); onlineTask *const task = d->createOnlineTaskObject(query.value(1).toString(), id, *this); onlineJob job = onlineJob(task, id); job.setJobSend(query.value(2).toDateTime()); eMyMoney::OnlineJob::sendingState state; const QString stateString = query.value(4).toString(); if (stateString == "acceptedByBank") state = eMyMoney::OnlineJob::sendingState::acceptedByBank; else if (stateString == "rejectedByBank") state = eMyMoney::OnlineJob::sendingState::rejectedByBank; else if (stateString == "abortedByUser") state = eMyMoney::OnlineJob::sendingState::abortedByUser; else if (stateString == "sendingError") state = eMyMoney::OnlineJob::sendingState::sendingError; else // includes: stateString == "noBankAnswer" state = eMyMoney::OnlineJob::sendingState::noBankAnswer; job.setBankAnswer(state, query.value(4).toDateTime()); job.setLock(query.value(5).toString() == QLatin1String("Y") ? true : false); jobList.insert(job.id(), job); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return jobList; } QMap MyMoneyStorageSql::fetchOnlineJobs() const { return fetchOnlineJobs(QStringList(), false); } payeeIdentifier MyMoneyStorageSql::fetchPayeeIdentifier(const QString& id) const { QMap list = fetchPayeeIdentifiers(QStringList(id)); QMap::const_iterator iter = list.constFind(id); if (iter == list.constEnd()) throw MYMONEYEXCEPTION(QString::fromLatin1("payeeIdentifier with id '%1' not found").arg(id)); // krazy:exclude=crashy return *iter; } QMap< QString, payeeIdentifier > MyMoneyStorageSql::fetchPayeeIdentifiers(const QStringList& idList) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); // Create query QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare("SELECT id, type FROM kmmPayeeIdentifier;"); } else { QString queryIdSet = QString("?, ").repeated(idList.length()); queryIdSet.chop(2); // remove ", " from end query.prepare(QLatin1String("SELECT id, type FROM kmmPayeeIdentifier WHERE id IN (") + queryIdSet + QLatin1String(");")); QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading payee identifiers")); // krazy:exclude=crashy QMap identList; while (query.next()) { const auto id = query.value(0).toString(); identList.insert(id, d->createPayeeIdentifierObject(*this, query.value(1).toString(), id)); } return identList; } QMap< QString, payeeIdentifier > MyMoneyStorageSql::fetchPayeeIdentifiers() const { return fetchPayeeIdentifiers(QStringList()); } QMap MyMoneyStorageSql::fetchTags(const QStringList& idList, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int tagsNb = (idList.isEmpty() ? d->m_tags : idList.size()); d->signalProgress(0, tagsNb, QObject::tr("Loading tags...")); } else { // if (m_tagListRead) return; } int progress = 0; QMap taList; //ulong lastId; const MyMoneyDbTable& t = d->m_db.m_tables["kmmTags"]; QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare(t.selectAllString()); } else { QString whereClause = " where ("; QString itemConnector = ""; foreach (const QString& it, idList) { whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); itemConnector = " or "; } whereClause += ')'; query.prepare(t.selectAllString(false) + whereClause); } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Tag")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int notesCol = t.fieldNumber("notes"); int tagColorCol = t.fieldNumber("tagColor"); int closedCol = t.fieldNumber("closed"); while (query.next()) { QString pid; MyMoneyTag tag; pid = GETSTRING(idCol); tag.setName(GETSTRING(nameCol)); tag.setNotes(GETSTRING(notesCol)); tag.setClosed((GETSTRING(closedCol) == "Y")); tag.setTagColor(QColor(GETSTRING(tagColorCol))); taList[pid] = MyMoneyTag(pid, tag); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return taList; } QMap MyMoneyStorageSql::fetchTags() const { return fetchTags(QStringList(), false); } QMap MyMoneyStorageSql::fetchAccounts(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int accountsNb = (idList.isEmpty() ? d->m_accounts : idList.size()); d->signalProgress(0, accountsNb, QObject::tr("Loading accounts...")); int progress = 0; QMap accList; QStringList kvpAccountList(idList); const MyMoneyDbTable& t = d->m_db.m_tables["kmmAccounts"]; QSqlQuery query(*const_cast (this)); QSqlQuery sq(*const_cast (this)); QString childQueryString = "SELECT id, parentId FROM kmmAccounts WHERE "; QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE id IN ("; childQueryString += " parentId IN ("; QString inString; for (int i = 0; i < idList.count(); ++i) { inString += QString(":id%1, ").arg(i); } inString = inString.left(inString.length() - 2) + ')'; queryString += inString; childQueryString += inString; } else { childQueryString += " NOT parentId IS NULL"; } queryString += " ORDER BY id"; childQueryString += " ORDER BY parentid, id"; if (forUpdate) { queryString += d->m_driver->forUpdateString(); childQueryString += d->m_driver->forUpdateString(); } query.prepare(queryString); sq.prepare(childQueryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); sq.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Account")); // krazy:exclude=crashy if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading subAccountList")); // krazy:exclude=crashy // Reserve enough space for all values. Approximate it with the size of the // idList in case the db doesn't support reporting the size of the // resultset to the caller. //FIXME: this is for if/when there is a QHash conversion //accList.reserve(q.size() > 0 ? q.size() : idList.size()); static const int idCol = t.fieldNumber("id"); static const int institutionIdCol = t.fieldNumber("institutionId"); static const int parentIdCol = t.fieldNumber("parentId"); static const int lastReconciledCol = t.fieldNumber("lastReconciled"); static const int lastModifiedCol = t.fieldNumber("lastModified"); static const int openingDateCol = t.fieldNumber("openingDate"); static const int accountNumberCol = t.fieldNumber("accountNumber"); static const int accountTypeCol = t.fieldNumber("accountType"); static const int accountNameCol = t.fieldNumber("accountName"); static const int descriptionCol = t.fieldNumber("description"); static const int currencyIdCol = t.fieldNumber("currencyId"); static const int balanceCol = t.fieldNumber("balance"); static const int transactionCountCol = t.fieldNumber("transactionCount"); while (query.next()) { QString aid; MyMoneyAccount acc; aid = GETSTRING(idCol); acc.setInstitutionId(GETSTRING(institutionIdCol)); acc.setParentAccountId(GETSTRING(parentIdCol)); acc.setLastReconciliationDate(GETDATE_D(lastReconciledCol)); acc.setLastModified(GETDATE_D(lastModifiedCol)); acc.setOpeningDate(GETDATE_D(openingDateCol)); acc.setNumber(GETSTRING(accountNumberCol)); acc.setAccountType(static_cast(GETINT(accountTypeCol))); acc.setName(GETSTRING(accountNameCol)); acc.setDescription(GETSTRING(descriptionCol)); acc.setCurrencyId(GETSTRING(currencyIdCol)); acc.setBalance(MyMoneyMoney(GETSTRING(balanceCol))); const_cast (this)->d_func()->m_transactionCountMap[aid] = (ulong) GETULL(transactionCountCol); // Process any key value pair if (idList.empty()) kvpAccountList.append(aid); accList.insert(aid, MyMoneyAccount(aid, acc)); if (acc.value("PreferredAccount") == "Yes") { const_cast (this)->d_func()->m_preferred.addAccount(aid); } d->signalProgress(++progress, 0); } QMap::Iterator it_acc; QMap::Iterator accListEnd = accList.end(); while (sq.next()) { it_acc = accList.find(sq.value(1).toString()); if (it_acc != accListEnd && it_acc.value().id() == sq.value(1).toString()) { while (sq.isValid() && it_acc != accListEnd && it_acc.value().id() == sq.value(1).toString()) { it_acc.value().addAccountId(sq.value(0).toString()); if (!sq.next()) break; } sq.previous(); } } //TODO: There should be a better way than this. What's below is O(n log n) or more, // where it may be able to be done in O(n), if things are just right. // The operator[] call in the loop is the most expensive call in this function, according // to several profile runs. QHash kvpResult = d->readKeyValuePairs("ACCOUNT", kvpAccountList); QHash ::const_iterator kvp_end = kvpResult.constEnd(); for (QHash ::const_iterator it_kvp = kvpResult.constBegin(); it_kvp != kvp_end; ++it_kvp) { accList[it_kvp.key()].setPairs(it_kvp.value().pairs()); } kvpResult = d->readKeyValuePairs("ONLINEBANKING", kvpAccountList); kvp_end = kvpResult.constEnd(); for (QHash ::const_iterator it_kvp = kvpResult.constBegin(); it_kvp != kvp_end; ++it_kvp) { accList[it_kvp.key()].setOnlineBankingSettings(it_kvp.value()); } return accList; } QMap MyMoneyStorageSql::fetchAccounts() const { return fetchAccounts(QStringList(), false); } QMap MyMoneyStorageSql::fetchBalance(const QStringList& idList, const QDate& date) const { Q_D(const MyMoneyStorageSql); QMap returnValue; QSqlQuery query(*const_cast (this)); QString queryString = "SELECT action, shares, accountId, postDate " "FROM kmmSplits WHERE txType = 'N'"; if (idList.count() > 0) { queryString += "AND accountId in ("; for (int i = 0; i < idList.count(); ++i) { queryString += QString(":id%1, ").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } // SQLite stores dates as YYYY-MM-DDTHH:mm:ss with 0s for the time part. This makes // the <= operator misbehave when the date matches. To avoid this, add a day to the // requested date and use the < operator. if (date.isValid() && !date.isNull()) queryString += QString(" AND postDate < '%1'").arg(date.addDays(1).toString(Qt::ISODate)); queryString += " ORDER BY accountId, postDate;"; //DBG(queryString); query.prepare(queryString); int i = 0; foreach (const QString& bindVal, idList) { query.bindValue(QString(":id%1").arg(i), bindVal); ++i; } if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("fetching balance")); QString id; QString oldId; MyMoneyMoney temp; while (query.next()) { id = query.value(2).toString(); // If the old ID does not match the new ID, then the account being summed has changed. // Write the balance into the returnValue map and update the oldId to the current one. if (id != oldId) { if (!oldId.isEmpty()) { returnValue.insert(oldId, temp); temp = 0; } oldId = id; } if (MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares) == query.value(0).toString()) temp *= MyMoneyMoney(query.value(1).toString()); else temp += MyMoneyMoney(query.value(1).toString()); } // Do not forget the last id in the list. returnValue.insert(id, temp); // Return the map. return returnValue; } void MyMoneyStorageSql::readTransactions(const MyMoneyTransactionFilter& filter) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadTransactions(fetchTransactions(filter)); } catch (const MyMoneyException &) { throw; } } QMap MyMoneyStorageSql::fetchTransactions(const QString& tidList, const QString& dateClause, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); // if (m_transactionListRead) return; // all list already in memory if (d->m_displayStatus) { int transactionsNb = (tidList.isEmpty() ? d->m_transactions : tidList.size()); d->signalProgress(0, transactionsNb, QObject::tr("Loading transactions...")); } int progress = 0; // m_payeeList.clear(); QString whereClause = " WHERE txType = 'N' "; if (! tidList.isEmpty()) { whereClause += " AND id IN " + tidList; } if (!dateClause.isEmpty()) whereClause += " and " + dateClause; const MyMoneyDbTable& t = d->m_db.m_tables["kmmTransactions"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(false) + whereClause + " ORDER BY id;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Transaction")); // krazy:exclude=crashy const MyMoneyDbTable& ts = d->m_db.m_tables["kmmSplits"]; whereClause = " WHERE txType = 'N' "; if (! tidList.isEmpty()) { whereClause += " AND transactionId IN " + tidList; } if (!dateClause.isEmpty()) whereClause += " and " + dateClause; QSqlQuery qs(*const_cast (this)); QString splitQuery = ts.selectAllString(false) + whereClause + " ORDER BY transactionId, splitId;"; qs.prepare(splitQuery); if (!qs.exec()) throw MYMONEYEXCEPTION(d->buildError(qs, Q_FUNC_INFO, "reading Splits")); // krazy:exclude=crashy QString splitTxId = "ZZZ"; MyMoneySplit s; if (qs.next()) { splitTxId = qs.value(0).toString(); s = d->readSplit(qs); } else { splitTxId = "ZZZ"; } QMap txMap; QStringList txList; int idCol = t.fieldNumber("id"); int postDateCol = t.fieldNumber("postDate"); int memoCol = t.fieldNumber("memo"); int entryDateCol = t.fieldNumber("entryDate"); int currencyIdCol = t.fieldNumber("currencyId"); int bankIdCol = t.fieldNumber("bankId"); while (query.next()) { MyMoneyTransaction tx; QString txId = GETSTRING(idCol); tx.setPostDate(GETDATE_D(postDateCol)); tx.setMemo(GETSTRING(memoCol)); tx.setEntryDate(GETDATE_D(entryDateCol)); tx.setCommodity(GETSTRING(currencyIdCol)); tx.setBankID(GETSTRING(bankIdCol)); // skip all splits while the transaction id of the split is less than // the transaction id of the current transaction. Don't forget to check // for the ZZZ flag for the end of the list. while (txId < splitTxId && splitTxId != "ZZZ") { if (qs.next()) { splitTxId = qs.value(0).toString(); s = d->readSplit(qs); } else { splitTxId = "ZZZ"; } } // while the split transaction id matches the current transaction id, // add the split to the current transaction. Set the ZZZ flag if // all splits for this transaction have been read. while (txId == splitTxId) { tx.addSplit(s); if (qs.next()) { splitTxId = qs.value(0).toString(); s = d->readSplit(qs); } else { splitTxId = "ZZZ"; } } // Process any key value pair if (! txId.isEmpty()) { txList.append(txId); tx = MyMoneyTransaction(txId, tx); txMap.insert(tx.uniqueSortKey(), tx); } } // get the kvps QHash kvpMap = d->readKeyValuePairs("TRANSACTION", txList); QMap::Iterator txMapEnd = txMap.end(); for (QMap::Iterator i = txMap.begin(); i != txMapEnd; ++i) { i.value().setPairs(kvpMap[i.value().id()].pairs()); if (d->m_displayStatus) d->signalProgress(++progress, 0); } if ((tidList.isEmpty()) && (dateClause.isEmpty())) { //qDebug("setting full list read"); } return txMap; } QMap MyMoneyStorageSql::fetchTransactions(const QString& tidList) const { return fetchTransactions(tidList, QString(), false); } QMap MyMoneyStorageSql::fetchTransactions() const { return fetchTransactions(QString(), QString(), false); } QMap MyMoneyStorageSql::fetchTransactions(const MyMoneyTransactionFilter& filter) const { Q_D(const MyMoneyStorageSql); // analyze the filter // if (m_transactionListRead) return; // all list already in memory // if the filter is restricted to certain accounts/categories // check if we already have them all in memory QStringList accounts; QString inQuery; filter.accounts(accounts); filter.categories(accounts); // QStringList::iterator it; // bool allAccountsLoaded = true; // for (it = accounts.begin(); it != accounts.end(); ++it) { // if (m_accountsLoaded.find(*it) == m_accountsLoaded.end()) { // allAccountsLoaded = false; // break; // } // } // if (allAccountsLoaded) return; /* Some filter combinations do not lend themselves to implementation * in SQL, or are likely to require such extensive reading of the database * as to make it easier to just read everything into memory. */ bool canImplementFilter = true; MyMoneyMoney m1, m2; if (filter.amountFilter(m1, m2)) { d->alert("Amount Filter Set"); canImplementFilter = false; } QString n1, n2; if (filter.numberFilter(n1, n2)) { d->alert("Number filter set"); canImplementFilter = false; } int t1; if (filter.firstType(t1)) { d->alert("Type filter set"); canImplementFilter = false; } // int s1; // if (filter.firstState(s1)) { // alert("State filter set"); // canImplementFilter = false; // } QRegExp t2; if (filter.textFilter(t2)) { d->alert("text filter set"); canImplementFilter = false; } MyMoneyTransactionFilter::FilterSet s = filter.filterSet(); if (s.singleFilter.validityFilter) { d->alert("Validity filter set"); canImplementFilter = false; } if (!canImplementFilter) { QMap transactionList = fetchTransactions(); std::remove_if(transactionList.begin(), transactionList.end(), FilterFail(filter)); return transactionList; } bool splitFilterActive = false; // the split filter is active if we are selecting on fields in the split table // get start and end dates QDate start = filter.fromDate(); QDate end = filter.toDate(); // not entirely sure if the following is correct, but at best, saves a lot of reads, at worst // it only causes us to read a few more transactions that strictly necessary (I think...) if (start == MyMoneyStorageSqlPrivate::m_startDate) start = QDate(); bool txFilterActive = ((start != QDate()) || (end != QDate())); // and this for fields in the transaction table QString whereClause = ""; QString subClauseconnector = " where txType = 'N' and "; // payees QStringList payees; if (filter.payees(payees)) { QString itemConnector = "payeeId in ("; QString payeesClause = ""; foreach (const QString& it, payees) { payeesClause.append(QString("%1'%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!payeesClause.isEmpty()) { whereClause += subClauseconnector + payeesClause + ')'; subClauseconnector = " and "; } splitFilterActive = true; } //tags QStringList tags; if (filter.tags(tags)) { QString itemConnector = "splitId in ( SELECT splitId from kmmTagSplits where kmmTagSplits.transactionId = kmmSplits.transactionId and tagId in ("; QString tagsClause = ""; foreach (const QString& it, tags) { tagsClause.append(QString("%1'%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!tagsClause.isEmpty()) { whereClause += subClauseconnector + tagsClause + ')'; subClauseconnector = " and "; } splitFilterActive = true; } // accounts and categories if (!accounts.isEmpty()) { splitFilterActive = true; QString itemConnector = "accountId in ("; QString accountsClause = ""; foreach (const QString& it, accounts) { accountsClause.append(QString("%1 '%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!accountsClause.isEmpty()) { whereClause += subClauseconnector + accountsClause + ')'; subClauseconnector = " and ("; } } // split states QList splitStates; if (filter.states(splitStates)) { splitFilterActive = true; QString itemConnector = " reconcileFlag IN ("; QString statesClause = ""; foreach (int it, splitStates) { statesClause.append(QString(" %1 '%2'").arg(itemConnector) .arg(d->splitState(TransactionFilter::State(it)))); itemConnector = ','; } if (!statesClause.isEmpty()) { whereClause += subClauseconnector + statesClause + ')'; subClauseconnector = " and ("; } } // I've given up trying to work out the logic. we keep getting the wrong number of close brackets int obc = whereClause.count('('); int cbc = whereClause.count(')'); if (cbc > obc) { qDebug() << "invalid where clause " << whereClause; qFatal("aborting"); } while (cbc < obc) { whereClause.append(')'); cbc++; } // if the split filter is active, but the where clause and the date filter is empty // it means we already have all the transactions for the specified filter // in memory, so just exit if ((splitFilterActive) && (whereClause.isEmpty()) && (!txFilterActive)) { qDebug("all transactions already in storage"); return fetchTransactions(); } // if we have neither a split filter, nor a tx (date) filter // it's effectively a read all if ((!splitFilterActive) && (!txFilterActive)) { //qDebug("reading all transactions"); return fetchTransactions(); } // build a date clause for the transaction table QString dateClause; QString connector = ""; if (end != QDate()) { dateClause = QString("(postDate < '%1')").arg(end.addDays(1).toString(Qt::ISODate)); connector = " and "; } if (start != QDate()) { dateClause += QString("%1 (postDate >= '%2')").arg(connector).arg(start.toString(Qt::ISODate)); } // now get a list of transaction ids // if we have only a date filter, we need to build the list from the tx table // otherwise we need to build from the split table if (splitFilterActive) { inQuery = QString("(select distinct transactionId from kmmSplits %1)").arg(whereClause); } else { inQuery = QString("(select distinct id from kmmTransactions where %1)").arg(dateClause); txFilterActive = false; // kill off the date filter now } return fetchTransactions(inQuery, dateClause); //FIXME: if we have an accounts-only filter, recalc balances on loaded accounts } ulong MyMoneyStorageSql::transactionCount(const QString& aid) const { Q_D(const MyMoneyStorageSql); if (aid.isEmpty()) return d->m_transactions; else return d->m_transactionCountMap[aid]; } QHash MyMoneyStorageSql::transactionCountMap() const { Q_D(const MyMoneyStorageSql); return d->m_transactionCountMap; } bool MyMoneyStorageSql::isReferencedByTransaction(const QString& id) const { Q_D(const MyMoneyStorageSql); //FIXME-ALEX should I add sub query for kmmTagSplits here? QSqlQuery q(*const_cast (this)); q.prepare("SELECT COUNT(*) FROM kmmTransactions " "INNER JOIN kmmSplits ON kmmTransactions.id = kmmSplits.transactionId " "WHERE kmmTransactions.currencyId = :ID OR kmmSplits.payeeId = :ID " "OR kmmSplits.accountId = :ID OR kmmSplits.costCenterId = :ID"); q.bindValue(":ID", id); if ((!q.exec()) || (!q.next())) { // krazy:exclude=crashy d->buildError(q, Q_FUNC_INFO, "error retrieving reference count"); qFatal("Error retrieving reference count"); // definitely shouldn't happen } return (0 != q.value(0).toULongLong()); } QMap MyMoneyStorageSql::fetchSchedules(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int schedulesNb = (idList.isEmpty() ? d->m_schedules : idList.size()); d->signalProgress(0, schedulesNb, QObject::tr("Loading schedules...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmSchedules"]; QSqlQuery query(*const_cast (this)); QMap sList; //ulong lastId = 0; const MyMoneyDbTable& ts = d->m_db.m_tables["kmmSplits"]; QSqlQuery qs(*const_cast (this)); qs.prepare(ts.selectAllString(false) + " WHERE transactionId = :id ORDER BY splitId;"); QSqlQuery sq(*const_cast (this)); sq.prepare("SELECT payDate from kmmSchedulePaymentHistory where schedId = :id"); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" id = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } queryString += " ORDER BY id"; if (forUpdate) queryString += d->m_driver->forUpdateString(); query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Schedules")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int typeCol = t.fieldNumber("type"); int occurrenceCol = t.fieldNumber("occurence"); // krazy:exclude=spelling int occurrenceMultiplierCol = t.fieldNumber("occurenceMultiplier"); // krazy:exclude=spelling int paymentTypeCol = t.fieldNumber("paymentType"); int startDateCol = t.fieldNumber("startDate"); int endDateCol = t.fieldNumber("endDate"); int fixedCol = t.fieldNumber("fixed"); int lastDayInMonthCol = t.fieldNumber("lastDayInMonth"); int autoEnterCol = t.fieldNumber("autoEnter"); int lastPaymentCol = t.fieldNumber("lastPayment"); int weekendOptionCol = t.fieldNumber("weekendOption"); int nextPaymentDueCol = t.fieldNumber("nextPaymentDue"); while (query.next()) { MyMoneySchedule s; QString boolChar; QString sId = GETSTRING(idCol); s.setName(GETSTRING(nameCol)); s.setType(static_cast(GETINT(typeCol))); s.setOccurrencePeriod(static_cast(GETINT(occurrenceCol))); s.setOccurrenceMultiplier(GETINT(occurrenceMultiplierCol)); s.setPaymentType(static_cast(GETINT(paymentTypeCol))); s.setStartDate(GETDATE_D(startDateCol)); s.setEndDate(GETDATE_D(endDateCol)); boolChar = GETSTRING(fixedCol); s.setFixed(boolChar == "Y"); boolChar = GETSTRING(lastDayInMonthCol); s.setLastDayInMonth(boolChar == "Y"); boolChar = GETSTRING(autoEnterCol); s.setAutoEnter(boolChar == "Y"); s.setLastPayment(GETDATE_D(lastPaymentCol)); s.setWeekendOption(static_cast(GETINT(weekendOptionCol))); QDate nextPaymentDue = GETDATE_D(nextPaymentDueCol); // convert simple occurrence to compound occurrence int mult = s.occurrenceMultiplier(); - Schedule::Occurrence occ = s.occurrencePeriod(); + Schedule::Occurrence occ = s.occurrence(); MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); s.setOccurrencePeriod(occ); s.setOccurrenceMultiplier(mult); // now assign the id to the schedule MyMoneySchedule _s(sId, s); s = _s; // read the associated transaction // m_payeeList.clear(); const MyMoneyDbTable& transactionTable = d->m_db.m_tables["kmmTransactions"]; QSqlQuery q(*const_cast (this)); q.prepare(transactionTable.selectAllString(false) + " WHERE id = :id;"); q.bindValue(":id", s.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("reading Scheduled Transaction"))); // krazy:exclude=crashy QSqlRecord rec = q.record(); if (!q.next()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("retrieving scheduled transaction"))); MyMoneyTransaction tx(s.id(), MyMoneyTransaction()); // we cannot use the GET.... macros here as they are bound to the query variable tx.setPostDate(d->getDate(q.value(transactionTable.fieldNumber("postDate")).toString())); tx.setMemo(q.value(transactionTable.fieldNumber("memo")).toString()); tx.setEntryDate(d->getDate(q.value(transactionTable.fieldNumber("entryDate")).toString())); tx.setCommodity(q.value(transactionTable.fieldNumber("currencyId")).toString()); tx.setBankID(q.value(transactionTable.fieldNumber("bankId")).toString()); qs.bindValue(":id", s.id()); if (!qs.exec()) throw MYMONEYEXCEPTION(d->buildError(qs, Q_FUNC_INFO, "reading Scheduled Splits")); // krazy:exclude=crashy while (qs.next()) { MyMoneySplit sp(d->readSplit(qs)); tx.addSplit(sp); } // if (!m_payeeList.isEmpty()) // readPayees(m_payeeList); // Process any key value pair tx.setPairs(d->readKeyValuePairs("TRANSACTION", s.id()).pairs()); // If the transaction doesn't have a post date, setTransaction will reject it. // The old way of handling things was to store the next post date in the schedule object // and set the transaction post date to QDate(). // For compatibility, if this is the case, copy the next post date from the schedule object // to the transaction object post date. if (!tx.postDate().isValid()) { tx.setPostDate(nextPaymentDue); } s.setTransaction(tx); // read in the recorded payments sq.bindValue(":id", s.id()); if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading schedule payment history")); // krazy:exclude=crashy while (sq.next()) s.recordPayment(sq.value(0).toDate()); sList[s.id()] = s; //FIXME: enable when schedules have KVPs. // s.setPairs(readKeyValuePairs("SCHEDULE", s.id()).pairs()); //ulong id = MyMoneyUtils::extractId(s.id().data()); //if(id > lastId) // lastId = id; d->signalProgress(++progress, 0); } return sList; } QMap MyMoneyStorageSql::fetchSchedules() const { return fetchSchedules(QStringList(), false); } QMap MyMoneyStorageSql::fetchSecurities(const QStringList& /*idList*/, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); d->signalProgress(0, d->m_securities, QObject::tr("Loading securities...")); int progress = 0; QMap sList; ulong lastId = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmSecurities"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(false) + " ORDER BY id;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Securities")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int symbolCol = t.fieldNumber("symbol"); int typeCol = t.fieldNumber("type"); int roundingMethodCol = t.fieldNumber("roundingMethod"); int smallestAccountFractionCol = t.fieldNumber("smallestAccountFraction"); int pricePrecisionCol = t.fieldNumber("pricePrecision"); int tradingCurrencyCol = t.fieldNumber("tradingCurrency"); int tradingMarketCol = t.fieldNumber("tradingMarket"); while (query.next()) { MyMoneySecurity e; QString eid; eid = GETSTRING(idCol); e.setName(GETSTRING(nameCol)); e.setTradingSymbol(GETSTRING(symbolCol)); e.setSecurityType(static_cast(GETINT(typeCol))); e.setRoundingMethod(static_cast(GETINT(roundingMethodCol))); int saf = GETINT(smallestAccountFractionCol); int pp = GETINT(pricePrecisionCol); e.setTradingCurrency(GETSTRING(tradingCurrencyCol)); e.setTradingMarket(GETSTRING(tradingMarketCol)); if (e.tradingCurrency().isEmpty()) e.setTradingCurrency(d->m_storage->pairs()["kmm-baseCurrency"]); if (saf == 0) saf = 100; if (pp == 0 || pp > 10) pp = 4; e.setSmallestAccountFraction(saf); e.setPricePrecision(pp); // Process any key value pairs e.setPairs(d->readKeyValuePairs("SECURITY", eid).pairs()); //tell the storage objects we have a new security object. // FIXME: Adapt to new interface make sure, to take care of the currencies as well // see MyMoneyStorageXML::readSecurites() MyMoneySecurity security(eid, e); sList[security.id()] = security; ulong id = MyMoneyUtils::extractId(security.id()); if (id > lastId) lastId = id; d->signalProgress(++progress, 0); } return sList; } QMap MyMoneyStorageSql::fetchSecurities() const { return fetchSecurities(QStringList(), false); } MyMoneyPrice MyMoneyStorageSql::fetchSinglePrice(const QString& fromId, const QString& toId, const QDate& date_, bool exactDate, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); const MyMoneyDbTable& t = d->m_db.m_tables["kmmPrices"]; static const int priceDateCol = t.fieldNumber("priceDate"); static const int priceCol = t.fieldNumber("price"); static const int priceSourceCol = t.fieldNumber("priceSource"); QSqlQuery query(*const_cast (this)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. // See balance query for why the date logic seems odd. QString queryString = t.selectAllString(false) + " WHERE fromId = :fromId AND toId = :toId AND priceDate < :priceDate "; if (exactDate) queryString += "AND priceDate > :exactDate "; queryString += "ORDER BY priceDate DESC;"; query.prepare(queryString); QDate date(date_); if (!date.isValid()) date = QDate::currentDate(); query.bindValue(":fromId", fromId); query.bindValue(":toId", toId); query.bindValue(":priceDate", date.addDays(1).toString(Qt::ISODate)); if (exactDate) query.bindValue(":exactDate", date.toString(Qt::ISODate)); if (! query.exec()) return MyMoneyPrice(); // krazy:exclude=crashy if (query.next()) { return MyMoneyPrice(fromId, toId, GETDATE_D(priceDateCol), MyMoneyMoney(GETSTRING(priceCol)), GETSTRING(priceSourceCol)); } return MyMoneyPrice(); } MyMoneyPriceList MyMoneyStorageSql::fetchPrices(const QStringList& fromIdList, const QStringList& toIdList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int pricesNb = (fromIdList.isEmpty() ? d->m_prices : fromIdList.size()); d->signalProgress(0, pricesNb, QObject::tr("Loading prices...")); int progress = 0; const_cast (this)->d_func()->m_readingPrices = true; MyMoneyPriceList pList; const MyMoneyDbTable& t = d->m_db.m_tables["kmmPrices"]; QSqlQuery query(*const_cast (this)); QString queryString = t.selectAllString(false); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! fromIdList.empty()) { queryString += " WHERE ("; for (int i = 0; i < fromIdList.count(); ++i) { queryString += QString(" fromId = :fromId%1 OR").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } if (! toIdList.empty()) { queryString += " AND ("; for (int i = 0; i < toIdList.count(); ++i) { queryString += QString(" toId = :toId%1 OR").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! fromIdList.empty()) { QStringList::ConstIterator bindVal = fromIdList.constBegin(); for (int i = 0; bindVal != fromIdList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":fromId%1").arg(i), *bindVal); } } if (! toIdList.empty()) { QStringList::ConstIterator bindVal = toIdList.constBegin(); for (int i = 0; bindVal != toIdList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":toId%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Prices")); // krazy:exclude=crashy static const int fromIdCol = t.fieldNumber("fromId"); static const int toIdCol = t.fieldNumber("toId"); static const int priceDateCol = t.fieldNumber("priceDate"); static const int priceCol = t.fieldNumber("price"); static const int priceSourceCol = t.fieldNumber("priceSource"); while (query.next()) { QString from = GETSTRING(fromIdCol); QString to = GETSTRING(toIdCol); QDate date = GETDATE_D(priceDateCol); pList [MyMoneySecurityPair(from, to)].insert(date, MyMoneyPrice(from, to, date, MyMoneyMoney(GETSTRING(priceCol)), GETSTRING(priceSourceCol))); d->signalProgress(++progress, 0); } const_cast (this)->d_func()->m_readingPrices = false; return pList; } MyMoneyPriceList MyMoneyStorageSql::fetchPrices() const { return fetchPrices(QStringList(), QStringList(), false); } QMap MyMoneyStorageSql::fetchCurrencies(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int currenciesNb = (idList.isEmpty() ? d->m_currencies : idList.size()); d->signalProgress(0, currenciesNb, QObject::tr("Loading currencies...")); int progress = 0; QMap cList; const MyMoneyDbTable& t = d->m_db.m_tables["kmmCurrencies"]; QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" isocode = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } queryString += " ORDER BY ISOcode"; if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Currencies")); // krazy:exclude=crashy int ISOcodeCol = t.fieldNumber("ISOcode"); int nameCol = t.fieldNumber("name"); int typeCol = t.fieldNumber("type"); int symbol1Col = t.fieldNumber("symbol1"); int symbol2Col = t.fieldNumber("symbol2"); int symbol3Col = t.fieldNumber("symbol3"); int smallestCashFractionCol = t.fieldNumber("smallestCashFraction"); int smallestAccountFractionCol = t.fieldNumber("smallestAccountFraction"); int pricePrecisionCol = t.fieldNumber("pricePrecision"); while (query.next()) { QString id; MyMoneySecurity c; QChar symbol[3]; id = GETSTRING(ISOcodeCol); c.setName(GETSTRING(nameCol)); c.setSecurityType(static_cast(GETINT(typeCol))); symbol[0] = QChar(GETINT(symbol1Col)); symbol[1] = QChar(GETINT(symbol2Col)); symbol[2] = QChar(GETINT(symbol3Col)); c.setSmallestCashFraction(GETINT(smallestCashFractionCol)); c.setSmallestAccountFraction(GETINT(smallestAccountFractionCol)); c.setPricePrecision(GETINT(pricePrecisionCol)); c.setTradingSymbol(QString(symbol, 3).trimmed()); cList[id] = MyMoneySecurity(id, c); d->signalProgress(++progress, 0); } return cList; } QMap MyMoneyStorageSql::fetchCurrencies() const { return fetchCurrencies(QStringList(), false); } QMap MyMoneyStorageSql::fetchReports(const QStringList& /*idList*/, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); d->signalProgress(0, d->m_reports, QObject::tr("Loading reports...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmReportConfig"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(true)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading reports")); // krazy:exclude=crashy int xmlCol = t.fieldNumber("XML"); QMap rList; while (query.next()) { QDomDocument dom; dom.setContent(GETSTRING(xmlCol), false); QDomNode child = dom.firstChild(); child = child.firstChild(); auto report = MyMoneyXmlContentHandler2::readReport(child.toElement()); rList[report.id()] = report; d->signalProgress(++progress, 0); } return rList; } QMap MyMoneyStorageSql::fetchReports() const { return fetchReports(QStringList(), false); } QMap MyMoneyStorageSql::fetchBudgets(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int budgetsNb = (idList.isEmpty() ? d->m_budgets : idList.size()); d->signalProgress(0, budgetsNb, QObject::tr("Loading budgets...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmBudgetConfig"]; QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); if (! idList.empty()) { queryString += " WHERE id = '" + idList.join("' OR id = '") + '\''; } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading budgets")); // krazy:exclude=crashy QMap budgets; int xmlCol = t.fieldNumber("XML"); while (query.next()) { QDomDocument dom; dom.setContent(GETSTRING(xmlCol), false); QDomNode child = dom.firstChild(); child = child.firstChild(); auto budget = MyMoneyXmlContentHandler2::readBudget(child.toElement()); budgets.insert(budget.id(), budget); d->signalProgress(++progress, 0); } return budgets; } QMap MyMoneyStorageSql::fetchBudgets() const { return fetchBudgets(QStringList(), false); } ulong MyMoneyStorageSql::getNextBudgetId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdBudgets>(QLatin1String("kmmBudgetConfig"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextAccountId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdAccounts>(QLatin1String("kmmAccounts"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextInstitutionId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdInstitutions>(QLatin1String("kmmInstitutions"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextPayeeId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdPayees>(QLatin1String("kmmPayees"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextTagId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdTags>(QLatin1String("kmmTags"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextReportId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdReports>(QLatin1String("kmmReportConfig"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextScheduleId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdSchedules>(QLatin1String("kmmSchedules"), QLatin1String("id"), 3); } ulong MyMoneyStorageSql::getNextSecurityId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdSecurities>(QLatin1String("kmmSecurities"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextTransactionId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdTransactions>(QLatin1String("kmmTransactions"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextOnlineJobId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdOnlineJobs>(QLatin1String("kmmOnlineJobs"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextPayeeIdentifierId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdPayeeIdentifier>(QLatin1String("kmmPayeeIdentifier"), QLatin1String("id"), 5); } ulong MyMoneyStorageSql::getNextCostCenterId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdCostCenter>(QLatin1String("kmmCostCenterIdentifier"), QLatin1String("id"), 5); } ulong MyMoneyStorageSql::incrementBudgetId() { Q_D(MyMoneyStorageSql); d->m_hiIdBudgets = getNextBudgetId() + 1; return (d->m_hiIdBudgets - 1); } /** * @warning This method uses getNextAccountId() internaly. The database is not informed which can cause issues * when the database is accessed concurrently. Then maybe a single id is used twice but the RDBMS will detect the * issue and KMyMoney crashes. This issue can only occour when two instances of KMyMoney access the same database. * But in this unlikley case MyMoneyStorageSql will have a lot more issues, I think. */ ulong MyMoneyStorageSql::incrementAccountId() { Q_D(MyMoneyStorageSql); d->m_hiIdAccounts = getNextAccountId() + 1; return (d->m_hiIdAccounts - 1); } ulong MyMoneyStorageSql::incrementInstitutionId() { Q_D(MyMoneyStorageSql); d->m_hiIdInstitutions = getNextInstitutionId() + 1; return (d->m_hiIdInstitutions - 1); } ulong MyMoneyStorageSql::incrementPayeeId() { Q_D(MyMoneyStorageSql); d->m_hiIdPayees = getNextPayeeId() + 1; return (d->m_hiIdPayees - 1); } ulong MyMoneyStorageSql::incrementTagId() { Q_D(MyMoneyStorageSql); d->m_hiIdTags = getNextTagId() + 1; return (d->m_hiIdTags - 1); } ulong MyMoneyStorageSql::incrementReportId() { Q_D(MyMoneyStorageSql); d->m_hiIdReports = getNextReportId() + 1; return (d->m_hiIdReports - 1); } ulong MyMoneyStorageSql::incrementScheduleId() { Q_D(MyMoneyStorageSql); d->m_hiIdSchedules = getNextScheduleId() + 1; return (d->m_hiIdSchedules - 1); } ulong MyMoneyStorageSql::incrementSecurityId() { Q_D(MyMoneyStorageSql); d->m_hiIdSecurities = getNextSecurityId() + 1; return (d->m_hiIdSecurities - 1); } ulong MyMoneyStorageSql::incrementTransactionId() { Q_D(MyMoneyStorageSql); d->m_hiIdTransactions = getNextTransactionId() + 1; return (d->m_hiIdTransactions - 1); } ulong MyMoneyStorageSql::incrementOnlineJobId() { Q_D(MyMoneyStorageSql); d->m_hiIdOnlineJobs = getNextOnlineJobId() + 1; return (d->m_hiIdOnlineJobs - 1); } ulong MyMoneyStorageSql::incrementPayeeIdentfierId() { Q_D(MyMoneyStorageSql); d->m_hiIdPayeeIdentifier = getNextPayeeIdentifierId() + 1; return (d->m_hiIdPayeeIdentifier - 1); } ulong MyMoneyStorageSql::incrementCostCenterId() { Q_D(MyMoneyStorageSql); d->m_hiIdCostCenter = getNextCostCenterId() + 1; return (d->m_hiIdCostCenter - 1); } void MyMoneyStorageSql::loadAccountId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdAccounts = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadTransactionId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdTransactions = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadPayeeId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdPayees = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadTagId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdTags = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadInstitutionId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdInstitutions = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadScheduleId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdSchedules = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadSecurityId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdSecurities = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadReportId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdReports = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadBudgetId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdBudgets = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadOnlineJobId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdOnlineJobs = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadPayeeIdentifierId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdPayeeIdentifier = id; d->writeFileInfo(); } //**************************************************** void MyMoneyStorageSql::setProgressCallback(void(*callback)(int, int, const QString&)) { Q_D(MyMoneyStorageSql); d->m_progressCallback = callback; } void MyMoneyStorageSql::readFile(QIODevice* s, MyMoneyStorageMgr* storage) { Q_UNUSED(s); Q_UNUSED(storage) } void MyMoneyStorageSql::writeFile(QIODevice* s, MyMoneyStorageMgr* storage) { Q_UNUSED(s); Q_UNUSED(storage) } // **************************** Error display routine ******************************* QDate MyMoneyStorageSqlPrivate::m_startDate = QDate(1900, 1, 1); void MyMoneyStorageSql::setStartDate(const QDate& startDate) { MyMoneyStorageSqlPrivate::m_startDate = startDate; } QMap< QString, MyMoneyCostCenter > MyMoneyStorageSql::fetchCostCenters(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); Q_UNUSED(forUpdate); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int costCenterNb = (idList.isEmpty() ? 100 : idList.size()); d->signalProgress(0, costCenterNb, QObject::tr("Loading cost center...")); } int progress = 0; QMap costCenterList; //ulong lastId; const MyMoneyDbTable& t = d->m_db.m_tables["kmmCostCenter"]; QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare(t.selectAllString()); } else { QString whereClause = " where ("; QString itemConnector = ""; foreach (const QString& it, idList) { whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); itemConnector = " or "; } whereClause += ')'; query.prepare(t.selectAllString(false) + whereClause); } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading CostCenter")); // krazy:exclude=crashy const int idCol = t.fieldNumber("id"); const int nameCol = t.fieldNumber("name"); while (query.next()) { MyMoneyCostCenter costCenter; QString pid = GETSTRING(idCol); costCenter.setName(GETSTRING(nameCol)); costCenterList[pid] = MyMoneyCostCenter(pid, costCenter); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return costCenterList; } QMap< QString, MyMoneyCostCenter > MyMoneyStorageSql::fetchCostCenters() const { return fetchCostCenters(QStringList(), false); } diff --git a/kmymoney/plugins/sql/mymoneystoragesql_p.h b/kmymoney/plugins/sql/mymoneystoragesql_p.h index 09e27ca62..563bad624 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql_p.h +++ b/kmymoney/plugins/sql/mymoneystoragesql_p.h @@ -1,3316 +1,3316 @@ /*************************************************************************** mymoneystoragesql.cpp --------------------- begin : 11 November 2005 copyright : (C) 2005 by Tony Bloomfield email : tonybloom@users.sourceforge.net : Fernando Vilas : Christian Dávid (C) 2017 by Ł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. * * * ***************************************************************************/ #ifndef MYMONEYSTORAGESQL_P_H #define MYMONEYSTORAGESQL_P_H #include "mymoneystoragesql.h" // ---------------------------------------------------------------------------- // System Includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "onlinejobadministration.h" #include "onlinetasks/interfaces/tasks/onlinetask.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "mymoneyprice.h" #include "mymoneyutils.h" #include "mymoneydbdef.h" #include "mymoneydbdriver.h" #include "payeeidentifierdata.h" #include "payeeidentifier.h" #include "payeeidentifiertyped.h" #include "payeeidentifier/ibanbic/ibanbic.h" #include "payeeidentifier/nationalaccount/nationalaccount.h" #include "onlinetasks/sepa/sepaonlinetransferimpl.h" #include "xmlstoragehelper.h" #include "mymoneyenums.h" using namespace eMyMoney; class FilterFail { public: explicit FilterFail(const MyMoneyTransactionFilter& filter) : m_filter(filter) {} inline bool operator()(const QPair& transactionPair) { return (*this)(transactionPair.second); } inline bool operator()(const MyMoneyTransaction& transaction) { return !m_filter.match(transaction); } private: MyMoneyTransactionFilter m_filter; }; //***************************************************************************** // Create a class to handle db transactions using scope // // Don't let the database object get destroyed while this object exists, // that would result in undefined behavior. class MyMoneyDbTransaction { public: explicit MyMoneyDbTransaction(MyMoneyStorageSql& db, const QString& name) : m_db(db), m_name(name) { db.startCommitUnit(name); } ~MyMoneyDbTransaction() { if (std::uncaught_exception()) { m_db.cancelCommitUnit(m_name); } else { try{ m_db.endCommitUnit(m_name); } catch (const MyMoneyException &) { try { m_db.cancelCommitUnit(m_name); } catch (const MyMoneyException &e) { qDebug() << e.what(); } } } } private: MyMoneyStorageSql& m_db; QString m_name; }; /** * The MyMoneySqlQuery class is derived from QSqlQuery to provide * a way to adjust some queries based on database type and make * debugging easier by providing a place to put debug statements. */ class MyMoneySqlQuery : public QSqlQuery { public: explicit MyMoneySqlQuery(MyMoneyStorageSql* db = 0) : QSqlQuery(*db) { } virtual ~MyMoneySqlQuery() { } bool exec() { qDebug() << "start sql:" << lastQuery(); bool rc = QSqlQuery::exec(); qDebug() << "end sql:" << QSqlQuery::executedQuery(); qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected(); return (rc); } bool exec(const QString & query) { qDebug() << "start sql:" << query; bool rc = QSqlQuery::exec(query); qDebug() << "end sql:" << QSqlQuery::executedQuery(); qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected(); return rc; } bool prepare(const QString & query) { return (QSqlQuery::prepare(query)); } }; #define GETSTRING(a) query.value(a).toString() #define GETDATE(a) getDate(GETSTRING(a)) #define GETDATE_D(a) d->getDate(GETSTRING(a)) #define GETDATETIME(a) getDateTime(GETSTRING(a)) #define GETINT(a) query.value(a).toInt() #define GETULL(a) query.value(a).toULongLong() #define MYMONEYEXCEPTIONSQL(exceptionMessage) MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, exceptionMessage)) #define MYMONEYEXCEPTIONSQL_D(exceptionMessage) MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, exceptionMessage)) class MyMoneyStorageSqlPrivate { Q_DISABLE_COPY(MyMoneyStorageSqlPrivate) Q_DECLARE_PUBLIC(MyMoneyStorageSql) public: explicit MyMoneyStorageSqlPrivate(MyMoneyStorageSql* qq) : q_ptr(qq), m_dbVersion(0), m_storage(nullptr), m_loadAll(false), m_override(false), m_institutions(0), m_accounts(0), m_payees(0), m_tags(0), m_transactions(0), m_splits(0), m_securities(0), m_prices(0), m_currencies(0), m_schedules(0), m_reports(0), m_kvps(0), m_budgets(0), m_onlineJobs(0), m_payeeIdentifier(0), m_hiIdInstitutions(0), m_hiIdPayees(0), m_hiIdTags(0), m_hiIdAccounts(0), m_hiIdTransactions(0), m_hiIdSchedules(0), m_hiIdSecurities(0), m_hiIdReports(0), m_hiIdBudgets(0), m_hiIdOnlineJobs(0), m_hiIdPayeeIdentifier(0), m_hiIdCostCenter(0), m_displayStatus(false), m_readingPrices(false), m_newDatabase(false), m_progressCallback(nullptr) { m_preferred.setReportAllSplits(false); } ~MyMoneyStorageSqlPrivate() { } enum class SQLAction { Save, Modify, Remove }; /** * MyMoneyStorageSql get highest ID number from the database * * @return : highest ID number */ ulong highestNumberFromIdString(QString tableName, QString tableField, int prefixLength) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); if (!query.exec(m_driver->highestNumberFromIdString(tableName, tableField, prefixLength)) || !query.next()) throw MYMONEYEXCEPTIONSQL("retrieving highest ID number"); return query.value(0).toULongLong(); } /** * @name writeFromStorageMethods * @{ * These method write all data from m_storage to the database. Data which is * stored in the database is deleted. */ void writeUserInformation(); void writeInstitutions() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database // anything not in the list needs to be inserted // anything which is will be updated and removed from the list // anything left over at the end will need to be deleted // this is an expensive and inconvenient way to do things; find a better way // one way would be to build the lists when reading the db // unfortunately this object does not persist between read and write // it would also be nice if we could tell which objects had been updated since we read them in QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmInstitutions;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Institution list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList list = m_storage->institutionList(); QList insertList; QList updateList; QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmInstitutions"].updateString()); query2.prepare(m_db.m_tables["kmmInstitutions"].insertString()); signalProgress(0, list.count(), "Writing Institutions..."); foreach (const MyMoneyInstitution& i, list) { if (dbList.contains(i.id())) { dbList.removeAll(i.id()); updateList << i; } else { insertList << i; } signalProgress(++m_institutions, 0); } if (!insertList.isEmpty()) writeInstitutionList(insertList, query2); if (!updateList.isEmpty()) writeInstitutionList(updateList, query); if (!dbList.isEmpty()) { QVariantList deleteList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { deleteList << it; } query.prepare("DELETE FROM kmmInstitutions WHERE id = :id"); query.bindValue(":id", deleteList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Institution"); deleteKeyValuePairs("OFXSETTINGS", deleteList); } } void writePayees() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QSqlQuery query(*q); query.prepare("SELECT id FROM kmmPayees;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Payee list"); // krazy:exclude=crashy QList dbList; dbList.reserve(query.numRowsAffected()); while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->payeeList(); MyMoneyPayee user(QString("USER"), m_storage->user()); list.prepend(user); signalProgress(0, list.count(), "Writing Payees..."); Q_FOREACH(const MyMoneyPayee& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); q->modifyPayee(it); } else { q->addPayee(it); } signalProgress(++m_payees, 0); } if (!dbList.isEmpty()) { QMap payeesToDelete = q->fetchPayees(dbList, true); Q_FOREACH(const MyMoneyPayee& payee, payeesToDelete) { q->removePayee(payee); } } } void writeTags() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmTags;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Tag list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->tagList(); signalProgress(0, list.count(), "Writing Tags..."); QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmTags"].updateString()); query2.prepare(m_db.m_tables["kmmTags"].insertString()); foreach (const MyMoneyTag& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeTag(it, query); } else { writeTag(it, query2); } signalProgress(++m_tags, 0); } if (!dbList.isEmpty()) { QVariantList deleteList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { deleteList << it; } query.prepare(m_db.m_tables["kmmTags"].deleteString()); query.bindValue(":id", deleteList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Tag"); m_tags -= query.numRowsAffected(); } } void writeAccounts() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmAccounts;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Account list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list; m_storage->accountList(list); unsigned progress = 0; signalProgress(0, list.count(), "Writing Accounts..."); if (dbList.isEmpty()) { // new table, insert standard accounts query.prepare(m_db.m_tables["kmmAccounts"].insertString()); } else { query.prepare(m_db.m_tables["kmmAccounts"].updateString()); } // Attempt to write the standard accounts. For an empty db, this will fail. try { QList stdList; stdList << m_storage->asset(); stdList << m_storage->liability(); stdList << m_storage->expense(); stdList << m_storage->income(); stdList << m_storage->equity(); writeAccountList(stdList, query); m_accounts += stdList.size(); } catch (const MyMoneyException &) { // If the above failed, assume that the database is empty and create // the standard accounts by hand before writing them. MyMoneyAccount acc_l; acc_l.setAccountType(Account::Type::Liability); acc_l.setName("Liability"); MyMoneyAccount liability(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability), acc_l); MyMoneyAccount acc_a; acc_a.setAccountType(Account::Type::Asset); acc_a.setName("Asset"); MyMoneyAccount asset(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset), acc_a); MyMoneyAccount acc_e; acc_e.setAccountType(Account::Type::Expense); acc_e.setName("Expense"); MyMoneyAccount expense(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense), acc_e); MyMoneyAccount acc_i; acc_i.setAccountType(Account::Type::Income); acc_i.setName("Income"); MyMoneyAccount income(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income), acc_i); MyMoneyAccount acc_q; acc_q.setAccountType(Account::Type::Equity); acc_q.setName("Equity"); MyMoneyAccount equity(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity), acc_q); QList stdList; stdList << asset; stdList << liability; stdList << expense; stdList << income; stdList << equity; writeAccountList(stdList, query); m_accounts += stdList.size(); } QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmAccounts"].updateString()); query2.prepare(m_db.m_tables["kmmAccounts"].insertString()); QList updateList; QList insertList; // Update the accounts that exist; insert the ones that do not. foreach (const MyMoneyAccount& it, list) { m_transactionCountMap[it.id()] = m_storage->transactionCount(it.id()); if (dbList.contains(it.id())) { dbList.removeAll(it.id()); updateList << it; } else { insertList << it; } signalProgress(++progress, 0); ++m_accounts; } writeAccountList(updateList, query); writeAccountList(insertList, query2); // Delete the accounts that are in the db but no longer in memory. if (!dbList.isEmpty()) { QVariantList kvpList; query.prepare("DELETE FROM kmmAccounts WHERE id = :id"); foreach (const QString& it, dbList) { if (!m_storage->isStandardAccount(it)) { kvpList << it; } } query.bindValue(":id", kvpList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Account"); deleteKeyValuePairs("ACCOUNT", kvpList); deleteKeyValuePairs("ONLINEBANKING", kvpList); } } void writeTransactions() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmTransactions WHERE txType = 'N';"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Transaction list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList list; m_storage->transactionList(list, filter); signalProgress(0, list.count(), "Writing Transactions..."); QSqlQuery q2(*q); query.prepare(m_db.m_tables["kmmTransactions"].updateString()); q2.prepare(m_db.m_tables["kmmTransactions"].insertString()); foreach (const MyMoneyTransaction& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeTransaction(it.id(), it, query, "N"); } else { writeTransaction(it.id(), it, q2, "N"); } signalProgress(++m_transactions, 0); } if (!dbList.isEmpty()) { foreach (const QString& it, dbList) { deleteTransaction(it); } } } void writeSchedules() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmSchedules;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Schedule list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const auto list = m_storage->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QSqlQuery query2(*q); //TODO: find a way to prepare the queries outside of the loop. writeSchedule() // modifies the query passed to it, so they have to be re-prepared every pass. signalProgress(0, list.count(), "Writing Schedules..."); foreach (const MyMoneySchedule& it, list) { query.prepare(m_db.m_tables["kmmSchedules"].updateString()); query2.prepare(m_db.m_tables["kmmSchedules"].insertString()); bool insert = true; if (dbList.contains(it.id())) { dbList.removeAll(it.id()); insert = false; writeSchedule(it, query, insert); } else { writeSchedule(it, query2, insert); } signalProgress(++m_schedules, 0); } if (!dbList.isEmpty()) { foreach (const QString& it, dbList) { deleteSchedule(it); } } } void writeSecurities() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT id FROM kmmSecurities;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building security list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList securityList = m_storage->securityList(); signalProgress(0, securityList.count(), "Writing Securities..."); query.prepare(m_db.m_tables["kmmSecurities"].updateString()); query2.prepare(m_db.m_tables["kmmSecurities"].insertString()); foreach (const MyMoneySecurity& it, securityList) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeSecurity(it, query); } else { writeSecurity(it, query2); } signalProgress(++m_securities, 0); } if (!dbList.isEmpty()) { QVariantList idList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.prepare("DELETE FROM kmmSecurities WHERE id = :id"); query2.prepare("DELETE FROM kmmPrices WHERE fromId = :id OR toId = :id"); query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Security"); query2.bindValue(":fromId", idList); query2.bindValue(":toId", idList); if (!query2.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Security"); deleteKeyValuePairs("SECURITY", idList); } } void writePrices() { Q_Q(MyMoneyStorageSql); // due to difficulties in matching and determining deletes // easiest way is to delete all and re-insert QSqlQuery query(*q); query.prepare("DELETE FROM kmmPrices"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Prices"); // krazy:exclude=crashy m_prices = 0; const MyMoneyPriceList list = m_storage->priceList(); signalProgress(0, list.count(), "Writing Prices..."); MyMoneyPriceList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { writePricePair(*it); } } void writeCurrencies() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT ISOCode FROM kmmCurrencies;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Currency list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList currencyList = m_storage->currencyList(); signalProgress(0, currencyList.count(), "Writing Currencies..."); query.prepare(m_db.m_tables["kmmCurrencies"].updateString()); query2.prepare(m_db.m_tables["kmmCurrencies"].insertString()); foreach (const MyMoneySecurity& it, currencyList) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeCurrency(it, query); } else { writeCurrency(it, query2); } signalProgress(++m_currencies, 0); } if (!dbList.isEmpty()) { QVariantList isoCodeList; query.prepare("DELETE FROM kmmCurrencies WHERE ISOCode = :ISOCode"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { isoCodeList << it; } query.bindValue(":ISOCode", isoCodeList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Currency"); } } void writeFileInfo() { Q_Q(MyMoneyStorageSql); // we have no real way of knowing when these change, so re-write them every time QVariantList kvpList; kvpList << ""; QList > pairs; pairs << m_storage->pairs(); deleteKeyValuePairs("STORAGE", kvpList); writeKeyValuePairs("STORAGE", kvpList, pairs); QSqlQuery query(*q); query.prepare("SELECT count(*) FROM kmmFileInfo;"); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTIONSQL("checking fileinfo"); // krazy:exclude=crashy if (query.value(0).toInt() == 0) { // Cannot use "INSERT INTO kmmFileInfo DEFAULT VALUES;" because it is not supported by MySQL query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version) VALUES (null);")); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("inserting fileinfo"); // krazy:exclude=crashy } query.prepare(QLatin1String( "UPDATE kmmFileInfo SET " "version = :version, " "fixLevel = :fixLevel, " "created = :created, " "lastModified = :lastModified, " "baseCurrency = :baseCurrency, " "dateRangeStart = :dateRangeStart, " "dateRangeEnd = :dateRangeEnd, " "hiInstitutionId = :hiInstitutionId, " "hiPayeeId = :hiPayeeId, " "hiTagId = :hiTagId, " "hiAccountId = :hiAccountId, " "hiTransactionId = :hiTransactionId, " "hiScheduleId = :hiScheduleId, " "hiSecurityId = :hiSecurityId, " "hiReportId = :hiReportId, " "hiBudgetId = :hiBudgetId, " "hiOnlineJobId = :hiOnlineJobId, " "hiPayeeIdentifierId = :hiPayeeIdentifierId, " "encryptData = :encryptData, " "updateInProgress = :updateInProgress, " "logonUser = :logonUser, " "logonAt = :logonAt, " //! @todo The following updates are for backwards compatibility only //! remove backwards compatibility in a later version "institutions = :institutions, " "accounts = :accounts, " "payees = :payees, " "tags = :tags, " "transactions = :transactions, " "splits = :splits, " "securities = :securities, " "prices = :prices, " "currencies = :currencies, " "schedules = :schedules, " "reports = :reports, " "kvps = :kvps, " "budgets = :budgets; " ) ); query.bindValue(":version", m_dbVersion); query.bindValue(":fixLevel", m_storage->fileFixVersion()); query.bindValue(":created", m_storage->creationDate().toString(Qt::ISODate)); //q.bindValue(":lastModified", m_storage->lastModificationDate().toString(Qt::ISODate)); query.bindValue(":lastModified", QDate::currentDate().toString(Qt::ISODate)); query.bindValue(":baseCurrency", m_storage->pairs()["kmm-baseCurrency"]); query.bindValue(":dateRangeStart", QDate()); query.bindValue(":dateRangeEnd", QDate()); //FIXME: This modifies all m_ used in this function. // Sometimes the memory has been updated. // Should most of these be tracked in a view? // Variables actually needed are: version, fileFixVersion, creationDate, // baseCurrency, encryption, update info, and logon info. //try { //readFileInfo(); //} catch (...) { //q->startCommitUnit(Q_FUNC_INFO); //} //! @todo The following bindings are for backwards compatibility only //! remove backwards compatibility in a later version query.bindValue(":hiInstitutionId", QVariant::fromValue(q->getNextInstitutionId())); query.bindValue(":hiPayeeId", QVariant::fromValue(q->getNextPayeeId())); query.bindValue(":hiTagId", QVariant::fromValue(q->getNextTagId())); query.bindValue(":hiAccountId", QVariant::fromValue(q->getNextAccountId())); query.bindValue(":hiTransactionId", QVariant::fromValue(q->getNextTransactionId())); query.bindValue(":hiScheduleId", QVariant::fromValue(q->getNextScheduleId())); query.bindValue(":hiSecurityId", QVariant::fromValue(q->getNextSecurityId())); query.bindValue(":hiReportId", QVariant::fromValue(q->getNextReportId())); query.bindValue(":hiBudgetId", QVariant::fromValue(q->getNextBudgetId())); query.bindValue(":hiOnlineJobId", QVariant::fromValue(q->getNextOnlineJobId())); query.bindValue(":hiPayeeIdentifierId", QVariant::fromValue(q->getNextPayeeIdentifierId())); query.bindValue(":encryptData", m_encryptData); query.bindValue(":updateInProgress", "N"); query.bindValue(":logonUser", m_logonUser); query.bindValue(":logonAt", m_logonAt.toString(Qt::ISODate)); //! @todo The following bindings are for backwards compatibility only //! remove backwards compatibility in a later version query.bindValue(":institutions", (unsigned long long) m_institutions); query.bindValue(":accounts", (unsigned long long) m_accounts); query.bindValue(":payees", (unsigned long long) m_payees); query.bindValue(":tags", (unsigned long long) m_tags); query.bindValue(":transactions", (unsigned long long) m_transactions); query.bindValue(":splits", (unsigned long long) m_splits); query.bindValue(":securities", (unsigned long long) m_securities); query.bindValue(":prices", (unsigned long long) m_prices); query.bindValue(":currencies", (unsigned long long) m_currencies); query.bindValue(":schedules", (unsigned long long) m_schedules); query.bindValue(":reports", (unsigned long long) m_reports); query.bindValue(":kvps", (unsigned long long) m_kvps); query.bindValue(":budgets", (unsigned long long) m_budgets); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing FileInfo"); // krazy:exclude=crashy } void writeReports() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT id FROM kmmReportConfig;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Report list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->reportList(); signalProgress(0, list.count(), "Writing Reports..."); query.prepare(m_db.m_tables["kmmReportConfig"].updateString()); query2.prepare(m_db.m_tables["kmmReportConfig"].insertString()); foreach (const MyMoneyReport& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeReport(it, query); } else { writeReport(it, query2); } signalProgress(++m_reports, 0); } if (!dbList.isEmpty()) { QVariantList idList; query.prepare("DELETE FROM kmmReportConfig WHERE id = :id"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Report"); } } void writeBudgets() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT name FROM kmmBudgetConfig;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Budget list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->budgetList(); signalProgress(0, list.count(), "Writing Budgets..."); query.prepare(m_db.m_tables["kmmBudgetConfig"].updateString()); query2.prepare(m_db.m_tables["kmmBudgetConfig"].insertString()); foreach (const MyMoneyBudget& it, list) { if (dbList.contains(it.name())) { dbList.removeAll(it.name()); writeBudget(it, query); } else { writeBudget(it, query2); } signalProgress(++m_budgets, 0); } if (!dbList.isEmpty()) { QVariantList idList; query.prepare("DELETE FROM kmmBudgetConfig WHERE id = :id"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.bindValue(":name", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Budget"); } } bool clearTable(const QString& tableName, QSqlQuery& query) { if (query.exec(QString("SELECT count(*) FROM %1").arg(tableName))) { if (query.next()) { if (query.value(0).toUInt() > 0) { if (!query.exec(QString("DELETE FROM %1").arg(tableName))) return false; } } } return true; } void writeOnlineJobs() { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); if (!clearTable(QStringLiteral("kmmOnlineJobs"), query)) throw MYMONEYEXCEPTIONSQL("Clean kmmOnlineJobs table"); if (!clearTable(QStringLiteral("kmmSepaOrders"), query)) throw MYMONEYEXCEPTIONSQL("Clean kmmSepaOrders table"); if (!clearTable(QStringLiteral("kmmNationalAccountNumber"), query)) throw MYMONEYEXCEPTIONSQL("Clean kmmNationalAccountNumber table"); const QList jobs(m_storage->onlineJobList()); signalProgress(0, jobs.count(), i18n("Inserting online jobs.")); // Create list for onlineJobs which failed and the reason therefor QList > failedJobs; int jobCount = 0; foreach (const onlineJob& job, jobs) { try { q->addOnlineJob(job); } catch (const MyMoneyException &e) { // Do not save e as this may point to an inherited class failedJobs.append(QPair(job, e.what())); qDebug() << "Failed to save onlineJob" << job.id() << "Reason:" << e.what(); } signalProgress(++jobCount, 0); } if (!failedJobs.isEmpty()) { /** @todo Improve error message */ throw MYMONEYEXCEPTION_CSTRING("Could not save onlineJob."); } } /** @} */ /** * @name writeMethods * @{ * These methods bind the data fields of MyMoneyObjects to a given query and execute the query. * This is helpfull as the query has usually an update and a insert format. */ void writeInstitutionList(const QList& iList, QSqlQuery& query) { QVariantList idList; QVariantList nameList; QVariantList managerList; QVariantList routingCodeList; QVariantList addressStreetList; QVariantList addressCityList; QVariantList addressZipcodeList; QVariantList telephoneList; QList > kvpPairsList; foreach (const MyMoneyInstitution& i, iList) { idList << i.id(); nameList << i.name(); managerList << i.manager(); routingCodeList << i.sortcode(); addressStreetList << i.street(); addressCityList << i.city(); addressZipcodeList << i.postcode(); telephoneList << i.telephone(); kvpPairsList << i.pairs(); } query.bindValue(":id", idList); query.bindValue(":name", nameList); query.bindValue(":manager", managerList); query.bindValue(":routingCode", routingCodeList); query.bindValue(":addressStreet", addressStreetList); query.bindValue(":addressCity", addressCityList); query.bindValue(":addressZipcode", addressZipcodeList); query.bindValue(":telephone", telephoneList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing Institution"); deleteKeyValuePairs("OFXSETTINGS", idList); writeKeyValuePairs("OFXSETTINGS", idList, kvpPairsList); // Set m_hiIdInstitutions to 0 to force recalculation the next time it is requested m_hiIdInstitutions = 0; } void writePayee(const MyMoneyPayee& p, QSqlQuery& query, bool isUserInfo = false) { if (isUserInfo) { query.bindValue(":id", "USER"); } else { query.bindValue(":id", p.id()); } query.bindValue(":name", p.name()); query.bindValue(":reference", p.reference()); query.bindValue(":email", p.email()); query.bindValue(":addressStreet", p.address()); query.bindValue(":addressCity", p.city()); query.bindValue(":addressZipcode", p.postcode()); query.bindValue(":addressState", p.state()); query.bindValue(":telephone", p.telephone()); query.bindValue(":notes", p.notes()); query.bindValue(":defaultAccountId", p.defaultAccountId()); bool ignoreCase; QString matchKeys; auto type = p.matchData(ignoreCase, matchKeys); query.bindValue(":matchData", static_cast(type)); if (ignoreCase) query.bindValue(":matchIgnoreCase", "Y"); else query.bindValue(":matchIgnoreCase", "N"); query.bindValue(":matchKeys", matchKeys); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL("writing Payee"); // krazy:exclude=crashy if (!isUserInfo) m_hiIdPayees = 0; } void writeTag(const MyMoneyTag& ta, QSqlQuery& query) { query.bindValue(":id", ta.id()); query.bindValue(":name", ta.name()); query.bindValue(":tagColor", ta.tagColor().name()); if (ta.isClosed()) query.bindValue(":closed", "Y"); else query.bindValue(":closed", "N"); query.bindValue(":notes", ta.notes()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Tag"); // krazy:exclude=crashy m_hiIdTags = 0; } void writeAccountList(const QList& accList, QSqlQuery& query) { //MyMoneyMoney balance = m_storagePtr->balance(acc.id(), QDate()); QVariantList idList; QVariantList institutionIdList; QVariantList parentIdList; QVariantList lastReconciledList; QVariantList lastModifiedList; QVariantList openingDateList; QVariantList accountNumberList; QVariantList accountTypeList; QVariantList accountTypeStringList; QVariantList isStockAccountList; QVariantList accountNameList; QVariantList descriptionList; QVariantList currencyIdList; QVariantList balanceList; QVariantList balanceFormattedList; QVariantList transactionCountList; QList > pairs; QList > onlineBankingPairs; foreach (const MyMoneyAccount& a, accList) { idList << a.id(); institutionIdList << a.institutionId(); parentIdList << a.parentAccountId(); if (a.lastReconciliationDate() == QDate()) lastReconciledList << a.lastReconciliationDate(); else lastReconciledList << a.lastReconciliationDate().toString(Qt::ISODate); lastModifiedList << a.lastModified(); if (a.openingDate() == QDate()) openingDateList << a.openingDate(); else openingDateList << a.openingDate().toString(Qt::ISODate); accountNumberList << a.number(); accountTypeList << (int)a.accountType(); accountTypeStringList << MyMoneyAccount::accountTypeToString(a.accountType()); if (a.accountType() == Account::Type::Stock) isStockAccountList << "Y"; else isStockAccountList << "N"; accountNameList << a.name(); descriptionList << a.description(); currencyIdList << a.currencyId(); // This section attempts to get the balance from the database, if possible // That way, the balance fields are kept in sync. If that fails, then // It is assumed that the account actually knows its correct balance. //FIXME: Using exceptions for branching always feels like a kludge. // Look for a better way. try { MyMoneyMoney bal = m_storage->balance(a.id(), QDate()); balanceList << bal.toString(); balanceFormattedList << bal.formatMoney("", -1, false); } catch (const MyMoneyException &) { balanceList << a.balance().toString(); balanceFormattedList << a.balance().formatMoney("", -1, false); } transactionCountList << quint64(m_transactionCountMap[a.id()]); //MMAccount inherits from KVPContainer AND has a KVPContainer member //so handle both pairs << a.pairs(); onlineBankingPairs << a.onlineBankingSettings().pairs(); } query.bindValue(":id", idList); query.bindValue(":institutionId", institutionIdList); query.bindValue(":parentId", parentIdList); query.bindValue(":lastReconciled", lastReconciledList); query.bindValue(":lastModified", lastModifiedList); query.bindValue(":openingDate", openingDateList); query.bindValue(":accountNumber", accountNumberList); query.bindValue(":accountType", accountTypeList); query.bindValue(":accountTypeString", accountTypeStringList); query.bindValue(":isStockAccount", isStockAccountList); query.bindValue(":accountName", accountNameList); query.bindValue(":description", descriptionList); query.bindValue(":currencyId", currencyIdList); query.bindValue(":balance", balanceList); query.bindValue(":balanceFormatted", balanceFormattedList); query.bindValue(":transactionCount", transactionCountList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing Account"); //Add in Key-Value Pairs for accounts. deleteKeyValuePairs("ACCOUNT", idList); deleteKeyValuePairs("ONLINEBANKING", idList); writeKeyValuePairs("ACCOUNT", idList, pairs); writeKeyValuePairs("ONLINEBANKING", idList, onlineBankingPairs); m_hiIdAccounts = 0; } void writeTransaction(const QString& txId, const MyMoneyTransaction& tx, QSqlQuery& query, const QString& type) { query.bindValue(":id", txId); query.bindValue(":txType", type); query.bindValue(":postDate", tx.postDate().toString(Qt::ISODate)); query.bindValue(":memo", tx.memo()); query.bindValue(":entryDate", tx.entryDate().toString(Qt::ISODate)); query.bindValue(":currencyId", tx.commodity()); query.bindValue(":bankId", tx.bankID()); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL("writing Transaction"); // krazy:exclude=crashy m_txPostDate = tx.postDate(); // FIXME: TEMP till Tom puts date in split object QList splitList = tx.splits(); writeSplits(txId, type, splitList); //Add in Key-Value Pairs for transactions. QVariantList idList; idList << txId; deleteKeyValuePairs("TRANSACTION", idList); QList > pairs; pairs << tx.pairs(); writeKeyValuePairs("TRANSACTION", idList, pairs); m_hiIdTransactions = 0; } void writeSplits(const QString& txId, const QString& type, const QList& splitList) { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QList insertList; QList updateList; QList insertIdList; QList updateIdList; QSqlQuery query(*q); query.prepare("SELECT splitId FROM kmmSplits where transactionId = :id;"); query.bindValue(":id", txId); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Split list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toUInt()); QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmSplits"].updateString()); query2.prepare(m_db.m_tables["kmmSplits"].insertString()); auto i = 0; for (auto it = splitList.constBegin(); it != splitList.constEnd(); ++it) { if (dbList.contains(i)) { dbList.removeAll(i); updateList << *it; updateIdList << i; } else { ++m_splits; insertList << *it; insertIdList << i; } ++i; } if (!insertList.isEmpty()) { writeSplitList(txId, insertList, type, insertIdList, query2); writeTagSplitsList(txId, insertList, insertIdList); } if (!updateList.isEmpty()) { writeSplitList(txId, updateList, type, updateIdList, query); deleteTagSplitsList(txId, updateIdList); writeTagSplitsList(txId, updateList, updateIdList); } if (!dbList.isEmpty()) { QVector txIdList(dbList.count(), txId); QVariantList splitIdList; query.prepare("DELETE FROM kmmSplits WHERE transactionId = :txId AND splitId = :splitId"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it, dbList) { splitIdList << it; } query.bindValue(":txId", txIdList.toList()); query.bindValue(":splitId", splitIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits"); } } void writeTagSplitsList (const QString& txId, const QList& splitList, const QList& splitIdList) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QVariantList tagIdList; QVariantList txIdList; QVariantList splitIdList_TagSplits; int i = 0, l = 0; foreach (const MyMoneySplit& s, splitList) { for (l = 0; l < s.tagIdList().size(); ++l) { tagIdList << s.tagIdList()[l]; splitIdList_TagSplits << splitIdList[i]; txIdList << txId; } i++; } QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmTagSplits"].insertString()); query.bindValue(":tagId", tagIdList); query.bindValue(":splitId", splitIdList_TagSplits); query.bindValue(":transactionId", txIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing tagSplits"); } void writeSplitList (const QString& txId, const QList& splitList, const QString& type, const QList& splitIdList, QSqlQuery& query) { QVariantList txIdList; QVariantList typeList; QVariantList payeeIdList; QVariantList reconcileDateList; QVariantList actionList; QVariantList reconcileFlagList; QVariantList valueList; QVariantList valueFormattedList; QVariantList sharesList; QVariantList sharesFormattedList; QVariantList priceList; QVariantList priceFormattedList; QVariantList memoList; QVariantList accountIdList; QVariantList costCenterIdList; QVariantList checkNumberList; QVariantList postDateList; QVariantList bankIdList; QVariantList kvpIdList; QList > kvpPairsList; int i = 0; foreach (const MyMoneySplit& s, splitList) { txIdList << txId; typeList << type; payeeIdList << s.payeeId(); if (s.reconcileDate() == QDate()) reconcileDateList << s.reconcileDate(); else reconcileDateList << s.reconcileDate().toString(Qt::ISODate); actionList << s.action(); reconcileFlagList << (int)s.reconcileFlag(); valueList << s.value().toString(); valueFormattedList << s.value().formatMoney("", -1, false).replace(QChar(','), QChar('.')); sharesList << s.shares().toString(); MyMoneyAccount acc = m_storage->account(s.accountId()); MyMoneySecurity sec = m_storage->security(acc.currencyId()); sharesFormattedList << s.price(). formatMoney("", MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()), false). replace(QChar(','), QChar('.')); MyMoneyMoney price = s.actualPrice(); if (!price.isZero()) { priceList << price.toString(); priceFormattedList << price.formatMoney ("", sec.pricePrecision(), false) .replace(QChar(','), QChar('.')); } else { priceList << QString(); priceFormattedList << QString(); } memoList << s.memo(); accountIdList << s.accountId(); costCenterIdList << s.costCenterId(); checkNumberList << s.number(); postDateList << m_txPostDate.toString(Qt::ISODate); // FIXME: when Tom puts date into split object bankIdList << s.bankID(); kvpIdList << QString(txId + QString::number(splitIdList[i])); kvpPairsList << s.pairs(); ++i; } query.bindValue(":transactionId", txIdList); query.bindValue(":txType", typeList); QVariantList iList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it_s, splitIdList) { iList << it_s; } query.bindValue(":splitId", iList); query.bindValue(":payeeId", payeeIdList); query.bindValue(":reconcileDate", reconcileDateList); query.bindValue(":action", actionList); query.bindValue(":reconcileFlag", reconcileFlagList); query.bindValue(":value", valueList); query.bindValue(":valueFormatted", valueFormattedList); query.bindValue(":shares", sharesList); query.bindValue(":sharesFormatted", sharesFormattedList); query.bindValue(":price", priceList); query.bindValue(":priceFormatted", priceFormattedList); query.bindValue(":memo", memoList); query.bindValue(":accountId", accountIdList); query.bindValue(":costCenterId", costCenterIdList); query.bindValue(":checkNumber", checkNumberList); query.bindValue(":postDate", postDateList); query.bindValue(":bankId", bankIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing Split"); deleteKeyValuePairs("SPLIT", kvpIdList); writeKeyValuePairs("SPLIT", kvpIdList, kvpPairsList); } void writeSchedule(const MyMoneySchedule& sch, QSqlQuery& query, bool insert) { query.bindValue(":id", sch.id()); query.bindValue(":name", sch.name()); query.bindValue(":type", (int)sch.type()); query.bindValue(":typeString", MyMoneySchedule::scheduleTypeToString(sch.type())); - query.bindValue(":occurence", (int)sch.occurrencePeriod()); // krazy:exclude=spelling + query.bindValue(":occurence", (int)sch.occurrence()); // krazy:exclude=spelling query.bindValue(":occurenceMultiplier", sch.occurrenceMultiplier()); // krazy:exclude=spelling query.bindValue(":occurenceString", sch.occurrenceToString()); // krazy:exclude=spelling query.bindValue(":paymentType", (int)sch.paymentType()); query.bindValue(":paymentTypeString", MyMoneySchedule::paymentMethodToString(sch.paymentType())); query.bindValue(":startDate", sch.startDate().toString(Qt::ISODate)); query.bindValue(":endDate", sch.endDate().toString(Qt::ISODate)); if (sch.isFixed()) { query.bindValue(":fixed", "Y"); } else { query.bindValue(":fixed", "N"); } if (sch.lastDayInMonth()) { query.bindValue(":lastDayInMonth", "Y"); } else { query.bindValue(":lastDayInMonth", "N"); } if (sch.autoEnter()) { query.bindValue(":autoEnter", "Y"); } else { query.bindValue(":autoEnter", "N"); } query.bindValue(":lastPayment", sch.lastPayment()); query.bindValue(":nextPaymentDue", sch.nextDueDate().toString(Qt::ISODate)); query.bindValue(":weekendOption", (int)sch.weekendOption()); query.bindValue(":weekendOptionString", MyMoneySchedule::weekendOptionToString(sch.weekendOption())); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Schedules"); // krazy:exclude=crashy //store the payment history for this scheduled task. //easiest way is to delete all and re-insert; it's not a high use table query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id;"); query.bindValue(":id", sch.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule Payment History"); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedulePaymentHistory"].insertString()); foreach (const QDate& it, sch.recordedPayments()) { query.bindValue(":schedId", sch.id()); query.bindValue(":payDate", it.toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Schedule Payment History"); // krazy:exclude=crashy } //store the transaction data for this task. if (!insert) { query.prepare(m_db.m_tables["kmmTransactions"].updateString()); } else { query.prepare(m_db.m_tables["kmmTransactions"].insertString()); } writeTransaction(sch.id(), sch.transaction(), query, "S"); //FIXME: enable when schedules have KVPs. //Add in Key-Value Pairs for transactions. //deleteKeyValuePairs("SCHEDULE", sch.id()); //writeKeyValuePairs("SCHEDULE", sch.id(), sch.pairs()); } void writeSecurity(const MyMoneySecurity& security, QSqlQuery& query) { query.bindValue(":id", security.id()); query.bindValue(":name", security.name()); query.bindValue(":symbol", security.tradingSymbol()); query.bindValue(":type", static_cast(security.securityType())); query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(security.securityType())); query.bindValue(":roundingMethod", static_cast(security.roundingMethod())); query.bindValue(":smallestAccountFraction", security.smallestAccountFraction()); query.bindValue(":pricePrecision", security.pricePrecision()); query.bindValue(":tradingCurrency", security.tradingCurrency()); query.bindValue(":tradingMarket", security.tradingMarket()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Securities"); // krazy:exclude=crashy //Add in Key-Value Pairs for security QVariantList idList; idList << security.id(); QList > pairs; pairs << security.pairs(); deleteKeyValuePairs("SECURITY", idList); writeKeyValuePairs("SECURITY", idList, pairs); m_hiIdSecurities = 0; } void writePricePair(const MyMoneyPriceEntries& p) { MyMoneyPriceEntries::ConstIterator it; for (it = p.constBegin(); it != p.constEnd(); ++it) { writePrice(*it); signalProgress(++m_prices, 0); } } void writePrice(const MyMoneyPrice& p) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmPrices"].insertString()); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); query.bindValue(":price", p.rate(QString()).toString()); query.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", 2)); query.bindValue(":priceSource", p.source()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Prices"); // krazy:exclude=crashy } void writeCurrency(const MyMoneySecurity& currency, QSqlQuery& query) { query.bindValue(":ISOcode", currency.id()); query.bindValue(":name", currency.name()); query.bindValue(":type", static_cast(currency.securityType())); query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(currency.securityType())); // writing the symbol as three short ints is a PITA, but the // problem is that database drivers have incompatible ways of declaring UTF8 QString symbol = currency.tradingSymbol() + " "; const ushort* symutf = symbol.utf16(); //int ix = 0; //while (x[ix] != '\0') qDebug() << "symbol" << symbol << "char" << ix << "=" << x[ix++]; //q.bindValue(":symbol1", symbol.mid(0,1).unicode()->unicode()); //q.bindValue(":symbol2", symbol.mid(1,1).unicode()->unicode()); //q.bindValue(":symbol3", symbol.mid(2,1).unicode()->unicode()); query.bindValue(":symbol1", symutf[0]); query.bindValue(":symbol2", symutf[1]); query.bindValue(":symbol3", symutf[2]); query.bindValue(":symbolString", symbol); query.bindValue(":smallestCashFraction", currency.smallestCashFraction()); query.bindValue(":smallestAccountFraction", currency.smallestAccountFraction()); query.bindValue(":pricePrecision", currency.pricePrecision()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Currencies"); // krazy:exclude=crashy } void writeReport(const MyMoneyReport& rep, QSqlQuery& query) { QDomDocument d; // create a dummy XML document QDomElement e = d.createElement("REPORTS"); d.appendChild(e); MyMoneyXmlContentHandler2::writeReport(rep, d, e); // write the XML to document query.bindValue(":id", rep.id()); query.bindValue(":name", rep.name()); query.bindValue(":XML", d.toString()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Reports"); // krazy:exclude=crashy } void writeBudget(const MyMoneyBudget& bud, QSqlQuery& query) { QDomDocument d; // create a dummy XML document QDomElement e = d.createElement("BUDGETS"); d.appendChild(e); MyMoneyXmlContentHandler2::writeBudget(bud, d, e); // write the XML to document query.bindValue(":id", bud.id()); query.bindValue(":name", bud.name()); query.bindValue(":start", bud.budgetStart()); query.bindValue(":XML", d.toString()); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL("writing Budgets"); // krazy:exclude=crashy } void writeKeyValuePairs(const QString& kvpType, const QVariantList& kvpId, const QList >& pairs) { Q_Q(MyMoneyStorageSql); if (pairs.empty()) return; QVariantList type; QVariantList id; QVariantList key; QVariantList value; int pairCount = 0; for (int i = 0; i < kvpId.size(); ++i) { QMap::ConstIterator it; for (it = pairs[i].constBegin(); it != pairs[i].constEnd(); ++it) { type << kvpType; id << kvpId[i]; key << it.key(); value << it.value(); } pairCount += pairs[i].size(); } QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmKeyValuePairs"].insertString()); query.bindValue(":kvpType", type); query.bindValue(":kvpId", id); query.bindValue(":kvpKey", key); query.bindValue(":kvpData", value); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing KVP"); m_kvps += pairCount; } void writeOnlineJob(const onlineJob& job, QSqlQuery& query) { Q_ASSERT(job.id().startsWith('O')); query.bindValue(":id", job.id()); query.bindValue(":type", job.taskIid()); query.bindValue(":jobSend", job.sendDate()); query.bindValue(":bankAnswerDate", job.bankAnswerDate()); switch (job.bankAnswerState()) { case eMyMoney::OnlineJob::sendingState::acceptedByBank: query.bindValue(":state", QLatin1String("acceptedByBank")); break; case eMyMoney::OnlineJob::sendingState::rejectedByBank: query.bindValue(":state", QLatin1String("rejectedByBank")); break; case eMyMoney::OnlineJob::sendingState::abortedByUser: query.bindValue(":state", QLatin1String("abortedByUser")); break; case eMyMoney::OnlineJob::sendingState::sendingError: query.bindValue(":state", QLatin1String("sendingError")); break; case eMyMoney::OnlineJob::sendingState::noBankAnswer: default: query.bindValue(":state", QLatin1String("noBankAnswer")); } query.bindValue(":locked", QVariant::fromValue(job.isLocked() ? QLatin1String("Y") : QLatin1String("N"))); } void writePayeeIdentifier(const payeeIdentifier& pid, QSqlQuery& query) { query.bindValue(":id", pid.idString()); query.bindValue(":type", pid.iid()); if (!query.exec()) { // krazy:exclude=crashy qWarning() << buildError(query, Q_FUNC_INFO, QString("modifying payeeIdentifier")); throw MYMONEYEXCEPTIONSQL("modifying payeeIdentifier"); // krazy:exclude=crashy } } /** @} */ /** * @name readMethods * @{ */ void readFileInfo() { Q_Q(MyMoneyStorageSql); signalProgress(0, 1, QObject::tr("Loading file information...")); QSqlQuery query(*q); query.prepare( "SELECT " " created, lastModified, " " encryptData, logonUser, logonAt, " " (SELECT count(*) FROM kmmInstitutions) AS institutions, " " (SELECT count(*) from kmmAccounts) AS accounts, " " (SELECT count(*) FROM kmmCurrencies) AS currencies, " " (SELECT count(*) FROM kmmPayees) AS payees, " " (SELECT count(*) FROM kmmTags) AS tags, " " (SELECT count(*) FROM kmmTransactions) AS transactions, " " (SELECT count(*) FROM kmmSplits) AS splits, " " (SELECT count(*) FROM kmmSecurities) AS securities, " " (SELECT count(*) FROM kmmCurrencies) AS currencies, " " (SELECT count(*) FROM kmmSchedules) AS schedules, " " (SELECT count(*) FROM kmmPrices) AS prices, " " (SELECT count(*) FROM kmmKeyValuePairs) AS kvps, " " (SELECT count(*) FROM kmmReportConfig) AS reports, " " (SELECT count(*) FROM kmmBudgetConfig) AS budgets, " " (SELECT count(*) FROM kmmOnlineJobs) AS onlineJobs, " " (SELECT count(*) FROM kmmPayeeIdentifier) AS payeeIdentifier " "FROM kmmFileInfo;" ); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("reading FileInfo"); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTIONSQL("retrieving FileInfo"); QSqlRecord rec = query.record(); m_storage->setCreationDate(GETDATE(rec.indexOf("created"))); m_storage->setLastModificationDate(GETDATE(rec.indexOf("lastModified"))); m_institutions = (ulong) GETULL(rec.indexOf("institutions")); m_accounts = (ulong) GETULL(rec.indexOf("accounts")); m_payees = (ulong) GETULL(rec.indexOf("payees")); m_tags = (ulong) GETULL(rec.indexOf("tags")); m_transactions = (ulong) GETULL(rec.indexOf("transactions")); m_splits = (ulong) GETULL(rec.indexOf("splits")); m_securities = (ulong) GETULL(rec.indexOf("securities")); m_currencies = (ulong) GETULL(rec.indexOf("currencies")); m_schedules = (ulong) GETULL(rec.indexOf("schedules")); m_prices = (ulong) GETULL(rec.indexOf("prices")); m_kvps = (ulong) GETULL(rec.indexOf("kvps")); m_reports = (ulong) GETULL(rec.indexOf("reports")); m_budgets = (ulong) GETULL(rec.indexOf("budgets")); m_onlineJobs = (ulong) GETULL(rec.indexOf("onlineJobs")); m_payeeIdentifier = (ulong) GETULL(rec.indexOf("payeeIdentifier")); m_encryptData = GETSTRING(rec.indexOf("encryptData")); m_logonUser = GETSTRING(rec.indexOf("logonUser")); m_logonAt = GETDATETIME(rec.indexOf("logonAt")); signalProgress(1, 0); m_storage->setPairs(readKeyValuePairs("STORAGE", QString("")).pairs()); } void readLogonData(); void readUserInformation(); void readInstitutions() { Q_Q(MyMoneyStorageSql); try { QMap iList = q->fetchInstitutions(); m_storage->loadInstitutions(iList); readFileInfo(); } catch (const MyMoneyException &) { throw; } } void readAccounts() { Q_Q(MyMoneyStorageSql); m_storage->loadAccounts(q->fetchAccounts()); } void readTransactions(const QString& tidList, const QString& dateClause) { Q_Q(MyMoneyStorageSql); try { m_storage->loadTransactions(q->fetchTransactions(tidList, dateClause)); } catch (const MyMoneyException &) { throw; } } void readTransactions() { readTransactions(QString(), QString()); } MyMoneySplit readSplit(const QSqlQuery& query) const { Q_Q(const MyMoneyStorageSql); // Set these up as statics, since the field numbers should not change // during execution. static const MyMoneyDbTable& t = m_db.m_tables["kmmSplits"]; static const int splitIdCol = t.fieldNumber("splitId"); static const int transactionIdCol = t.fieldNumber("transactionId"); static const int payeeIdCol = t.fieldNumber("payeeId"); static const int reconcileDateCol = t.fieldNumber("reconcileDate"); static const int actionCol = t.fieldNumber("action"); static const int reconcileFlagCol = t.fieldNumber("reconcileFlag"); static const int valueCol = t.fieldNumber("value"); static const int sharesCol = t.fieldNumber("shares"); static const int priceCol = t.fieldNumber("price"); static const int memoCol = t.fieldNumber("memo"); static const int accountIdCol = t.fieldNumber("accountId"); static const int costCenterIdCol = t.fieldNumber("costCenterId"); static const int checkNumberCol = t.fieldNumber("checkNumber"); // static const int postDateCol = t.fieldNumber("postDate"); // FIXME - when Tom puts date into split object static const int bankIdCol = t.fieldNumber("bankId"); MyMoneySplit s; QList tagIdList; QSqlQuery query1(*const_cast (q)); query1.prepare("SELECT tagId from kmmTagSplits where splitId = :id and transactionId = :transactionId"); query1.bindValue(":id", GETSTRING(splitIdCol)); query1.bindValue(":transactionId", GETSTRING(transactionIdCol)); if (!query1.exec()) throw MYMONEYEXCEPTIONSQL("reading tagId in Split"); // krazy:exclude=crashy while (query1.next()) tagIdList << query1.value(0).toString(); s.setTagIdList(tagIdList); s.setPayeeId(GETSTRING(payeeIdCol)); s.setReconcileDate(GETDATE(reconcileDateCol)); s.setAction(GETSTRING(actionCol)); s.setReconcileFlag(static_cast(GETINT(reconcileFlagCol))); s.setValue(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(valueCol)))); s.setShares(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(sharesCol)))); s.setPrice(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(priceCol)))); s.setMemo(GETSTRING(memoCol)); s.setAccountId(GETSTRING(accountIdCol)); s.setCostCenterId(GETSTRING(costCenterIdCol)); s.setNumber(GETSTRING(checkNumberCol)); //s.setPostDate(GETDATETIME(postDateCol)); // FIXME - when Tom puts date into split object s.setBankID(GETSTRING(bankIdCol)); return s; } const MyMoneyKeyValueContainer readKeyValuePairs(const QString& kvpType, const QString& kvpId) const { Q_Q(const MyMoneyStorageSql); MyMoneyKeyValueContainer list; QSqlQuery query(*const_cast (q)); query.prepare("SELECT kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type and kvpId = :id;"); query.bindValue(":type", kvpType); query.bindValue(":id", kvpId); if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("reading Kvp for %1 %2").arg(kvpType) // krazy:exclude=crashy .arg(kvpId)); while (query.next()) list.setValue(query.value(0).toString(), query.value(1).toString()); return (list); } const QHash readKeyValuePairs(const QString& kvpType, const QStringList& kvpIdList) const { Q_Q(const MyMoneyStorageSql); QHash retval; QSqlQuery query(*const_cast (q)); QString idList; if (!kvpIdList.empty()) { idList = QString(" and kvpId IN ('%1')").arg(kvpIdList.join("', '")); } QString sQuery = QString("SELECT kvpId, kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type %1 order by kvpId;").arg(idList); query.prepare(sQuery); query.bindValue(":type", kvpType); if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("reading Kvp List for %1").arg(kvpType)); // krazy:exclude=crashy // Reserve enough space for all values. retval.reserve(kvpIdList.size()); // The loop below is designed to limit the number of calls to // QHash::operator[] in order to speed up calls to this function. This // assumes that QString::operator== is faster. /* if (q.next()) { QString oldkey = q.value(0).toString(); MyMoneyKeyValueContainer& kvpc = retval[oldkey]; kvpc.setValue(q.value(1).toString(), q.value(2).toString()); while (q.next()) { if (q.value(0).toString() != oldkey) { oldkey = q.value(0).toString(); kvpc = retval[oldkey]; } kvpc.setValue(q.value(1).toString(), q.value(2).toString()); } } */ const bool isOnlineBanking = kvpType.toLower().compare(QLatin1String("onlinebanking")) == 0; while (query.next()) { QString kvpId = query.value(0).toString(); QString kvpKey = query.value(1).toString(); QString kvpData = query.value(2).toString(); if (isOnlineBanking) { if ((kvpKey.toLower().compare(QLatin1String("provider")) == 0) && (kvpData.toLower().compare(QLatin1String("kmymoney ofx")) == 0)) { kvpData = QStringLiteral("ofximporter"); } } retval[kvpId].setValue(kvpKey, kvpData); } return (retval); } void readSchedules() { Q_Q(MyMoneyStorageSql); try { m_storage->loadSchedules(q->fetchSchedules()); } catch (const MyMoneyException &) { throw; } } void readSecurities() { Q_Q(MyMoneyStorageSql); try { m_storage->loadSecurities(q->fetchSecurities()); } catch (const MyMoneyException &) { throw; } } void readPrices() { Q_Q(MyMoneyStorageSql); try { m_storage->loadPrices(q->fetchPrices()); } catch (const MyMoneyException &) { throw; } } void readCurrencies() { Q_Q(MyMoneyStorageSql); try { m_storage->loadCurrencies(q->fetchCurrencies()); } catch (const MyMoneyException &) { throw; } } void readReports() { Q_Q(MyMoneyStorageSql); try { m_storage->loadReports(q->fetchReports()); } catch (const MyMoneyException &) { throw; } } void readBudgets() { Q_Q(MyMoneyStorageSql); m_storage->loadBudgets(q->fetchBudgets()); } void readOnlineJobs() { Q_Q(MyMoneyStorageSql); m_storage->loadOnlineJobs(q->fetchOnlineJobs()); } /** @} */ void deleteTransaction(const QString& id) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); QVariantList idList; idList << id; query.prepare("DELETE FROM kmmSplits WHERE transactionId = :transactionId;"); query.bindValue(":transactionId", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits"); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = 'SPLIT' " "AND kvpId LIKE '?%'"); query.bindValue(1, idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits KVP"); m_splits -= query.numRowsAffected(); deleteKeyValuePairs("TRANSACTION", idList); query.prepare(m_db.m_tables["kmmTransactions"].deleteString()); query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Transaction"); } void deleteTagSplitsList(const QString& txId, const QList& splitIdList) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QVariantList iList; QVariantList transactionIdList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it_s, splitIdList) { iList << it_s; transactionIdList << txId; } QSqlQuery query(*q); query.prepare("DELETE FROM kmmTagSplits WHERE transactionId = :transactionId AND splitId = :splitId"); query.bindValue(":splitId", iList); query.bindValue(":transactionId", transactionIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting tagSplits"); } void deleteSchedule(const QString& id) { Q_Q(MyMoneyStorageSql); deleteTransaction(id); QSqlQuery query(*q); query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id"); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule Payment History"); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedules"].deleteString()); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule"); // krazy:exclude=crashy //FIXME: enable when schedules have KVPs. //deleteKeyValuePairs("SCHEDULE", id); } void deleteKeyValuePairs(const QString& kvpType, const QVariantList& idList) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = :kvpType AND kvpId = :kvpId;"); QVariantList typeList; for (int i = 0; i < idList.size(); ++i) { typeList << kvpType; } query.bindValue(":kvpType", typeList); query.bindValue(":kvpId", idList); if (!query.execBatch()) { QString idString; for (int i = 0; i < idList.size(); ++i) { idString.append(idList[i].toString() + ' '); } throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("deleting kvp for %1 %2").arg(kvpType).arg(idString)); } m_kvps -= query.numRowsAffected(); } ulong calcHighId(ulong i, const QString& id) { QString nid = id; ulong high = (ulong) nid.remove(QRegExp("[A-Z]*")).toULongLong(); return std::max(high, i); } void setVersion(const QString& version); int splitState(const TransactionFilter::State& state) const { auto rc = (int)Split::State::NotReconciled; switch (state) { default: case TransactionFilter::State::NotReconciled: break; case TransactionFilter::State::Cleared: rc = (int)Split::State::Cleared; break; case TransactionFilter::State::Reconciled: rc = (int)Split::State::Reconciled; break; case TransactionFilter::State::Frozen: rc = (int)Split::State::Frozen; break; } return rc; } QDate getDate(const QString& date) const { return (date.isNull() ? QDate() : QDate::fromString(date, Qt::ISODate)); } QDateTime getDateTime(const QString& date) const { return (date.isNull() ? QDateTime() : QDateTime::fromString(date, Qt::ISODate)); } bool fileExists(const QString& dbName) { QFile f(dbName); if (!f.exists()) { m_error = i18n("SQLite file %1 does not exist", dbName); return (false); } return (true); } /** @brief a function to build a comprehensive error message for an SQL error */ QString& buildError(const QSqlQuery& query, const QString& function, const QString& messageb) const { Q_Q(const MyMoneyStorageSql); return (buildError(query, function, messageb, q)); } QString& buildError(const QSqlQuery& query, const QString& function, const QString& message, const QSqlDatabase* db) const { Q_Q(const MyMoneyStorageSql); QString s = QString("Error in function %1 : %2").arg(function).arg(message); s += QString("\nDriver = %1, Host = %2, User = %3, Database = %4") .arg(db->driverName()).arg(db->hostName()).arg(db->userName()).arg(db->databaseName()); QSqlError e = db->lastError(); s += QString("\nDriver Error: %1").arg(e.driverText()); s += QString("\nDatabase Error No %1: %2").arg(e.number()).arg(e.databaseText()); s += QString("\nText: %1").arg(e.text()); s += QString("\nError type %1").arg(e.type()); e = query.lastError(); s += QString("\nExecuted: %1").arg(query.executedQuery()); s += QString("\nQuery error No %1: %2").arg(e.number()).arg(e.text()); s += QString("\nError type %1").arg(e.type()); const_cast (q)->d_func()->m_error = s; qDebug("%s", qPrintable(s)); const_cast (q)->cancelCommitUnit(function); return (const_cast (q)->d_func()->m_error); } /** * MyMoneyStorageSql create database * * @param url pseudo-URL of database to be opened * * @return true - creation successful * @return false - could not create * */ bool createDatabase(const QUrl &url) { Q_Q(MyMoneyStorageSql); int rc = true; if (!m_driver->requiresCreation()) return(true); // not needed for sqlite QString dbName = url.path().right(url.path().length() - 1); // remove separator slash if (!m_driver->canAutocreate()) { m_error = i18n("Automatic database creation for type %1 is not currently implemented.\n" "Please create database %2 manually", q->driverName(), dbName); return (false); } // create the database (only works for mysql and postgre at present) { // for this code block, see QSqlDatabase API re removeDatabase QSqlDatabase maindb = QSqlDatabase::addDatabase(q->driverName(), "main"); maindb.setDatabaseName(m_driver->defaultDbName()); maindb.setHostName(url.host()); maindb.setUserName(url.userName()); maindb.setPassword(url.password()); if (!maindb.open()) { throw MYMONEYEXCEPTION(QString::fromLatin1("opening database %1 in function %2") .arg(maindb.databaseName()).arg(Q_FUNC_INFO)); } else { QSqlQuery qm(maindb); qm.exec(QString::fromLatin1("PRAGMA key = '%1'").arg(q->password())); QString qs = m_driver->createDbString(dbName) + ';'; if (!qm.exec(qs)) { // krazy:exclude=crashy buildError(qm, Q_FUNC_INFO, i18n("Error in create database %1; do you have create permissions?", dbName), &maindb); rc = false; } maindb.close(); } } QSqlDatabase::removeDatabase("main"); return (rc); } int upgradeDb() { Q_Q(MyMoneyStorageSql); //signalProgress(0, 1, QObject::tr("Upgrading database...")); QSqlQuery query(*q); query.prepare("SELECT version FROM kmmFileInfo;"); if (!query.exec() || !query.next()) { // krazy:exclude=crashy if (!m_newDatabase) { buildError(query, Q_FUNC_INFO, "Error retrieving file info (version)"); return(1); } else { m_dbVersion = m_db.currentVersion(); m_storage->setFileFixVersion(m_storage->currentFixVersion()); QSqlQuery query2(*q); query2.prepare("UPDATE kmmFileInfo SET version = :version, \ fixLevel = :fixLevel;"); query2.bindValue(":version", m_dbVersion); query2.bindValue(":fixLevel", m_storage->currentFixVersion()); if (!query2.exec()) { // krazy:exclude=crashy buildError(query2, Q_FUNC_INFO, "Error updating file info(version)"); return(1); } return (0); } } // prior to dbv6, 'version' format was 'dbversion.fixLevel+1' // as of dbv6, these are separate fields QString version = query.value(0).toString(); if (version.contains('.')) { m_dbVersion = query.value(0).toString().section('.', 0, 0).toUInt(); m_storage->setFileFixVersion(query.value(0).toString().section('.', 1, 1).toUInt() - 1); } else { m_dbVersion = version.toUInt(); query.prepare("SELECT fixLevel FROM kmmFileInfo;"); if (!query.exec() || !query.next()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving file info (fixLevel)"); return(1); } m_storage->setFileFixVersion(query.value(0).toUInt()); } if (m_dbVersion == m_db.currentVersion()) return 0; int rc = 0; // Drop VIEWs QStringList lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (lowerTables.contains(tt.key().toLower())) { if (!query.exec("DROP VIEW " + tt.value().name() + ';')) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("dropping view %1").arg(tt.key())); } } while ((m_dbVersion < m_db.currentVersion()) && (rc == 0)) { switch (m_dbVersion) { case 0: if ((rc = upgradeToV1()) != 0) return (1); ++m_dbVersion; break; case 1: if ((rc = upgradeToV2()) != 0) return (1); ++m_dbVersion; break; case 2: if ((rc = upgradeToV3()) != 0) return (1); ++m_dbVersion; break; case 3: if ((rc = upgradeToV4()) != 0) return (1); ++m_dbVersion; break; case 4: if ((rc = upgradeToV5()) != 0) return (1); ++m_dbVersion; break; case 5: if ((rc = upgradeToV6()) != 0) return (1); ++m_dbVersion; break; case 6: if ((rc = upgradeToV7()) != 0) return (1); ++m_dbVersion; break; case 7: if ((rc = upgradeToV8()) != 0) return (1); ++m_dbVersion; break; case 8: if ((rc = upgradeToV9()) != 0) return (1); ++m_dbVersion; break; case 9: if ((rc = upgradeToV10()) != 0) return (1); ++m_dbVersion; break; case 10: if ((rc = upgradeToV11()) != 0) return (1); ++m_dbVersion; break; case 11: if ((rc = upgradeToV12()) != 0) return (1); ++m_dbVersion; break; default: qWarning("Unknown version number in database - %d", m_dbVersion); } } // restore VIEWs lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { if (!query.exec(tt.value().createString())) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating view %1").arg(tt.key())); } } // write updated version to DB //setVersion(QString("%1.%2").arg(m_dbVersion).arg(m_minorVersion)) query.prepare(QString("UPDATE kmmFileInfo SET version = :version;")); query.bindValue(":version", m_dbVersion); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating db version"); return (1); } //signalProgress(-1,-1); return (0); } int upgradeToV1() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // change kmmSplits pkey to (transactionId, splitId) if (!query.exec("ALTER TABLE kmmSplits ADD PRIMARY KEY (transactionId, splitId);")) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating kmmSplits pkey"); return (1); } // change kmmSplits alter checkNumber varchar(32) if (!query.exec(m_db.m_tables["kmmSplits"].modifyColumnString(m_driver, "checkNumber", // krazy:exclude=crashy MyMoneyDbColumn("checkNumber", "varchar(32)")))) { buildError(query, Q_FUNC_INFO, "Error expanding kmmSplits.checkNumber"); return (1); } // change kmmSplits add postDate datetime if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); // initialize it to same value as transaction (do it the long way round) query.prepare("SELECT id, postDate FROM kmmTransactions WHERE txType = 'N';"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error priming kmmSplits.postDate"); return (1); } QMap tids; while (query.next()) tids[query.value(0).toString()] = query.value(1).toDateTime(); QMap::ConstIterator it; for (it = tids.constBegin(); it != tids.constEnd(); ++it) { query.prepare("UPDATE kmmSplits SET postDate=:postDate WHERE transactionId = :id;"); query.bindValue(":postDate", it.value().toString(Qt::ISODate)); query.bindValue(":id", it.key()); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "priming kmmSplits.postDate"); return(1); } } // add index to kmmKeyValuePairs to (kvpType,kvpId) QStringList list; list << "kvpType" << "kvpId"; if (!query.exec(MyMoneyDbIndex("kmmKeyValuePairs", "kmmKVPtype_id", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmKeyValuePairs index"); return (1); } // add index to kmmSplits to (accountId, txType) list.clear(); list << "accountId" << "txType"; if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmSplitsaccount_type", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index"); return (1); } // change kmmSchedulePaymentHistory pkey to (schedId, payDate) if (!query.exec("ALTER TABLE kmmSchedulePaymentHistory ADD PRIMARY KEY (schedId, payDate);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmSchedulePaymentHistory pkey"); return (1); } // change kmmPrices pkey to (fromId, toId, priceDate) if (!query.exec("ALTER TABLE kmmPrices ADD PRIMARY KEY (fromId, toId, priceDate);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmPrices pkey"); return (1); } // change kmmReportConfig pkey to (name) // There wasn't one previously, so no need to drop it. if (!query.exec("ALTER TABLE kmmReportConfig ADD PRIMARY KEY (name);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmReportConfig pkey"); return (1); } // change kmmFileInfo add budgets, hiBudgetId unsigned bigint // change kmmFileInfo add logonUser // change kmmFileInfo add logonAt datetime if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); // change kmmAccounts add transactionCount unsigned bigint as last field if (!alterTable(m_db.m_tables["kmmAccounts"], m_dbVersion)) return (1); // calculate the transaction counts. the application logic defines an account's tx count // in such a way as to count multiple splits in a tx which reference the same account as one. // this is the only way I can think of to do this which will work in sqlite too. // inefficient, but it only gets done once... // get a list of all accounts so we'll get a zero value for those without txs query.prepare("SELECT id FROM kmmAccounts"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving accounts for transaction counting"); return(1); } while (query.next()) { m_transactionCountMap[query.value(0).toString()] = 0; } query.prepare("SELECT accountId, transactionId FROM kmmSplits WHERE txType = 'N' ORDER BY 1, 2"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving splits for transaction counting"); return(1); } QString lastAcc, lastTx; while (query.next()) { QString thisAcc = query.value(0).toString(); QString thisTx = query.value(1).toString(); if ((thisAcc != lastAcc) || (thisTx != lastTx)) ++m_transactionCountMap[thisAcc]; lastAcc = thisAcc; lastTx = thisTx; } QHash::ConstIterator itm; query.prepare("UPDATE kmmAccounts SET transactionCount = :txCount WHERE id = :id;"); for (itm = m_transactionCountMap.constBegin(); itm != m_transactionCountMap.constEnd(); ++itm) { query.bindValue(":txCount", QString::number(itm.value())); query.bindValue(":id", itm.key()); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating transaction count"); return (1); } } m_transactionCountMap.clear(); return (0); } int upgradeToV2() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); // change kmmSplits add price, priceFormatted fields if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); return (0); } int upgradeToV3() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSchedules - add occurrenceMultiplier // The default value is given here to populate the column. if (!query.exec("ALTER TABLE kmmSchedules ADD COLUMN " + MyMoneyDbIntColumn("occurenceMultiplier", MyMoneyDbIntColumn::SMALL, false, false, true) .generateDDL(m_driver) + " DEFAULT 0;")) { buildError(query, Q_FUNC_INFO, "Error adding kmmSchedules.occurenceMultiplier"); return (1); } //The default is less than any useful value, so as each schedule is hit, it will update //itself to the appropriate value. return 0; } int upgradeToV4() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSplits - add index on transactionId + splitId QStringList list; list << "transactionId" << "splitId"; if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmTx_Split", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index on (transactionId, splitId)"); return (1); } return 0; } int upgradeToV5() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); // kmmSplits - add bankId if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); //kmmPayees - add columns "notes" "defaultAccountId" "matchData" "matchIgnoreCase" "matchKeys"; if (!alterTable(m_db.m_tables["kmmPayees"], m_dbVersion)) return (1); // kmmReportConfig - drop primary key on name since duplicate names are allowed if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion)) return (1); //} return 0; } int upgradeToV6() { Q_Q(MyMoneyStorageSql); q->startCommitUnit(Q_FUNC_INFO); QSqlQuery query(*q); // kmmFileInfo - add fixLevel if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); // upgrade Mysql to InnoDB transaction-safe engine // the following is not a good way to test for mysql - think of a better way if (!m_driver->tableOptionString().isEmpty()) { for (QMap::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) { if (!query.exec(QString("ALTER TABLE %1 ENGINE = InnoDB;").arg(tt.value().name()))) { buildError(query, Q_FUNC_INFO, "Error updating to InnoDB"); return (1); } } } // the alterTable function really doesn't work too well // with adding a new column which is also to be primary key // so add the column first if (!query.exec("ALTER TABLE kmmReportConfig ADD COLUMN " + MyMoneyDbColumn("id", "varchar(32)").generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "adding id to report table"); return(1); } QMap reportList = q->fetchReports(); // the V5 database allowed lots of duplicate reports with no // way to distinguish between them. The fetchReports call // will have effectively removed all duplicates // so we now delete from the db and re-write them if (!query.exec("DELETE FROM kmmReportConfig;")) { buildError(query, Q_FUNC_INFO, "Error deleting reports"); return (1); } // add unique id to reports table if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion)) return(1); QMap::const_iterator it_r; for (it_r = reportList.constBegin(); it_r != reportList.constEnd(); ++it_r) { MyMoneyReport r = *it_r; query.prepare(m_db.m_tables["kmmReportConfig"].insertString()); writeReport(*it_r, query); } q->endCommitUnit(Q_FUNC_INFO); return 0; } int upgradeToV7() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); // add tags support // kmmFileInfo - add tags and hiTagId if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); m_tags = 0; return 0; } int upgradeToV8() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); // Added onlineJobs and payeeIdentifier if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); return 0; } int upgradeToV9() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); // kmmSplits - add bankId if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); return 0; } int upgradeToV10() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); if (!alterTable(m_db.m_tables["kmmPayeesPayeeIdentifier"], m_dbVersion)) return (1); if (!alterTable(m_db.m_tables["kmmAccountsPayeeIdentifier"], m_dbVersion)) return (1); return 0; } int upgradeToV11() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); // add column roundingMethodCol to kmmSecurities if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion)) return 1; // add column pricePrecision to kmmCurrencies if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion)) return 1; return 0; } int upgradeToV12() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); switch(haveColumnInTable(QLatin1String("kmmSchedules"), QLatin1String("lastDayInMonth"))) { case -1: return 1; case 1: // column exists, nothing to do break; case 0: // need update of kmmSchedules // add column lastDayInMonth. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmSchedules"], m_dbVersion-1)) return 1; break; } switch(haveColumnInTable(QLatin1String("kmmSecurities"), QLatin1String("roundingMethod"))) { case -1: return 1; case 1: // column exists, nothing to do break; case 0: // need update of kmmSecurities and kmmCurrencies // add column roundingMethodCol to kmmSecurities. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion-1)) return 1; // add column pricePrecision to kmmCurrencies. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion-1)) return 1; break; } return 0; } int createTables() { Q_Q(MyMoneyStorageSql); // check tables, create if required // convert everything to lower case, since SQL standard is case insensitive // table and column names (when not delimited), but some DBMSs disagree. QStringList lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { createTable(tt.value()); } } QSqlQuery query(*q); for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { if (!query.exec(tt.value().createString())) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating view %1").arg(tt.key())); } } // The columns to store version info changed with version 6. Prior versions are not supported here but an error is prevented and // an old behaviour is used: call upgradeDb(). m_dbVersion = m_db.currentVersion(); if (m_dbVersion >= 6) { // create the fileinfo stuff if it does not exist query.prepare("SELECT count(*) FROM kmmFileInfo;"); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTIONSQL("checking fileinfo"); // krazy:exclude=crashy if (query.value(0).toInt() == 0) { query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version, fixLevel) VALUES(?,?);")); query.bindValue(0, m_dbVersion); query.bindValue(1, m_storage->fileFixVersion()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("Saving database version")); } } return upgradeDb(); } void createTable(const MyMoneyDbTable& t, int version = std::numeric_limits::max()) { Q_Q(MyMoneyStorageSql); // create the tables QStringList ql = t.generateCreateSQL(m_driver, version).split('\n', QString::SkipEmptyParts); QSqlQuery query(*q); foreach (const QString& i, ql) { if (!query.exec(i)) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating table/index %1").arg(t.name())); } } bool alterTable(const MyMoneyDbTable& t, int fromVersion) { Q_Q(MyMoneyStorageSql); const int toVersion = fromVersion + 1; QString tempTableName = t.name(); tempTableName.replace("kmm", "kmmtmp"); QSqlQuery query(*q); // drop primary key if it has one (and driver supports it) if (t.hasPrimaryKey(fromVersion)) { QString dropString = m_driver->dropPrimaryKeyString(t.name()); if (!dropString.isEmpty()) { if (!query.exec(dropString)) { buildError(query, Q_FUNC_INFO, QString("Error dropping old primary key from %1").arg(t.name())); return false; } } } for (MyMoneyDbTable::index_iterator i = t.indexBegin(); i != t.indexEnd(); ++i) { QString indexName = t.name() + '_' + i->name() + "_idx"; if (!query.exec(m_driver->dropIndexString(t.name(), indexName))) { buildError(query, Q_FUNC_INFO, QString("Error dropping index from %1").arg(t.name())); return false; } } if (!query.exec(QString("ALTER TABLE " + t.name() + " RENAME TO " + tempTableName + ';'))) { buildError(query, Q_FUNC_INFO, QString("Error renaming table %1").arg(t.name())); return false; } createTable(t, toVersion); if (q->getRecCount(tempTableName) > 0) { query.prepare(QString("INSERT INTO " + t.name() + " (" + t.columnList(fromVersion, true) + ") SELECT " + t.columnList(fromVersion, false) + " FROM " + tempTableName + ';')); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, QString("Error inserting into new table %1").arg(t.name())); return false; } } if (!query.exec(QString("DROP TABLE " + tempTableName + ';'))) { buildError(query, Q_FUNC_INFO, QString("Error dropping old table %1").arg(t.name())); return false; } return true; } void clean() { Q_Q(MyMoneyStorageSql); // delete all existing records QMap::ConstIterator it = m_db.tableBegin(); QSqlQuery query(*q); while (it != m_db.tableEnd()) { query.prepare(QString("DELETE from %1;").arg(it.key())); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("cleaning database"); // krazy:exclude=crashy ++it; } } int isEmpty() { Q_Q(MyMoneyStorageSql); // check all tables are empty QMap::ConstIterator tt = m_db.tableBegin(); int recordCount = 0; QSqlQuery query(*q); while ((tt != m_db.tableEnd()) && (recordCount == 0)) { query.prepare(QString("select count(*) from %1;").arg((*tt).name())); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("getting record count"); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTIONSQL("retrieving record count"); recordCount += query.value(0).toInt(); ++tt; } // a fresh created database contains at least one record (see createTables()) in // the kmmFileInfo table providing file and fix version. So we report empty // even if there is a recordCount of 1 if (recordCount > 1) { return -1; // not empty } else { return 0; } } // for bug 252841 QStringList tables(QSql::TableType tt) { Q_Q(MyMoneyStorageSql); return (m_driver->tables(tt, static_cast(*q))); } //! Returns 1 in case the @a column exists in @a table, 0 if not. In case of error, -1 is returned. int haveColumnInTable(const QString& table, const QString& column) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); QString cmd = QString("SELECT * FROM %1 LIMIT 1").arg(table); if(!query.exec(cmd)) { buildError(query, Q_FUNC_INFO, QString("Error detecting if %1 exists in %2").arg(column).arg(table)); return -1; } QSqlRecord rec = query.record(); return (rec.indexOf(column) != -1) ? 1 : 0; } /** * @brief Ensure the storagePlugin with iid was setup * * @throws MyMoneyException in case of an error which makes the use * of the plugin unavailable. */ bool setupStoragePlugin(QString iid) { Q_Q(MyMoneyStorageSql); // setupDatabase has to be called every time because this simple technique to check if was updated already // does not work if a user opens another file // also the setup is removed if the current database transaction is rolled back if (iid.isEmpty() /*|| m_loadedStoragePlugins.contains(iid)*/) return false; MyMoneyDbTransaction t(*q, Q_FUNC_INFO); auto rc = false; if (iid == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) rc = setupIBANBIC(*q); else if (iid == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) rc = setupNationalAccount(*q); else if (iid == sepaOnlineTransferImpl::name()) rc = setupSepaOnlineTransfer(*q); else rc = false; return rc; } bool setupIBANBIC(QSqlDatabase connection) { auto iid = QLatin1String("org.kmymoney.payeeIdentifier.ibanbic.sqlStoragePlugin"); // Get current version QSqlQuery query = QSqlQuery(connection); query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?"); query.bindValue(0, iid); if (!query.exec()) { qWarning("Could not execute query for ibanBicStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } int currentVersion = 0; if (query.next()) currentVersion = query.value(0).toInt(); // Create database in it's most recent version if version is 0 // (version 0 means the database was not installed) if (currentVersion == 0) { // If the database is recreated the table may be still there. So drop it if needed. No error handling needed // as this step is not necessary - only the creation is important. if (!query.exec("DROP TABLE IF EXISTS kmmIbanBic;")) return false; if (!query.exec( "CREATE TABLE kmmIbanBic (" " id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmPayeeIdentifier( id ) ON DELETE CASCADE ON UPDATE CASCADE," " iban varchar(32)," " bic char(11) CHECK(length(bic) = 11 OR bic IS NULL)," " name text" " );" )) { qWarning("Could not create table for ibanBicStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)"); query.bindValue(0, iid); query.bindValue(1, 1); query.bindValue(2, 0); query.bindValue(3, "DROP TABLE kmmIbanBic;"); if (query.exec()) return true; qWarning("Could not save plugin info for ibanBicStoragePlugin (%s): %s", qPrintable(iid), qPrintable(query.lastError().text())); return false; } // Check if version is valid with this plugin switch (currentVersion) { case 1: return true; } return false; } bool setupNationalAccount(QSqlDatabase connection) { auto iid = QLatin1String("org.kmymoney.payeeIdentifier.nationalAccount.sqlStoragePlugin"); // Get current version QSqlQuery query = QSqlQuery(connection); query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?"); query.bindValue(0, iid); if (!query.exec()) { qWarning("Could not execute query for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } int currentVersion = 0; if (query.next()) currentVersion = query.value(0).toInt(); // Create database in it's most recent version if version is 0 // (version 0 means the database was not installed) if (currentVersion == 0) { // If the database is recreated the table may be still there. So drop it if needed. No error handling needed // as this step is not necessary - only the creation is important. if (!query.exec("DROP TABLE IF EXISTS kmmNationalAccountNumber;")) return false; if (!query.exec( "CREATE TABLE kmmNationalAccountNumber (" " id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmPayeeIdentifier( id ) ON DELETE CASCADE ON UPDATE CASCADE," " countryCode varchar(3)," " accountNumber TEXT," " bankCode TEXT," " name TEXT" " );" )) { qWarning("Could not create table for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)"); query.bindValue(0, iid); query.bindValue(1, 1); query.bindValue(2, 0); query.bindValue(3, "DROP TABLE kmmNationalAccountNumber;"); if (query.exec()) return true; qWarning("Could not save plugin info for nationalAccountStoragePlugin (%s): %s", qPrintable(iid), qPrintable(query.lastError().text())); return false; } // Check if version is valid with this plugin switch (currentVersion) { case 1: return true; } return false; } bool setupSepaOnlineTransfer(QSqlDatabase connection) { auto iid = QLatin1String("org.kmymoney.creditTransfer.sepa.sqlStoragePlugin"); // Get current version QSqlQuery query = QSqlQuery(connection); query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?"); query.bindValue(0, iid); if (!query.exec()) { qWarning("Could not execute query for sepaStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } int currentVersion = 0; if (query.next()) currentVersion = query.value(0).toInt(); // Create database in it's most recent version if version is 0 // (version 0 means the database was not installed) if (currentVersion <= 1) { // If the database is recreated the table may be still there. So drop it if needed. No error handling needed // as this step is not necessary - only the creation is important. if (!query.exec("DROP TABLE IF EXISTS kmmSepaOrders;")) return false; if (!query.exec( "CREATE TABLE kmmSepaOrders (" " id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmOnlineJobs( id ) ON UPDATE CASCADE ON DELETE CASCADE," " originAccount varchar(32) REFERENCES kmmAccounts( id ) ON UPDATE CASCADE ON DELETE SET NULL," " value text," " purpose text," " endToEndReference varchar(35)," " beneficiaryName varchar(27)," " beneficiaryIban varchar(32)," " beneficiaryBic char(11)," " textKey int," " subTextKey int" " );" )) { qWarning("Error while creating table kmmSepaOrders: %s", qPrintable(query.lastError().text())); return false; } query.prepare("DELETE FROM kmmPluginInfo WHERE iid = ?;"); query.bindValue(0, iid); query.exec(); query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)"); query.bindValue(0, iid); query.bindValue(1, 2); query.bindValue(2, 0); query.bindValue(3, "DROP TABLE kmmSepaOrders;"); if (query.exec()) return true; qWarning("Error while inserting kmmPluginInfo for '%s': %s", qPrintable(iid), qPrintable(query.lastError().text())); return false; } // Check if version is valid with this plugin switch (currentVersion) { case 2: return true; } return false; } bool actOnIBANBICObjectInSQL(SQLAction action, const payeeIdentifier &obj) { payeeIdentifierTyped payeeIdentifier = payeeIdentifierTyped(obj); Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); auto writeQuery = [&]() { query.bindValue(":id", obj.idString()); query.bindValue(":iban", payeeIdentifier->electronicIban()); const auto bic = payeeIdentifier->fullStoredBic(); query.bindValue(":bic", (bic.isEmpty()) ? QVariant(QVariant::String) : bic); query.bindValue(":name", payeeIdentifier->ownerName()); if (!query.exec()) { // krazy:exclude=crashy qWarning("Error while saving ibanbic data for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmIbanBic " " ( id, iban, bic, name )" " VALUES( :id, :iban, :bic, :name ) " ); return writeQuery(); case SQLAction::Modify: query.prepare("UPDATE kmmIbanBic SET iban = :iban, bic = :bic, name = :name WHERE id = :id;"); return writeQuery(); case SQLAction::Remove: query.prepare("DELETE FROM kmmIbanBic WHERE id = ?;"); query.bindValue(0, obj.idString()); if (!query.exec()) { qWarning("Error while deleting ibanbic data '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; } return false; } bool actOnNationalAccountObjectInSQL(SQLAction action, const payeeIdentifier &obj) { payeeIdentifierTyped payeeIdentifier = payeeIdentifierTyped(obj); Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); auto writeQuery = [&]() { query.bindValue(":id", obj.idString()); query.bindValue(":countryCode", payeeIdentifier->country()); query.bindValue(":accountNumber", payeeIdentifier->accountNumber()); query.bindValue(":bankCode", (payeeIdentifier->bankCode().isEmpty()) ? QVariant(QVariant::String) : payeeIdentifier->bankCode()); query.bindValue(":name", payeeIdentifier->ownerName()); if (!query.exec()) { // krazy:exclude=crashy qWarning("Error while saving national account number for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmNationalAccountNumber " " ( id, countryCode, accountNumber, bankCode, name )" " VALUES( :id, :countryCode, :accountNumber, :bankCode, :name ) " ); return writeQuery(); case SQLAction::Modify: query.prepare("UPDATE kmmNationalAccountNumber SET countryCode = :countryCode, accountNumber = :accountNumber, bankCode = :bankCode, name = :name WHERE id = :id;"); return writeQuery(); case SQLAction::Remove: query.prepare("DELETE FROM kmmNationalAccountNumber WHERE id = ?;"); query.bindValue(0, obj.idString()); if (!query.exec()) { qWarning("Error while deleting national account number '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; } return false; } bool actOnSepaOnlineTransferObjectInSQL(SQLAction action, const onlineTask &obj, const QString& id) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); const auto& task = dynamic_cast(obj); auto bindValuesToQuery = [&]() { auto value = task.value().toString(); if (value.isEmpty()) { value = QStringLiteral("0"); } query.bindValue(":id", id); query.bindValue(":originAccount", task.responsibleAccount()); query.bindValue(":value", value); query.bindValue(":purpose", task.purpose()); query.bindValue(":endToEndReference", (task.endToEndReference().isEmpty()) ? QVariant() : QVariant::fromValue(task.endToEndReference())); query.bindValue(":beneficiaryName", task.beneficiaryTyped().ownerName()); query.bindValue(":beneficiaryIban", task.beneficiaryTyped().electronicIban()); query.bindValue(":beneficiaryBic", (task.beneficiaryTyped().storedBic().isEmpty()) ? QVariant() : QVariant::fromValue(task.beneficiaryTyped().storedBic())); query.bindValue(":textKey", task.textKey()); query.bindValue(":subTextKey", task.subTextKey()); }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmSepaOrders (" " id, originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, " " beneficiaryBic, textKey, subTextKey) " " VALUES( :id, :originAccount, :value, :purpose, :endToEndReference, :beneficiaryName, :beneficiaryIban, " " :beneficiaryBic, :textKey, :subTextKey ) " ); bindValuesToQuery(); if (!query.exec()) { qWarning("Error while saving sepa order '%s': %s", qPrintable(id), qPrintable(query.lastError().text())); return false; } return true; case SQLAction::Modify: query.prepare( "UPDATE kmmSepaOrders SET" " originAccount = :originAccount," " value = :value," " purpose = :purpose," " endToEndReference = :endToEndReference," " beneficiaryName = :beneficiaryName," " beneficiaryIban = :beneficiaryIban," " beneficiaryBic = :beneficiaryBic," " textKey = :textKey," " subTextKey = :subTextKey " " WHERE id = :id"); bindValuesToQuery(); if (!query.exec()) { qWarning("Could not modify sepaOnlineTransfer '%s': %s", qPrintable(id), qPrintable(query.lastError().text())); return false; } return true; case SQLAction::Remove: query.prepare("DELETE FROM kmmSepaOrders WHERE id = ?"); query.bindValue(0, id); return query.exec(); } return false; } void actOnPayeeIdentifierObjectInSQL(SQLAction action, const payeeIdentifier& obj) { setupStoragePlugin(obj->payeeIdentifierId()); auto isSuccessfull = false; if (obj->payeeIdentifierId() == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) isSuccessfull = actOnIBANBICObjectInSQL(action, obj); else if (obj->payeeIdentifierId() == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) isSuccessfull = actOnNationalAccountObjectInSQL(action, obj); if (!isSuccessfull) { switch (action) { case SQLAction::Save: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(obj.idString())); case SQLAction::Modify: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(obj.idString())); case SQLAction::Remove: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(obj.idString())); } } } void actOnOnlineJobInSQL(SQLAction action, const onlineTask& obj, const QString& id) { setupStoragePlugin(obj.taskName()); auto isSuccessfull = false; if (obj.taskName() == sepaOnlineTransferImpl::name()) isSuccessfull = actOnSepaOnlineTransferObjectInSQL(action, obj, id); if (!isSuccessfull) { switch (action) { case SQLAction::Save: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(id)); case SQLAction::Modify: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(id)); case SQLAction::Remove: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(id)); } } } payeeIdentifierData* createIBANBICObject(QSqlDatabase db, const QString& identId) const { QSqlQuery query(db); query.prepare("SELECT iban, bic, name FROM kmmIbanBic WHERE id = ?;"); query.bindValue(0, identId); if (!query.exec() || !query.next()) { qWarning("Could load iban bic identifier from database"); return nullptr; } payeeIdentifiers::ibanBic *const ident = new payeeIdentifiers::ibanBic; ident->setIban(query.value(0).toString()); ident->setBic(query.value(1).toString()); ident->setOwnerName(query.value(2).toString()); return ident; } payeeIdentifierData* createNationalAccountObject(QSqlDatabase db, const QString& identId) const { QSqlQuery query(db); query.prepare("SELECT countryCode, accountNumber, bankCode, name FROM kmmNationalAccountNumber WHERE id = ?;"); query.bindValue(0, identId); if (!query.exec() || !query.next()) { qWarning("Could load national account number from database"); return nullptr; } payeeIdentifiers::nationalAccount *const ident = new payeeIdentifiers::nationalAccount; ident->setCountry(query.value(0).toString()); ident->setAccountNumber(query.value(1).toString()); ident->setBankCode(query.value(2).toString()); ident->setOwnerName(query.value(3).toString()); return ident; } payeeIdentifier createPayeeIdentifierObject(QSqlDatabase db, const QString& identifierType, const QString& identifierId) const { payeeIdentifierData* identData = nullptr; if (identifierType == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) identData = createIBANBICObject(db, identifierId); else if (identifierType == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) identData = createNationalAccountObject(db, identifierId); return payeeIdentifier(identifierId, identData); } onlineTask* createSepaOnlineTransferObject(QSqlDatabase connection, const QString& onlineJobId) const { Q_ASSERT(!onlineJobId.isEmpty()); Q_ASSERT(connection.isOpen()); QSqlQuery query = QSqlQuery( "SELECT originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, " " beneficiaryBic, textKey, subTextKey FROM kmmSepaOrders WHERE id = ?", connection ); query.bindValue(0, onlineJobId); if (query.exec() && query.next()) { sepaOnlineTransferImpl* task = new sepaOnlineTransferImpl(); task->setOriginAccount(query.value(0).toString()); task->setValue(MyMoneyMoney(query.value(1).toString())); task->setPurpose(query.value(2).toString()); task->setEndToEndReference(query.value(3).toString()); task->setTextKey(query.value(7).toUInt()); task->setSubTextKey(query.value(8).toUInt()); payeeIdentifiers::ibanBic beneficiary; beneficiary.setOwnerName(query.value(4).toString()); beneficiary.setIban(query.value(5).toString()); beneficiary.setBic(query.value(6).toString()); task->setBeneficiary(beneficiary); return task; } return nullptr; } onlineTask* createOnlineTaskObject(const QString& iid, const QString& onlineTaskId, QSqlDatabase connection) const { onlineTask* taskOnline = nullptr; if (iid == sepaOnlineTransferImpl::name()) { // @todo This is probably memory leak but for now it works alike to original code onlineJobAdministration::instance()->registerOnlineTask(new sepaOnlineTransferImpl); taskOnline = createSepaOnlineTransferObject(connection, onlineTaskId); } if (!taskOnline) qWarning("In the file is a onlineTask for which I could not find the plugin ('%s')", qPrintable(iid)); return taskOnline; } void alert(QString s) const // FIXME: remove... { qDebug() << s; } void signalProgress(qint64 current, qint64 total, const QString& msg) const { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void signalProgress(qint64 current, qint64 total) const { signalProgress(current, total, QString()); } template ulong getNextId(const QString& table, const QString& id, const int prefixLength) const { Q_CHECK_PTR(cache); if (this->*cache == 0) { MyMoneyStorageSqlPrivate* nonConstThis = const_cast(this); nonConstThis->*cache = 1 + nonConstThis->highestNumberFromIdString(table, id, prefixLength); } Q_ASSERT(this->*cache > 0); // everything else is never a valid id return this->*cache; } //void startCommitUnit (const QString& callingFunction); //void endCommitUnit (const QString& callingFunction); //void cancelCommitUnit (const QString& callingFunction); MyMoneyStorageSql *q_ptr; // data QExplicitlySharedDataPointer m_driver; MyMoneyDbDef m_db; uint m_dbVersion; MyMoneyStorageMgr *m_storage; // input options bool m_loadAll; // preload all data bool m_override; // override open if already in use // error message QString m_error; // record counts ulong m_institutions; ulong m_accounts; ulong m_payees; ulong m_tags; ulong m_transactions; ulong m_splits; ulong m_securities; ulong m_prices; ulong m_currencies; ulong m_schedules; ulong m_reports; ulong m_kvps; ulong m_budgets; ulong m_onlineJobs; ulong m_payeeIdentifier; // Cache for next id to use // value 0 means data is not available and has to be loaded from the database ulong m_hiIdInstitutions; ulong m_hiIdPayees; ulong m_hiIdTags; ulong m_hiIdAccounts; ulong m_hiIdTransactions; ulong m_hiIdSchedules; ulong m_hiIdSecurities; ulong m_hiIdReports; ulong m_hiIdBudgets; ulong m_hiIdOnlineJobs; ulong m_hiIdPayeeIdentifier; ulong m_hiIdCostCenter; // encrypt option - usage TBD QString m_encryptData; /** * This variable is used to suppress status messages except during * initial data load and final write */ bool m_displayStatus; /** The following keeps track of commitment units (known as transactions in SQL * though it would be confusing to use that term within KMM). It is implemented * as a stack for debug purposes. Long term, probably a count would suffice */ QStack m_commitUnitStack; /** * This member variable is used to preload transactions for preferred accounts */ MyMoneyTransactionFilter m_preferred; /** * This member variable is used because reading prices from a file uses the 'add...' function rather than a * 'load...' function which other objects use. Having this variable allows us to avoid needing to check the * database to see if this really is a new or modified price */ bool m_readingPrices; /** * This member variable holds a map of transaction counts per account, indexed by * the account id. It is used * to avoid having to scan all transactions whenever a count is needed. It should * probably be moved into the MyMoneyAccount object; maybe we will do that once * the database code has been properly checked out */ QHash m_transactionCountMap; /** * These member variables hold the user name and date/time of logon */ QString m_logonUser; QDateTime m_logonAt; QDate m_txPostDate; // FIXME: remove when Tom puts date into split object bool m_newDatabase; /** * This member keeps the current precision to be used fro prices. * @sa setPrecision() */ static int m_precision; /** * This member keeps the current start date used for transaction retrieval. * @sa setStartDate() */ static QDate m_startDate; /** * */ QSet m_loadedStoragePlugins; void (*m_progressCallback)(int, int, const QString&); }; #endif diff --git a/kmymoney/plugins/xml/tests/mymoneyxmlcontenthandler-test.cpp b/kmymoney/plugins/xml/tests/mymoneyxmlcontenthandler-test.cpp index d81b8633e..9695bea40 100644 --- a/kmymoney/plugins/xml/tests/mymoneyxmlcontenthandler-test.cpp +++ b/kmymoney/plugins/xml/tests/mymoneyxmlcontenthandler-test.cpp @@ -1,1835 +1,1835 @@ /* * Copyright 2002-2017 Thomas Baumgart * Copyright 2003 Michael Edwardes * Copyright 2004-2006 Ace Jones * Copyright 2004 Kevin Tambascio * Copyright 2009-2014 Cristian Oneț * Copyright 2012 Alessandro Russo * Copyright 2016 Christian Dávid * Copyright 2018 Ł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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyxmlcontenthandler-test.h" #include #include "../mymoneystoragexml.cpp" #include "mymoneyobject_p.h" #include "mymoneyobject.h" #include "mymoneyexception.h" QTEST_GUILESS_MAIN(MyMoneyXmlContentHandlerTest) class TestMyMoneyObjectPrivate : public MyMoneyObjectPrivate { }; class TestMyMoneyObject : public MyMoneyObject { Q_DECLARE_PRIVATE(TestMyMoneyObject) public: TestMyMoneyObject() : MyMoneyObject(*new MyMoneyObjectPrivate) {} TestMyMoneyObject(const QString &id) : MyMoneyObject(*new MyMoneyObjectPrivate, id) { } TestMyMoneyObject(const TestMyMoneyObject &other) : MyMoneyObject(*new MyMoneyObjectPrivate(*other.d_func()), other.id()) { } TestMyMoneyObject(TestMyMoneyObject &&other) : TestMyMoneyObject() { swap(*this, other); } TestMyMoneyObject & operator=(TestMyMoneyObject other); friend void swap(TestMyMoneyObject& first, TestMyMoneyObject& second); ~TestMyMoneyObject() final override { } bool hasReferenceTo(const QString&) const final override { return false; } static TestMyMoneyObject readBaseXML(const QDomElement &node, bool forceId = true) { TestMyMoneyObject obj(node.attribute(QStringLiteral("id"))); if (obj.id().isEmpty() && forceId) throw MYMONEYEXCEPTION_CSTRING("Node has no ID"); return obj; } }; void swap(TestMyMoneyObject& first, TestMyMoneyObject& second) { using std::swap; swap(first.d_ptr, second.d_ptr); } TestMyMoneyObject & TestMyMoneyObject::operator=(TestMyMoneyObject other) { swap(*this, other); return *this; } void MyMoneyXmlContentHandlerTest::readMyMoneyObject() { TestMyMoneyObject t; QString ref_ok = QString( "\n" "\n" " \n" " \n" "\n" ); QString ref_false1 = QString( "\n" "\n" " \n" " \n" "\n" ); QString ref_false2 = QString( "\n" "\n" " \n" " \n" "\n" ); QDomDocument doc; QDomElement node; // id="" but required doc.setContent(ref_false1); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject::readBaseXML(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } // id attribute missing but required doc.setContent(ref_false2); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject::readBaseXML(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } // id present doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject::readBaseXML(node); QVERIFY(t.id() == "T000000000000000001"); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } // id="" but not required doc.setContent(ref_false1); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject::readBaseXML(node, false); QVERIFY(t.id().isEmpty()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::readKeyValueContainer() { MyMoneyKeyValueContainer kvp; kvp.setValue("Key", "Value"); kvp.setValue("key", "value"); QString ref_ok( "\n" "\n" " \n" " \n" " \n" " \n" "\n"); QString ref_false( "\n" "\n" " \n" " \n" " \n" " \n" "\n"); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); // make sure, an empty node does not trigger an exception try { QDomElement e; MyMoneyKeyValueContainer k; MyMoneyXmlContentHandler::addToKeyValueContainer(k, e); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } try { MyMoneyKeyValueContainer k; MyMoneyXmlContentHandler::addToKeyValueContainer(k, node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { MyMoneyKeyValueContainer k; MyMoneyXmlContentHandler::addToKeyValueContainer(k, node); // QVERIFY(k.d_func()->m_kvp.count() == 2); to be enabled soon QVERIFY(k.value("key") == "Value"); QVERIFY(k.value("Key") == "value"); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::writeKeyValueContainer() { MyMoneyKeyValueContainer kvp; kvp.setValue("Key", "Value"); kvp.setValue("key", "value"); QDomDocument doc("TEST"); QDomElement el = doc.createElement("KVP-CONTAINER"); doc.appendChild(el); MyMoneyXmlContentHandler::writeKeyValueContainer(kvp, doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement kvpContainer = doc.documentElement(); QCOMPARE(kvpContainer.tagName(), QLatin1String("KVP-CONTAINER")); QCOMPARE(kvpContainer.childNodes().size(), 1); QVERIFY(kvpContainer.childNodes().at(0).isElement()); QDomElement keyValuePairs = kvpContainer.childNodes().at(0).toElement(); QCOMPARE(keyValuePairs.tagName(), QLatin1String("KEYVALUEPAIRS")); QCOMPARE(keyValuePairs.childNodes().size(), 2); QVERIFY(keyValuePairs.childNodes().at(0).isElement()); QDomElement keyValuePair1 = keyValuePairs.childNodes().at(0).toElement(); QCOMPARE(keyValuePair1.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair1.attribute("key"), QLatin1String("Key")); QCOMPARE(keyValuePair1.attribute("value"), QLatin1String("Value")); QCOMPARE(keyValuePair1.childNodes().size(), 0); QVERIFY(keyValuePairs.childNodes().at(1).isElement()); QDomElement keyValuePair2 = keyValuePairs.childNodes().at(1).toElement(); QCOMPARE(keyValuePair2.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair2.attribute("key"), QLatin1String("key")); QCOMPARE(keyValuePair2.attribute("value"), QLatin1String("value")); QCOMPARE(keyValuePair2.childNodes().size(), 0); } void MyMoneyXmlContentHandlerTest::readTransaction() { MyMoneyTransaction t; QString ref_ok = QString( "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ); QString ref_false = QString( "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); try { t = MyMoneyXmlContentHandler::readTransaction(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); t.setValue("key", "VALUE"); try { t = MyMoneyXmlContentHandler::readTransaction(node); QVERIFY(t.postDate() == QDate(2001, 12, 28)); QVERIFY(t.entryDate() == QDate(2003, 9, 29)); QVERIFY(t.id() == "T000000000000000001"); QVERIFY(t.memo() == "Wohnung:Miete"); QVERIFY(t.commodity() == "EUR"); QVERIFY(t.pairs().count() == 1); QVERIFY(t.value("key") == "value"); QVERIFY(t.splits().count() == 1); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::readTransactionEx() { MyMoneyTransaction t; QString ref_ok = QString( "\n" "\n" "\n" " \n" " \n" " \n" " \n" " \n" " &lt;CONTAINER>\n" " &lt;TRANSACTION postdate="2010-03-05" memo="UMBUCHUNG" id="" commodity="EUR" entrydate="2010-03-08" >\n" " &lt;SPLITS>\n" " &lt;SPLIT payee="P000010" reconciledate="" shares="125000/100" action="Transfer" bankid="" number="" reconcileflag="0" memo="UMBUCHUNG" value="125000/100" id="S0001" account="A000087" />\n" " &lt;SPLIT payee="P000010" reconciledate="" shares="-125000/100" action="" bankid="A000076-2010-03-05-b6850c0-1" number="" reconcileflag="0" memo="UMBUCHUNG" value="-125000/100" id="S0002" account="A000076" />\n" " &lt;/SPLITS>\n" " &lt;KEYVALUEPAIRS>\n" " &lt;PAIR key="Imported" value="true" />\n" " &lt;/KEYVALUEPAIRS>\n" " &lt;/TRANSACTION>\n" " &lt;/CONTAINER>\n" "\" />\n" " \n" " \n" " \n" " \n" " \n" "\n" "\n" ); QDomDocument doc; QDomElement node; doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { t = MyMoneyXmlContentHandler::readTransaction(node); QVERIFY(t.pairs().count() == 0); QVERIFY(t.splits().size() == 2); QVERIFY(t.splits()[0].pairs().count() == 3); QVERIFY(t.splits()[1].pairs().count() == 0); QVERIFY(t.splits()[0].isMatched()); MyMoneyTransaction ti = t.splits()[0].matchedTransaction(); QVERIFY(ti.pairs().count() == 1); QVERIFY(ti.isImported()); QVERIFY(ti.splits().count() == 2); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::writeTransaction() { MyMoneyTransaction t("T000000000000000001"); t.setPostDate(QDate(2001, 12, 28)); t.setEntryDate(QDate(2003, 9, 29)); t.setMemo("Wohnung:Miete"); t.setCommodity("EUR"); t.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); QList tagIdList; tagIdList << "G000001"; s.setTagIdList(tagIdList); s.setShares(MyMoneyMoney(96379, 100)); s.setValue(MyMoneyMoney(96379, 100)); s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)); s.setAccountId("A000076"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); s.setBankID("SPID"); t.addSplit(s); QDomDocument doc("TEST"); QDomElement el = doc.createElement("TRANSACTION-CONTAINER"); doc.appendChild(el); MyMoneyXmlContentHandler::writeTransaction(t, doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement transactionContainer = doc.documentElement(); QVERIFY(transactionContainer.isElement()); QCOMPARE(transactionContainer.tagName(), QLatin1String("TRANSACTION-CONTAINER")); QVERIFY(transactionContainer.childNodes().size() == 1); QVERIFY(transactionContainer.childNodes().at(0).isElement()); QDomElement transaction = transactionContainer.childNodes().at(0).toElement(); QCOMPARE(transaction.tagName(), QLatin1String("TRANSACTION")); QCOMPARE(transaction.attribute("id"), QLatin1String("T000000000000000001")); QCOMPARE(transaction.attribute("postdate"), QLatin1String("2001-12-28")); QCOMPARE(transaction.attribute("commodity"), QLatin1String("EUR")); QCOMPARE(transaction.attribute("memo"), QLatin1String("Wohnung:Miete")); QCOMPARE(transaction.attribute("entrydate"), QLatin1String("2003-09-29")); QCOMPARE(transaction.childNodes().size(), 2); QVERIFY(transaction.childNodes().at(0).isElement()); QDomElement splits = transaction.childNodes().at(0).toElement(); QCOMPARE(splits.tagName(), QLatin1String("SPLITS")); QCOMPARE(splits.childNodes().size(), 1); QVERIFY(splits.childNodes().at(0).isElement()); QDomElement split = splits.childNodes().at(0).toElement(); QCOMPARE(split.tagName(), QLatin1String("SPLIT")); QCOMPARE(split.attribute("id"), QLatin1String("S0001")); QCOMPARE(split.attribute("payee"), QLatin1String("P000001")); QCOMPARE(split.attribute("reconcileflag"), QLatin1String("2")); QCOMPARE(split.attribute("shares"), QLatin1String("96379/100")); QCOMPARE(split.attribute("reconciledate"), QString()); QCOMPARE(split.attribute("action"), QLatin1String("Withdrawal")); QCOMPARE(split.attribute("bankid"), QLatin1String("SPID")); QCOMPARE(split.attribute("account"), QLatin1String("A000076")); QCOMPARE(split.attribute("number"), QString()); QCOMPARE(split.attribute("value"), QLatin1String("96379/100")); QCOMPARE(split.attribute("memo"), QString()); QCOMPARE(split.childNodes().size(), 1); QVERIFY(split.childNodes().at(0).isElement()); QDomElement tag = split.childNodes().at(0).toElement(); QCOMPARE(tag.tagName(), QLatin1String("TAG")); QCOMPARE(tag.attribute("id"), QLatin1String("G000001")); QCOMPARE(tag.childNodes().size(), 0); QDomElement keyValuePairs = transaction.childNodes().at(1).toElement(); QCOMPARE(keyValuePairs.tagName(), QLatin1String("KEYVALUEPAIRS")); QCOMPARE(keyValuePairs.childNodes().size(), 1); QVERIFY(keyValuePairs.childNodes().at(0).isElement()); QDomElement keyValuePair1 = keyValuePairs.childNodes().at(0).toElement(); QCOMPARE(keyValuePair1.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair1.attribute("key"), QLatin1String("key")); QCOMPARE(keyValuePair1.attribute("value"), QLatin1String("value")); QCOMPARE(keyValuePair1.childNodes().size(), 0); } void MyMoneyXmlContentHandlerTest::readSplit() { MyMoneySplit s; QString ref_ok = QString( "\n" "\n" " \n" " \n" " \n" "\n"); QString ref_false = QString( "\n" "\n" " \n" " \n" "\n"); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); try { s = MyMoneyXmlContentHandler::readSplit(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { s = MyMoneyXmlContentHandler::readSplit(node); QCOMPARE(s.id().isEmpty(), true); QCOMPARE(s.payeeId(), QLatin1String("P000001")); QList tagIdList; tagIdList << QLatin1String("G000001"); QCOMPARE(s.tagIdList(), tagIdList); QCOMPARE(s.reconcileDate(), QDate()); QCOMPARE(s.shares(), MyMoneyMoney(96379, 100)); QCOMPARE(s.value(), MyMoneyMoney(96379, 1000)); QCOMPARE(s.number(), QLatin1String("124")); QCOMPARE(s.bankID(), QLatin1String("SPID")); QCOMPARE(s.reconcileFlag(), eMyMoney::Split::State::Reconciled); QCOMPARE(s.action(), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); QCOMPARE(s.accountId(), QLatin1String("A000076")); QCOMPARE(s.costCenterId(), QLatin1String("C000005")); QCOMPARE(s.memo(), QLatin1String("MyMemo")); } catch (const MyMoneyException &) { } } void MyMoneyXmlContentHandlerTest::writeSplit() { MyMoneySplit s; s.setPayeeId("P000001"); QList tagIdList; tagIdList << "G000001"; s.setTagIdList(tagIdList); s.setShares(MyMoneyMoney(96379, 100)); s.setValue(MyMoneyMoney(96379, 1000)); s.setAccountId("A000076"); s.setCostCenterId("C000005"); s.setNumber("124"); s.setBankID("SPID"); s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); QDomDocument doc("TEST"); QDomElement el = doc.createElement("SPLIT-CONTAINER"); doc.appendChild(el); MyMoneyXmlContentHandler::writeSplit(s, doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement splitContainer = doc.documentElement(); QCOMPARE(splitContainer.tagName(), QLatin1String("SPLIT-CONTAINER")); QCOMPARE(splitContainer.childNodes().size(), 1); QCOMPARE(splitContainer.childNodes().at(0).isElement(), true); QDomElement split = splitContainer.childNodes().at(0).toElement(); QCOMPARE(split.tagName(), QLatin1String("SPLIT")); QCOMPARE(split.attribute("payee"), QLatin1String("P000001")); QCOMPARE(split.attribute("reconcileflag"), QLatin1String("2")); QCOMPARE(split.attribute("shares"), QLatin1String("96379/100")); QCOMPARE(split.attribute("reconciledate"), QString()); QCOMPARE(split.attribute("action"), QLatin1String("Deposit")); QCOMPARE(split.attribute("bankid"), QLatin1String("SPID")); QCOMPARE(split.attribute("account"), QLatin1String("A000076")); QCOMPARE(split.attribute("costcenter"), QLatin1String("C000005")); QCOMPARE(split.attribute("number"), QLatin1String("124")); QCOMPARE(split.attribute("value"), QLatin1String("96379/1000")); QCOMPARE(split.attribute("memo"), QString()); QCOMPARE(split.attribute("id"), QString()); QCOMPARE(split.childNodes().size(), 1); QCOMPARE(split.childNodes().at(0).isElement(), true); QDomElement tag = split.childNodes().at(0).toElement(); QCOMPARE(tag.tagName(), QLatin1String("TAG")); QCOMPARE(tag.attribute("id"), QLatin1String("G000001")); QCOMPARE(tag.childNodes().size(), 0); QString ref = QString( "\n" "\n" " \n" " \n" " \n" "\n"); } void MyMoneyXmlContentHandlerTest::testReplaceIDinSplit() { MyMoneySplit s; bool changed; s.setPayeeId("P000001"); s.setAccountId("A000076"); s.setCostCenterId("C000005"); changed = s.replaceId("X0001", "Y00001"); QCOMPARE(changed, false); QCOMPARE(s.payeeId(), QLatin1String("P000001")); QCOMPARE(s.accountId(), QLatin1String("A000076")); QCOMPARE(s.costCenterId(), QLatin1String("C000005")); changed = s.replaceId("P000002", "P000001"); QCOMPARE(changed, true); QCOMPARE(s.payeeId(), QLatin1String("P000002")); QCOMPARE(s.accountId(), QLatin1String("A000076")); QCOMPARE(s.costCenterId(), QLatin1String("C000005")); changed = s.replaceId("A000079", "A000076"); QCOMPARE(changed, true); QCOMPARE(s.payeeId(), QLatin1String("P000002")); QCOMPARE(s.accountId(), QLatin1String("A000079")); QCOMPARE(s.costCenterId(), QLatin1String("C000005")); changed = s.replaceId("C000006", "C000005"); QCOMPARE(changed, true); QCOMPARE(s.payeeId(), QLatin1String("P000002")); QCOMPARE(s.accountId(), QLatin1String("A000079")); QCOMPARE(s.costCenterId(), QLatin1String("C000006")); QString ref_ok = QString( "\n" "\n" " \n" " \n" " \n" " \n" " &lt;CONTAINER>\n" " &lt;TRANSACTION postdate="2010-03-05" memo="UMBUCHUNG" id="" commodity="EUR" entrydate="2010-03-08" >\n" " &lt;SPLITS>\n" " &lt;SPLIT payee="P000010" reconciledate="" shares="125000/100" action="Transfer" bankid="" number="" reconcileflag="0" memo="UMBUCHUNG" value="125000/100" id="S0001" account="A000087" />\n" " &lt;SPLIT payee="P000011" reconciledate="" shares="-125000/100" action="" bankid="A000076-2010-03-05-b6850c0-1" number="" reconcileflag="0" memo="UMBUCHUNG" value="-125000/100" id="S0002" account="A000076" />\n" " &lt;/SPLITS>\n" " &lt;KEYVALUEPAIRS>\n" " &lt;PAIR key="Imported" value="true" />\n" " &lt;/KEYVALUEPAIRS>\n" " &lt;/TRANSACTION>\n" " &lt;/CONTAINER>\n" "\" />\n" " \n" " \n" " \n" "\n" ); QDomDocument doc; QDomElement node; doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { s = MyMoneyXmlContentHandler::readSplit(node); QCOMPARE(s.payeeId(), QLatin1String("P000001")); QCOMPARE(s.replaceId("P2", "P1"), false); QCOMPARE(s.matchedTransaction().splits()[0].payeeId(), QLatin1String("P000010")); QCOMPARE(s.matchedTransaction().splits()[1].payeeId(), QLatin1String("P000011")); QCOMPARE(s.replaceId("P0010", "P000010"), true); QCOMPARE(s.matchedTransaction().splits()[0].payeeId(), QLatin1String("P0010")); QCOMPARE(s.matchedTransaction().splits()[1].payeeId(), QLatin1String("P000011")); QCOMPARE(s.replaceId("P0011", "P000011"), true); QCOMPARE(s.matchedTransaction().splits()[0].payeeId(), QLatin1String("P0010")); QCOMPARE(s.matchedTransaction().splits()[1].payeeId(), QLatin1String("P0011")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::readAccount() { MyMoneyAccount a; QString ref_ok = QString( "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"). arg(QDate::currentDate().toString(Qt::ISODate)).arg(QDate::currentDate().toString(Qt::ISODate)); QString ref_false = QString( "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"). arg(QDate::currentDate().toString(Qt::ISODate)).arg(QDate::currentDate().toString(Qt::ISODate)); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); try { a = MyMoneyXmlContentHandler::readAccount(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); a.addAccountId("TEST"); a.setValue("KEY", "VALUE"); try { a = MyMoneyXmlContentHandler::readAccount(node); QCOMPARE(a.id(), QStringLiteral("A000001")); QCOMPARE(a.name(), QStringLiteral("AccountName")); QCOMPARE(a.parentAccountId(), QStringLiteral("Parent")); QCOMPARE(a.lastModified(), QDate::currentDate()); QCOMPARE(a.lastReconciliationDate(), QDate()); QCOMPARE(a.institutionId(), QStringLiteral("B000001")); QCOMPARE(a.number(), QStringLiteral("465500")); QCOMPARE(a.openingDate(), QDate::currentDate()); QCOMPARE(a.accountType(), eMyMoney::Account::Type::Asset); QCOMPARE(a.description(), QStringLiteral("Desc")); QCOMPARE(a.accountList().count(), 2); QCOMPARE(a.accountList()[0], QStringLiteral("A000002")); QCOMPARE(a.accountList()[1], QStringLiteral("A000003")); QCOMPARE(a.pairs().count(), 3); QCOMPARE(a.value("key"), QStringLiteral("value")); QCOMPARE(a.value("Key"), QStringLiteral("Value")); QCOMPARE(a.pairs().contains("lastStatementDate"), false); QCOMPARE(a.reconciliationHistory().count(), 2); QCOMPARE(a.reconciliationHistory()[QDate(2011, 1, 1)].toString(), MyMoneyMoney(123, 100).toString()); QCOMPARE(a.reconciliationHistory()[QDate(2011, 2, 1)].toString(), MyMoneyMoney(456, 100).toString()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::writeAccount() { QString id = "A000001"; QString institutionid = "B000001"; QString parent = "Parent"; MyMoneyAccount r; r.setAccountType(eMyMoney::Account::Type::Asset); r.setOpeningDate(QDate::currentDate()); r.setLastModified(QDate::currentDate()); r.setDescription("Desc"); r.setName("AccountName"); r.setNumber("465500"); r.setParentAccountId(parent); r.setInstitutionId(institutionid); r.setValue(QString("key"), "value"); r.addAccountId("A000002"); r.addReconciliation(QDate(2011, 1, 1), MyMoneyMoney(123, 100)); r.addReconciliation(QDate(2011, 2, 1), MyMoneyMoney(456, 100)); QCOMPARE(r.pairs().count(), 2); QCOMPARE(r.value("key"), QLatin1String("value")); QCOMPARE(r.value("reconciliationHistory"), QLatin1String("2011-01-01:123/100;2011-02-01:114/25")); MyMoneyAccount a(id, r); QDomDocument doc("TEST"); QDomElement el = doc.createElement("ACCOUNT-CONTAINER"); doc.appendChild(el); MyMoneyXmlContentHandler::writeAccount(a, doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement accountContainer = doc.documentElement(); QVERIFY(accountContainer.isElement()); QCOMPARE(accountContainer.tagName(), QLatin1String("ACCOUNT-CONTAINER")); QVERIFY(accountContainer.childNodes().size() == 1); QVERIFY(accountContainer.childNodes().at(0).isElement()); QDomElement account = accountContainer.childNodes().at(0).toElement(); QCOMPARE(account.tagName(), QLatin1String("ACCOUNT")); QCOMPARE(account.attribute("id"), QLatin1String("A000001")); QCOMPARE(account.attribute("lastreconciled"), QString()); QCOMPARE(account.attribute("institution"), QLatin1String("B000001")); QCOMPARE(account.attribute("name"), QLatin1String("AccountName")); QCOMPARE(account.attribute("number"), QLatin1String("465500")); QCOMPARE(account.attribute("description"), QLatin1String("Desc")); QCOMPARE(account.attribute("parentaccount"), QLatin1String("Parent")); QCOMPARE(account.attribute("opened"), QDate::currentDate().toString(Qt::ISODate)); QCOMPARE(account.attribute("type"), QLatin1String("9")); QCOMPARE(account.attribute("lastmodified"), QDate::currentDate().toString(Qt::ISODate)); QCOMPARE(account.attribute("id"), QLatin1String("A000001")); QCOMPARE(account.childNodes().size(), 2); QVERIFY(account.childNodes().at(0).isElement()); QDomElement subAccounts = account.childNodes().at(0).toElement(); QCOMPARE(subAccounts.tagName(), QLatin1String("SUBACCOUNTS")); QCOMPARE(subAccounts.childNodes().size(), 1); QVERIFY(subAccounts.childNodes().at(0).isElement()); QDomElement subAccount = subAccounts.childNodes().at(0).toElement(); QCOMPARE(subAccount.tagName(), QLatin1String("SUBACCOUNT")); QCOMPARE(subAccount.attribute("id"), QLatin1String("A000002")); QCOMPARE(subAccount.childNodes().size(), 0); QDomElement keyValuePairs = account.childNodes().at(1).toElement(); QCOMPARE(keyValuePairs.tagName(), QLatin1String("KEYVALUEPAIRS")); QCOMPARE(keyValuePairs.childNodes().size(), 2); QVERIFY(keyValuePairs.childNodes().at(0).isElement()); QDomElement keyValuePair1 = keyValuePairs.childNodes().at(0).toElement(); QCOMPARE(keyValuePair1.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair1.attribute("key"), QLatin1String("key")); QCOMPARE(keyValuePair1.attribute("value"), QLatin1String("value")); QCOMPARE(keyValuePair1.childNodes().size(), 0); QVERIFY(keyValuePairs.childNodes().at(1).isElement()); QDomElement keyValuePair2 = keyValuePairs.childNodes().at(1).toElement(); QCOMPARE(keyValuePair2.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair2.attribute("key"), QLatin1String("reconciliationHistory")); QCOMPARE(keyValuePair2.attribute("value"), QLatin1String("2011-01-01:123/100;2011-02-01:114/25")); QCOMPARE(keyValuePair2.childNodes().size(), 0); } void MyMoneyXmlContentHandlerTest::readWritePayee() { QDomDocument doc; QDomElement parent = doc.createElement("Test"); doc.appendChild(parent); MyMoneyPayee payee1("some random id"); //if the ID isn't set, w ethrow an exception MyMoneyXmlContentHandler::writePayee(payee1, doc, parent); QString temp1 = "Account1"; payee1.setDefaultAccountId(temp1); MyMoneyXmlContentHandler::writePayee(payee1, doc, parent); QString temp2 = "Account2"; payee1.setDefaultAccountId(temp2); MyMoneyXmlContentHandler::writePayee(payee1, doc, parent); payee1.setDefaultAccountId(); MyMoneyXmlContentHandler::writePayee(payee1, doc, parent); QDomElement el = parent.firstChild().toElement(); QVERIFY(!el.isNull()); auto payee2 = MyMoneyXmlContentHandler::readPayee(el); QVERIFY(!payee2.defaultAccountEnabled()); QVERIFY(payee2.defaultAccountId().isEmpty()); el = el.nextSibling().toElement(); QVERIFY(!el.isNull()); auto payee3 = MyMoneyXmlContentHandler::readPayee(el); QVERIFY(payee3.defaultAccountEnabled()); QVERIFY(payee3.defaultAccountId() == temp1); el = el.nextSibling().toElement(); QVERIFY(!el.isNull()); auto payee4 = MyMoneyXmlContentHandler::readPayee(el); QVERIFY(payee4.defaultAccountEnabled()); QVERIFY(payee4.defaultAccountId() == temp2); el = el.nextSibling().toElement(); QVERIFY(!el.isNull()); auto payee5 = MyMoneyXmlContentHandler::readPayee(el); QVERIFY(!payee5.defaultAccountEnabled()); QVERIFY(payee5.defaultAccountId().isEmpty()); } void MyMoneyXmlContentHandlerTest::readWriteTag() { QDomDocument doc; QDomElement parent = doc.createElement("Test"); doc.appendChild(parent); MyMoneyTag tag1("some random id"); //if the ID isn't set, w ethrow an exception tag1.setName(QStringLiteral("MyTagName")); tag1.setNamedTagColor(QStringLiteral("red")); tag1.setNotes(QStringLiteral("Notes for a tag")); MyMoneyXmlContentHandler::writeTag(tag1, doc, parent); QDomElement el = parent.firstChild().toElement(); QVERIFY(!el.isNull()); auto tag2 = MyMoneyXmlContentHandler::readTag(el); QCOMPARE(tag1.name(), tag2.name()); QCOMPARE(tag1.notes(), tag2.notes()); QCOMPARE(tag1.tagColor(), tag2.tagColor()); } void MyMoneyXmlContentHandlerTest::readInstitution() { MyMoneyInstitution i; QString ref_ok = QString( "\n" "\n" " \n" "
\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"); QString ref_false = QString( "\n" "\n" " \n" "
\n" " \n" " \n" " \n" " \n" " \n" "\n"); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); try { i = MyMoneyXmlContentHandler::readInstitution(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } i.addAccountId("TEST"); doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { QStringList alist; alist << "A000001" << "A000003"; i = MyMoneyXmlContentHandler::readInstitution(node); QVERIFY(i.sortcode() == "sortcode"); QVERIFY(i.id() == "I00001"); QVERIFY(i.manager() == "manager"); QVERIFY(i.name() == "name"); QVERIFY(i.street() == "street"); QVERIFY(i.postcode() == "postcode"); QVERIFY(i.city() == "town"); QVERIFY(i.telephone() == "telephone"); QVERIFY(i.accountList() == alist); QVERIFY(i.value(QString("key")) == "value"); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::writeInstitution() { MyMoneyInstitution n("name", "town", "street", "postcode", "telephone", "manager", "sortcode");; n.addAccountId("A000001"); n.addAccountId("A000003"); n.setValue(QString("key"), "value"); QDomDocument doc("TEST"); QDomElement el = doc.createElement("INSTITUTION-CONTAINER"); doc.appendChild(el); MyMoneyInstitution i("I00001", n); MyMoneyXmlContentHandler::writeInstitution(i, doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement institutionContainer = doc.documentElement(); QVERIFY(institutionContainer.isElement()); QCOMPARE(institutionContainer.tagName(), QLatin1String("INSTITUTION-CONTAINER")); QVERIFY(institutionContainer.childNodes().size() == 1); QVERIFY(institutionContainer.elementsByTagName("INSTITUTION").at(0).isElement()); QDomElement institution = institutionContainer.elementsByTagName("INSTITUTION").at(0).toElement(); QCOMPARE(institution.tagName(), QLatin1String("INSTITUTION")); QCOMPARE(institution.attribute("id"), QLatin1String("I00001")); QCOMPARE(institution.attribute("manager"), QLatin1String("manager")); QCOMPARE(institution.attribute("name"), QLatin1String("name")); QCOMPARE(institution.attribute("sortcode"), QLatin1String("sortcode")); QCOMPARE(institution.childNodes().size(), 3); QVERIFY(institution.childNodes().at(0).isElement()); QDomElement address = institution.childNodes().at(0).toElement(); QCOMPARE(address.tagName(), QLatin1String("ADDRESS")); QCOMPARE(address.attribute("street"), QLatin1String("street")); QCOMPARE(address.attribute("telephone"), QLatin1String("telephone")); QCOMPARE(address.attribute("zip"), QLatin1String("postcode")); QCOMPARE(address.attribute("city"), QLatin1String("town")); QCOMPARE(address.childNodes().size(), 0); QVERIFY(institution.childNodes().at(1).isElement()); QDomElement accountIds = institution.childNodes().at(1).toElement(); QCOMPARE(accountIds.tagName(), QLatin1String("ACCOUNTIDS")); QCOMPARE(accountIds.childNodes().size(), 2); QVERIFY(accountIds.childNodes().at(0).isElement()); QDomElement account1 = accountIds.childNodes().at(0).toElement(); QCOMPARE(account1.tagName(), QLatin1String("ACCOUNTID")); QCOMPARE(account1.attribute("id"), QLatin1String("A000001")); QCOMPARE(account1.childNodes().size(), 0); QVERIFY(accountIds.childNodes().at(1).isElement()); QDomElement account2 = accountIds.childNodes().at(1).toElement(); QCOMPARE(account2.tagName(), QLatin1String("ACCOUNTID")); QCOMPARE(account2.attribute("id"), QLatin1String("A000003")); QCOMPARE(account2.childNodes().size(), 0); QVERIFY(institution.childNodes().at(2).isElement()); QDomElement keyValuePairs = institution.childNodes().at(2).toElement(); QCOMPARE(keyValuePairs.tagName(), QLatin1String("KEYVALUEPAIRS")); QCOMPARE(keyValuePairs.childNodes().size(), 1); QVERIFY(keyValuePairs.childNodes().at(0).isElement()); QDomElement keyValuePair1 = keyValuePairs.childNodes().at(0).toElement(); QCOMPARE(keyValuePair1.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair1.attribute("key"), QLatin1String("key")); QCOMPARE(keyValuePair1.attribute("value"), QLatin1String("value")); QCOMPARE(keyValuePair1.childNodes().size(), 0); } void MyMoneyXmlContentHandlerTest::readSchedule() { MyMoneySchedule sch; QString ref_ok1 = QString( "\n" "\n" " \n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ).arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)); // diff to ref_ok1 is that we now have an empty entrydate // in the transaction parameters QString ref_ok2 = QString( "\n" "\n" " \n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ).arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)); QString ref_false = QString( "\n" "\n" " \n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ).arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } doc.setContent(ref_ok1); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QCOMPARE(sch.id(), QLatin1String("SCH0002")); QCOMPARE(sch.nextDueDate(), QDate::currentDate().addDays(7)); QCOMPARE(sch.startDate(), QDate::currentDate()); QCOMPARE(sch.endDate(), QDate()); QCOMPARE(sch.autoEnter(), true); QCOMPARE(sch.isFixed(), true); QCOMPARE(sch.weekendOption(), Schedule::WeekendOption::MoveNothing); QCOMPARE(sch.lastPayment(), QDate::currentDate()); QCOMPARE(sch.paymentType(), Schedule::PaymentType::DirectDebit); QCOMPARE(sch.type(), Schedule::Type::Bill); QCOMPARE(sch.name(), QLatin1String("A Name")); - QCOMPARE(sch.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(sch.baseOccurrence(), Schedule::Occurrence::Weekly); QCOMPARE(sch.occurrenceMultiplier(), 1); QCOMPARE(sch.nextDueDate(), sch.lastPayment().addDays(7)); QCOMPARE(sch.recordedPayments().count(), 1); QCOMPARE(sch.recordedPayments()[0], QDate::currentDate()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } doc.setContent(ref_ok2); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QCOMPARE(sch.id(), QLatin1String("SCH0002")); QCOMPARE(sch.nextDueDate(), QDate::currentDate().addDays(7)); QCOMPARE(sch.startDate(), QDate::currentDate()); QCOMPARE(sch.endDate(), QDate()); QCOMPARE(sch.autoEnter(), true); QCOMPARE(sch.isFixed(), true); QCOMPARE(sch.weekendOption(), Schedule::WeekendOption::MoveNothing); QCOMPARE(sch.lastPayment(), QDate::currentDate()); QCOMPARE(sch.paymentType(), Schedule::PaymentType::DirectDebit); QCOMPARE(sch.type(), Schedule::Type::Bill); QCOMPARE(sch.name(), QLatin1String("A Name")); - QCOMPARE(sch.occurrence(), Schedule::Occurrence::Weekly); + QCOMPARE(sch.baseOccurrence(), Schedule::Occurrence::Weekly); QCOMPARE(sch.occurrenceMultiplier(), 1); QCOMPARE(sch.nextDueDate(), sch.lastPayment().addDays(7)); QCOMPARE(sch.recordedPayments().count(), 1); QCOMPARE(sch.recordedPayments()[0], QDate::currentDate()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::writeSchedule() { MyMoneySchedule tempSch("A Name", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 123, Schedule::PaymentType::DirectDebit, QDate::currentDate(), QDate(), true, true); MyMoneySchedule sch("SCH0001", tempSch); sch.setLastPayment(QDate::currentDate()); sch.recordPayment(QDate::currentDate()); MyMoneyTransaction t("T000000000000000001"); t.setPostDate(QDate(2001, 12, 28)); t.setEntryDate(QDate(2003, 9, 29)); t.setMemo("Wohnung:Miete"); t.setCommodity("EUR"); t.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); s.setShares(MyMoneyMoney(96379, 100)); s.setValue(MyMoneyMoney(96379, 100)); s.setAccountId("A000076"); s.setBankID("SPID1"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); t.addSplit(s); s.setPayeeId("P000001"); s.setShares(MyMoneyMoney(-96379, 100)); s.setValue(MyMoneyMoney(-96379, 100)); s.setAccountId("A000276"); s.setBankID("SPID2"); s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); t.addSplit(s); sch.setTransaction(t); QDomDocument doc("TEST"); QDomElement el = doc.createElement("SCHEDULE-CONTAINER"); doc.appendChild(el); MyMoneyXmlContentHandler::writeSchedule(sch, doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement scheduleContainer = doc.documentElement(); QVERIFY(scheduleContainer.isElement()); QCOMPARE(scheduleContainer.tagName(), QLatin1String("SCHEDULE-CONTAINER")); QCOMPARE(scheduleContainer.childNodes().size(), 1); QVERIFY(scheduleContainer.childNodes().at(0).isElement()); QDomElement schedule = scheduleContainer.childNodes().at(0).toElement(); QCOMPARE(schedule.tagName(), QLatin1String("SCHEDULED_TX")); QCOMPARE(schedule.attribute("id"), QLatin1String("SCH0001")); QCOMPARE(schedule.attribute("paymentType"), QLatin1String("1")); QCOMPARE(schedule.attribute("autoEnter"), QLatin1String("1")); QCOMPARE(schedule.attribute("occurenceMultiplier"), QLatin1String("123")); // krazy:exclude=spelling QCOMPARE(schedule.attribute("startDate"), QDate::currentDate().toString(Qt::ISODate)); QCOMPARE(schedule.attribute("lastPayment"), QDate::currentDate().toString(Qt::ISODate)); QCOMPARE(schedule.attribute("occurenceMultiplier"), QLatin1String("123")); // krazy:exclude=spelling QCOMPARE(schedule.attribute("occurence"), QLatin1String("4")); // krazy:exclude=spelling QCOMPARE(schedule.attribute("type"), QLatin1String("1")); QCOMPARE(schedule.attribute("name"), QLatin1String("A Name")); QCOMPARE(schedule.attribute("fixed"), QLatin1String("1")); QCOMPARE(schedule.attribute("endDate"), QString()); QCOMPARE(schedule.childNodes().size(), 2); QVERIFY(schedule.childNodes().at(0).isElement()); QDomElement payments = schedule.childNodes().at(0).toElement(); QVERIFY(schedule.childNodes().at(1).isElement()); QDomElement transaction = schedule.childNodes().at(1).toElement(); QCOMPARE(transaction.tagName(), QLatin1String("TRANSACTION")); QCOMPARE(transaction.attribute("id"), QString()); QCOMPARE(transaction.attribute("postdate"), QLatin1String("2001-12-28")); QCOMPARE(transaction.attribute("commodity"), QLatin1String("EUR")); QCOMPARE(transaction.attribute("memo"), QLatin1String("Wohnung:Miete")); QCOMPARE(transaction.attribute("entrydate"), QLatin1String("2003-09-29")); QCOMPARE(transaction.childNodes().size(), 2); QVERIFY(transaction.childNodes().at(0).isElement()); QDomElement splits = transaction.childNodes().at(0).toElement(); QCOMPARE(splits.tagName(), QLatin1String("SPLITS")); QCOMPARE(splits.childNodes().size(), 2); QVERIFY(splits.childNodes().at(0).isElement()); QDomElement split1 = splits.childNodes().at(0).toElement(); QCOMPARE(split1.tagName(), QLatin1String("SPLIT")); QCOMPARE(split1.attribute("id"), QLatin1String("S0001")); QCOMPARE(split1.attribute("payee"), QLatin1String("P000001")); QCOMPARE(split1.attribute("reconcileflag"), QLatin1String("2")); QCOMPARE(split1.attribute("shares"), QLatin1String("96379/100")); QCOMPARE(split1.attribute("reconciledate"), QString()); QCOMPARE(split1.attribute("action"), QString()); QCOMPARE(split1.attribute("bankid"), QString()); QCOMPARE(split1.attribute("account"), QLatin1String("A000076")); QCOMPARE(split1.attribute("number"), QString()); QCOMPARE(split1.attribute("value"), QLatin1String("96379/100")); QCOMPARE(split1.attribute("memo"), QString()); QCOMPARE(split1.childNodes().size(), 0); QVERIFY(splits.childNodes().at(1).isElement()); QDomElement split2 = splits.childNodes().at(1).toElement(); QCOMPARE(split2.tagName(), QLatin1String("SPLIT")); QCOMPARE(split2.attribute("id"), QLatin1String("S0002")); QCOMPARE(split2.attribute("payee"), QLatin1String("P000001")); QCOMPARE(split2.attribute("reconcileflag"), QLatin1String("1")); QCOMPARE(split2.attribute("shares"), QLatin1String("-96379/100")); QCOMPARE(split2.attribute("reconciledate"), QString()); QCOMPARE(split2.attribute("action"), QString()); QCOMPARE(split2.attribute("bankid"), QString()); QCOMPARE(split2.attribute("account"), QLatin1String("A000276")); QCOMPARE(split2.attribute("number"), QString()); QCOMPARE(split2.attribute("value"), QLatin1String("-96379/100")); QCOMPARE(split2.attribute("memo"), QString()); QCOMPARE(split2.childNodes().size(), 0); QDomElement keyValuePairs = transaction.childNodes().at(1).toElement(); QCOMPARE(keyValuePairs.tagName(), QLatin1String("KEYVALUEPAIRS")); QCOMPARE(keyValuePairs.childNodes().size(), 1); QVERIFY(keyValuePairs.childNodes().at(0).isElement()); QDomElement keyValuePair1 = keyValuePairs.childNodes().at(0).toElement(); QCOMPARE(keyValuePair1.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair1.attribute("key"), QLatin1String("key")); QCOMPARE(keyValuePair1.attribute("value"), QLatin1String("value")); QCOMPARE(keyValuePair1.childNodes().size(), 0); } void MyMoneyXmlContentHandlerTest::testOverdue() { MyMoneySchedule sch_overdue; MyMoneySchedule sch_intime; // the following checks only work correctly, if currentDate() is // between the 1st and 27th. If it is between 28th and 31st // we don't perform them. Note: this should be fixed. if (QDate::currentDate().day() > 27 || QDate::currentDate().day() == 1) { qDebug() << "testOverdue() skipped because current day is between 28th and 2nd"; return; } QDate startDate = QDate::currentDate().addDays(-1).addMonths(-23); QDate lastPaymentDate = QDate::currentDate().addDays(-1).addMonths(-1); QString ref = QString( "\n" "\n" " \n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"); QString ref_overdue = ref.arg(startDate.toString(Qt::ISODate)) .arg(lastPaymentDate.toString(Qt::ISODate)) .arg(lastPaymentDate.toString(Qt::ISODate)); QString ref_intime = ref.arg(startDate.addDays(1).toString(Qt::ISODate)) .arg(lastPaymentDate.addDays(1).toString(Qt::ISODate)) .arg(lastPaymentDate.addDays(1).toString(Qt::ISODate)); QDomDocument doc; QDomElement node; // std::cout << ref_intime << std::endl; try { doc.setContent(ref_overdue); node = doc.documentElement().firstChild().toElement(); sch_overdue = MyMoneyXmlContentHandler::readSchedule(node); doc.setContent(ref_intime); node = doc.documentElement().firstChild().toElement(); sch_intime = MyMoneyXmlContentHandler::readSchedule(node); QCOMPARE(sch_overdue.isOverdue(), true); QCOMPARE(sch_intime.isOverdue(), false); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::testNextPayment() /* * Test for a schedule where a payment hasn't yet been made. * First payment is in the future. */ { MyMoneySchedule sch; QString future_sched = QString( "\n" "\n" "\n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" "\n" ); QDomDocument doc; QDomElement node; doc.setContent(future_sched); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QCOMPARE(sch.nextPayment(QDate(2007, 2, 14)), QDate(2007, 2, 17)); QCOMPARE(sch.nextPayment(QDate(2007, 2, 17)), QDate(2008, 2, 17)); QCOMPARE(sch.nextPayment(QDate(2007, 2, 18)), QDate(2008, 2, 17)); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::testNextPaymentOnLastDayOfMonth() { MyMoneySchedule sch; QString future_sched = QString( "\n" "\n" "\n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" "\n" ); QDomDocument doc; QDomElement node; doc.setContent(future_sched); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QDate nextPayment; // check for the first payment to happen nextPayment = sch.nextPayment(QDate(2014, 10, 1)); QCOMPARE(nextPayment, QDate(2014, 10, 31)); sch.setLastPayment(nextPayment); QCOMPARE(sch.nextPayment(QDate(2014, 11, 1)), QDate(2014, 11, 30)); QCOMPARE(sch.nextPayment(QDate(2014, 12, 1)), QDate(2014, 12, 31)); QCOMPARE(sch.nextPayment(QDate(2015, 1, 1)), QDate(2015, 1, 31)); QCOMPARE(sch.nextPayment(QDate(2015, 2, 1)), QDate(2015, 2, 28)); QCOMPARE(sch.nextPayment(QDate(2015, 3, 1)), QDate(2015, 3, 31)); // now check that we also cover leap years QCOMPARE(sch.nextPayment(QDate(2016, 2, 1)), QDate(2016, 2, 29)); QCOMPARE(sch.nextPayment(QDate(2016, 3, 1)), QDate(2016, 3, 31)); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::testPaymentDates() { MyMoneySchedule sch; QString ref_ok = QString( "\n" "\n" "\n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" "\n" ); QDomDocument doc; QDomElement node; doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); QDate startDate(2006, 1, 28); QDate endDate(2006, 5, 30); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QDate nextPayment = sch.nextPayment(startDate); QList list = sch.paymentDates(nextPayment, endDate); QCOMPARE(list.count(), 3); QCOMPARE(list[0], QDate(2006, 2, 28)); QCOMPARE(list[1], QDate(2006, 3, 31)); // Would fall on a Sunday so gets moved back to 28th. QCOMPARE(list[2], QDate(2006, 4, 28)); // Add tests for each possible occurrence. // Check how paymentDates is meant to work // Build a list of expected dates and compare // Schedule::Occurrence::Once sch.setOccurrence(Schedule::Occurrence::Once); startDate.setDate(2009, 1, 1); endDate.setDate(2009, 12, 31); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 1); QCOMPARE(list[0], QDate(2009, 1, 1)); // Schedule::Occurrence::Daily sch.setOccurrence(Schedule::Occurrence::Daily); startDate.setDate(2009, 1, 1); endDate.setDate(2009, 1, 5); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2009, 1, 1)); QCOMPARE(list[1], QDate(2009, 1, 2)); // Would fall on Saturday so gets moved to 2nd. QCOMPARE(list[2], QDate(2009, 1, 2)); // Would fall on Sunday so gets moved to 2nd. QCOMPARE(list[3], QDate(2009, 1, 2)); QCOMPARE(list[4], QDate(2009, 1, 5)); // Schedule::Occurrence::Daily with multiplier 2 sch.setOccurrenceMultiplier(2); list = sch.paymentDates(startDate.addDays(1), endDate); QCOMPARE(list.count(), 2); // Would fall on Sunday so gets moved to 2nd. QCOMPARE(list[0], QDate(2009, 1, 2)); QCOMPARE(list[1], QDate(2009, 1, 5)); sch.setOccurrenceMultiplier(1); // Schedule::Occurrence::Weekly sch.setOccurrence(Schedule::Occurrence::Weekly); startDate.setDate(2009, 1, 6); endDate.setDate(2009, 2, 4); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2009, 1, 6)); QCOMPARE(list[1], QDate(2009, 1, 13)); QCOMPARE(list[2], QDate(2009, 1, 20)); QCOMPARE(list[3], QDate(2009, 1, 27)); QCOMPARE(list[4], QDate(2009, 2, 3)); // Schedule::Occurrence::EveryOtherWeek sch.setOccurrence(Schedule::Occurrence::EveryOtherWeek); startDate.setDate(2009, 2, 5); endDate.setDate(2009, 4, 3); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2009, 2, 5)); QCOMPARE(list[1], QDate(2009, 2, 19)); QCOMPARE(list[2], QDate(2009, 3, 5)); QCOMPARE(list[3], QDate(2009, 3, 19)); QCOMPARE(list[4], QDate(2009, 4, 2)); // Schedule::Occurrence::Fortnightly sch.setOccurrence(Schedule::Occurrence::Fortnightly); startDate.setDate(2009, 4, 4); endDate.setDate(2009, 5, 31); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 4); // First one would fall on a Saturday and would get moved // to 3rd which is before start date so is not in list. // Would fall on a Saturday so gets moved to 17th. QCOMPARE(list[0], QDate(2009, 4, 17)); // Would fall on a Saturday so gets moved to 1st. QCOMPARE(list[1], QDate(2009, 5, 1)); // Would fall on a Saturday so gets moved to 15th. QCOMPARE(list[2], QDate(2009, 5, 15)); // Would fall on a Saturday so gets moved to 29th. QCOMPARE(list[3], QDate(2009, 5, 29)); // Schedule::Occurrence::EveryHalfMonth sch.setOccurrence(Schedule::Occurrence::EveryHalfMonth); startDate.setDate(2009, 6, 1); endDate.setDate(2009, 8, 11); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2009, 6, 1)); QCOMPARE(list[1], QDate(2009, 6, 16)); QCOMPARE(list[2], QDate(2009, 7, 1)); QCOMPARE(list[3], QDate(2009, 7, 16)); // Would fall on a Saturday so gets moved to 31st. QCOMPARE(list[4], QDate(2009, 7, 31)); // Schedule::Occurrence::EveryThreeWeeks sch.setOccurrence(Schedule::Occurrence::EveryThreeWeeks); startDate.setDate(2009, 8, 12); endDate.setDate(2009, 11, 12); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2009, 8, 12)); QCOMPARE(list[1], QDate(2009, 9, 2)); QCOMPARE(list[2], QDate(2009, 9, 23)); QCOMPARE(list[3], QDate(2009, 10, 14)); QCOMPARE(list[4], QDate(2009, 11, 4)); // Schedule::Occurrence::EveryFourWeeks sch.setOccurrence(Schedule::Occurrence::EveryFourWeeks); startDate.setDate(2009, 11, 13); endDate.setDate(2010, 3, 13); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2009, 11, 13)); QCOMPARE(list[1], QDate(2009, 12, 11)); QCOMPARE(list[2], QDate(2010, 1, 8)); QCOMPARE(list[3], QDate(2010, 2, 5)); QCOMPARE(list[4], QDate(2010, 3, 5)); // Schedule::Occurrence::EveryThirtyDays sch.setOccurrence(Schedule::Occurrence::EveryThirtyDays); startDate.setDate(2010, 3, 19); endDate.setDate(2010, 7, 19); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2010, 3, 19)); // Would fall on a Sunday so gets moved to 16th. QCOMPARE(list[1], QDate(2010, 4, 16)); QCOMPARE(list[2], QDate(2010, 5, 18)); QCOMPARE(list[3], QDate(2010, 6, 17)); // Would fall on a Saturday so gets moved to 16th. QCOMPARE(list[4], QDate(2010, 7, 16)); // Schedule::Occurrence::EveryEightWeeks sch.setOccurrence(Schedule::Occurrence::EveryEightWeeks); startDate.setDate(2010, 7, 26); endDate.setDate(2011, 3, 26); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2010, 7, 26)); QCOMPARE(list[1], QDate(2010, 9, 20)); QCOMPARE(list[2], QDate(2010, 11, 15)); QCOMPARE(list[3], QDate(2011, 1, 10)); QCOMPARE(list[4], QDate(2011, 3, 7)); // Schedule::Occurrence::EveryOtherMonth sch.setOccurrence(Schedule::Occurrence::EveryOtherMonth); startDate.setDate(2011, 3, 14); endDate.setDate(2011, 11, 20); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2011, 3, 14)); // Would fall on a Saturday so gets moved to 13th. QCOMPARE(list[1], QDate(2011, 5, 13)); QCOMPARE(list[2], QDate(2011, 7, 14)); QCOMPARE(list[3], QDate(2011, 9, 14)); QCOMPARE(list[4], QDate(2011, 11, 14)); // Schedule::Occurrence::EveryThreeMonths sch.setOccurrence(Schedule::Occurrence::EveryThreeMonths); startDate.setDate(2011, 11, 15); endDate.setDate(2012, 11, 19); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2011, 11, 15)); QCOMPARE(list[1], QDate(2012, 2, 15)); QCOMPARE(list[2], QDate(2012, 5, 15)); QCOMPARE(list[3], QDate(2012, 8, 15)); QCOMPARE(list[4], QDate(2012, 11, 15)); // Schedule::Occurrence::Quarterly sch.setOccurrence(Schedule::Occurrence::Quarterly); startDate.setDate(2012, 11, 20); endDate.setDate(2013, 11, 23); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2012, 11, 20)); QCOMPARE(list[1], QDate(2013, 2, 20)); QCOMPARE(list[2], QDate(2013, 5, 20)); QCOMPARE(list[3], QDate(2013, 8, 20)); QCOMPARE(list[4], QDate(2013, 11, 20)); // Schedule::Occurrence::EveryFourMonths sch.setOccurrence(Schedule::Occurrence::EveryFourMonths); startDate.setDate(2013, 11, 21); endDate.setDate(2015, 3, 23); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2013, 11, 21)); QCOMPARE(list[1], QDate(2014, 3, 21)); QCOMPARE(list[2], QDate(2014, 7, 21)); QCOMPARE(list[3], QDate(2014, 11, 21)); // Would fall on a Saturday so gets moved to 20th. QCOMPARE(list[4], QDate(2015, 3, 20)); // Schedule::Occurrence::TwiceYearly sch.setOccurrence(Schedule::Occurrence::TwiceYearly); startDate.setDate(2015, 3, 22); endDate.setDate(2017, 3, 29); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 4); // First date would fall on a Sunday which would get moved // to 20th which is before start date so not in list. QCOMPARE(list[0], QDate(2015, 9, 22)); QCOMPARE(list[1], QDate(2016, 3, 22)); QCOMPARE(list[2], QDate(2016, 9, 22)); QCOMPARE(list[3], QDate(2017, 3, 22)); // Schedule::Occurrence::Yearly sch.setOccurrence(Schedule::Occurrence::Yearly); startDate.setDate(2017, 3, 23); endDate.setDate(2021, 3, 29); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2017, 3, 23)); QCOMPARE(list[1], QDate(2018, 3, 23)); // Would fall on a Saturday so gets moved to 22nd. QCOMPARE(list[2], QDate(2019, 3, 22)); QCOMPARE(list[3], QDate(2020, 3, 23)); QCOMPARE(list[4], QDate(2021, 3, 23)); // Schedule::Occurrence::EveryOtherYear sch.setOccurrence(Schedule::Occurrence::EveryOtherYear); startDate.setDate(2021, 3, 24); endDate.setDate(2029, 3, 30); sch.setStartDate(startDate); sch.setNextDueDate(startDate); list = sch.paymentDates(startDate, endDate); QCOMPARE(list.count(), 5); QCOMPARE(list[0], QDate(2021, 3, 24)); QCOMPARE(list[1], QDate(2023, 3, 24)); QCOMPARE(list[2], QDate(2025, 3, 24)); QCOMPARE(list[3], QDate(2027, 3, 24)); // Would fall on a Saturday so gets moved to 23rd. QCOMPARE(list[4], QDate(2029, 3, 23)); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::testHasReferenceTo() { MyMoneySchedule sch; QString ref_ok = QString( "\n" "\n" " \n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ).arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)) .arg(QDate::currentDate().toString(Qt::ISODate)); QDomDocument doc; QDomElement node; doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } QCOMPARE(sch.hasReferenceTo(QLatin1String("P000001")), true); QCOMPARE(sch.hasReferenceTo(QLatin1String("A000276")), true); QCOMPARE(sch.hasReferenceTo(QLatin1String("A000076")), true); QCOMPARE(sch.hasReferenceTo(QLatin1String("EUR")), true); } void MyMoneyXmlContentHandlerTest::testPaidEarlyOneTime() { // this tries to figure out what's wrong with // https://bugs.kde.org/show_bug.cgi?id=231029 MyMoneySchedule sch; QDate paymentInFuture = QDate::currentDate().addDays(7); QString ref_ok = QString( "\n" "\n" " \n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ).arg(paymentInFuture.toString(Qt::ISODate)) .arg(paymentInFuture.toString(Qt::ISODate)) .arg(paymentInFuture.toString(Qt::ISODate)); QDomDocument doc; QDomElement node; doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QCOMPARE(sch.isFinished(), true); - QCOMPARE(sch.occurrencePeriod(), Schedule::Occurrence::Monthly); + QCOMPARE(sch.occurrence(), Schedule::Occurrence::Monthly); QCOMPARE(sch.paymentDates(QDate::currentDate(), QDate::currentDate().addDays(21)).count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyXmlContentHandlerTest::testReplaceId() { MyMoneySchedule sch; QDate paymentInFuture = QDate::currentDate().addDays(7); QString ref_ok = QString( "\n" "\n" " \n" // krazy:exclude=spelling " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" ).arg(paymentInFuture.toString(Qt::ISODate)) .arg(paymentInFuture.toString(Qt::ISODate)) .arg(paymentInFuture.toString(Qt::ISODate)); QDomDocument doc; QDomElement node; doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { sch = MyMoneyXmlContentHandler::readSchedule(node); QCOMPARE(sch.transaction().postDate().isValid(), false); QCOMPARE(sch.transaction().splits()[0].accountId(), QLatin1String("A000076")); QCOMPARE(sch.transaction().splits()[1].accountId(), QLatin1String("A000276")); QCOMPARE(sch.replaceId(QLatin1String("A000079"), QLatin1String("A000076")), true); QCOMPARE(sch.transaction().splits()[0].accountId(), QLatin1String("A000079")); QCOMPARE(sch.transaction().splits()[1].accountId(), QLatin1String("A000276")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } diff --git a/kmymoney/widgets/kmymoneybriefschedule.cpp b/kmymoney/widgets/kmymoneybriefschedule.cpp index c0807621d..e3a32e948 100644 --- a/kmymoney/widgets/kmymoneybriefschedule.cpp +++ b/kmymoney/widgets/kmymoneybriefschedule.cpp @@ -1,209 +1,209 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2003-2012 Thomas Baumgart * Copyright 2017-2018 Ł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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kmymoneybriefschedule.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kmymoneybriefschedule.h" #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneyutils.h" #include "icons/icons.h" #include "mymoneyenums.h" using namespace Icons; class KMyMoneyBriefSchedulePrivate { Q_DISABLE_COPY(KMyMoneyBriefSchedulePrivate) public: KMyMoneyBriefSchedulePrivate() : ui(new Ui::KMyMoneyBriefSchedule), m_index(0) { } ~KMyMoneyBriefSchedulePrivate() { delete ui; } void loadSchedule() { try { if (m_index < m_scheduleList.count()) { MyMoneySchedule sched = m_scheduleList[m_index]; ui->m_indexLabel->setText(i18n("%1 of %2", m_index + 1, m_scheduleList.count())); ui->m_name->setText(sched.name()); ui->m_type->setText(KMyMoneyUtils::scheduleTypeToString(sched.type())); ui->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; auto 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 += ""; } } ui->m_details->setText(text); ui->m_prevButton->setEnabled(true); ui->m_nextButton->setEnabled(true); - ui->m_skipButton->setEnabled(sched.occurrencePeriod() != eMyMoney::Schedule::Occurrence::Once); + ui->m_skipButton->setEnabled(sched.occurrence() != eMyMoney::Schedule::Occurrence::Once); if (m_index == 0) ui->m_prevButton->setEnabled(false); if (m_index == (m_scheduleList.count() - 1)) ui->m_nextButton->setEnabled(false); } } catch (const MyMoneyException &) { } } Ui::KMyMoneyBriefSchedule *ui; QList m_scheduleList; int m_index; QDate m_date; }; KMyMoneyBriefSchedule::KMyMoneyBriefSchedule(QWidget *parent) : QWidget(parent), d_ptr(new KMyMoneyBriefSchedulePrivate) { Q_D(KMyMoneyBriefSchedule); d->ui->setupUi(this); d->ui->m_nextButton->setIcon(Icons::get(Icon::ArrowRight)); d->ui->m_prevButton->setIcon(Icons::get(Icon::ArrowLeft)); d->ui->m_skipButton->setIcon(Icons::get(Icon::MediaSeekForward)); d->ui->m_buttonEnter->setIcon(Icons::get(Icon::KeyEnter)); connect(d->ui->m_prevButton, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotPrevClicked); connect(d->ui->m_nextButton, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotNextClicked); connect(d->ui->m_closeButton, &QAbstractButton::clicked, this, &QWidget::hide); connect(d->ui->m_skipButton, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotSkipClicked); connect(d->ui->m_buttonEnter, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotEnterClicked); } KMyMoneyBriefSchedule::~KMyMoneyBriefSchedule() { Q_D(KMyMoneyBriefSchedule); delete d; } void KMyMoneyBriefSchedule::setSchedules(QList list, const QDate& date) { Q_D(KMyMoneyBriefSchedule); d->m_scheduleList = list; d->m_date = date; d->m_index = 0; if (list.count() >= 1) { d->loadSchedule(); } } void KMyMoneyBriefSchedule::slotPrevClicked() { Q_D(KMyMoneyBriefSchedule); if (d->m_index >= 1) { --d->m_index; d->loadSchedule(); } } void KMyMoneyBriefSchedule::slotNextClicked() { Q_D(KMyMoneyBriefSchedule); if (d->m_index < (d->m_scheduleList.count() - 1)) { d->m_index++; d->loadSchedule(); } } void KMyMoneyBriefSchedule::slotEnterClicked() { Q_D(KMyMoneyBriefSchedule); hide(); emit enterClicked(d->m_scheduleList[d->m_index], d->m_date); } void KMyMoneyBriefSchedule::slotSkipClicked() { Q_D(KMyMoneyBriefSchedule); hide(); emit skipClicked(d->m_scheduleList[d->m_index], d->m_date); } diff --git a/kmymoney/wizards/newloanwizard/keditloanwizard.cpp b/kmymoney/wizards/newloanwizard/keditloanwizard.cpp index 379f0b378..dda9074ae 100644 --- a/kmymoney/wizards/newloanwizard/keditloanwizard.cpp +++ b/kmymoney/wizards/newloanwizard/keditloanwizard.cpp @@ -1,546 +1,546 @@ /*************************************************************************** keditloanwizard.cpp - description ------------------- begin : Wed Nov 12 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Ł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 "keditloanwizard.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_interesteditwizardpage.h" #include "ui_paymentfrequencywizardpage.h" #include "ui_paymenteditwizardpage.h" #include "ui_loanattributeswizardpage.h" #include "ui_effectivedatewizardpage.h" #include "ui_interesttypewizardpage.h" #include "ui_editselectionwizardpage.h" #include "ui_finalpaymentwizardpage.h" #include "knewloanwizard.h" #include "knewloanwizard_p.h" #include "kmymoneylineedit.h" #include "kmymoneyedit.h" #include "kmymoneyaccountselector.h" #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneypayee.h" #include "mymoneyschedule.h" #include "mymoneytransactionfilter.h" class KEditLoanWizardPrivate : public KNewLoanWizardPrivate { Q_DISABLE_COPY(KEditLoanWizardPrivate) public: KEditLoanWizardPrivate(KEditLoanWizard *qq) : KNewLoanWizardPrivate(qq), m_lastSelection(0), m_fullyRepayLoan(false) { } MyMoneySchedule m_schedule; int m_lastSelection; bool m_fullyRepayLoan; }; KEditLoanWizard::KEditLoanWizard(const MyMoneyAccount& account, QWidget *parent) : KNewLoanWizard(*new KEditLoanWizardPrivate(this), parent) { Q_D(KEditLoanWizard); auto file = MyMoneyFile::instance(); setWindowTitle(i18n("Edit loan wizard")); d->m_account = account; try { QString id = d->m_account.value("schedule"); d->m_schedule = file->schedule(id); } catch (const MyMoneyException &) { } d->m_lastSelection = -1; loadWidgets(d->m_account); if (d->m_account.openingDate() > QDate::currentDate()) { //FIXME: port d->ui->m_effectiveDatePage->ui->m_effectiveDateNoteLabel->setText(QString("\n") + i18n( "Note: you will not be able to modify this account today, because the opening date \"%1\" is in the future. " "Please revisit this dialog when the time has come.", QLocale().toString(d->m_account.openingDate()))); } else { d->ui->m_effectiveDatePage->ui->m_effectiveDateNoteLabel->hide(); } // turn off all pages that are contained here for derived classes d->m_pages.clearBit(Page_Intro); d->m_pages.clearBit(Page_NewGeneralInfo); d->m_pages.clearBit(Page_LendBorrow); d->m_pages.clearBit(Page_Name); d->m_pages.clearBit(Page_NewCalculateLoan); d->m_pages.clearBit(Page_NewPayments); removePage(Page_AssetAccount); d->ui->m_assetAccountPage = 0; // turn on all pages that are contained here for derived classes d->m_pages.setBit(Page_EditIntro); d->m_pages.setBit(Page_EditSelection); // make sure, we show the correct start page setStartId(Page_EditIntro); } KEditLoanWizard::~KEditLoanWizard() { } void KEditLoanWizard::loadWidgets(const MyMoneyAccount& /* account */) { Q_D(KEditLoanWizard); auto file = MyMoneyFile::instance(); QString paymentAccountId, interestAccountId; //FIXME: port d->ui->m_namePage->ui->m_nameEdit->loadText(d->m_account.name()); d->ui->m_loanAmountPage->ui->m_loanAmountEdit->loadText(d->m_account.loanAmount().formatMoney(d->m_account.fraction(MyMoneyFile::instance()->security(d->m_account.currencyId())))); d->ui->m_finalPaymentPage->ui->m_finalPaymentEdit->loadText(d->m_account.finalPayment().formatMoney(d->m_account.fraction(MyMoneyFile::instance()->security(d->m_account.currencyId())))); setField("firstDueDateEdit", d->m_account.openingDate()); //FIXME: port if (d->m_account.fixedInterestRate()) { d->ui->m_interestTypePage->ui->m_fixedInterestButton->click(); } else { d->ui->m_interestTypePage->ui->m_variableInterestButton->click(); } QString institutionName = file->institution(d->m_account.institutionId()).name(); d->ui->m_loanAttributesPage->setInstitution(institutionName); MyMoneyMoney ir; if (d->m_schedule.startDate() > QDate::currentDate()) { ir = d->m_account.interestRate(d->m_schedule.startDate()); } else { ir = d->m_account.interestRate(QDate::currentDate()); } //FIXME: port d->ui->m_interestPage->ui->m_interestRateEdit->loadText(ir.formatMoney(QString(), 3)); d->ui->m_interestPage->ui->m_interestRateEdit->setPrecision(3); d->ui->m_interestEditPage->ui->m_newInterestRateEdit->loadText(ir.formatMoney(QString(), 3)); d->ui->m_interestEditPage->ui->m_newInterestRateEdit->setPrecision(3); d->ui->m_interestEditPage->ui->m_interestRateLabel->setText(QString(" ") + ir.formatMoney(QString(), 3) + QString("%")); - d->ui->m_paymentFrequencyPage->ui->m_paymentFrequencyUnitEdit->setCurrentIndex(d->ui->m_paymentFrequencyPage->ui->m_paymentFrequencyUnitEdit->findData(QVariant((int)d->m_schedule.occurrencePeriod()), Qt::UserRole, Qt::MatchExactly)); + d->ui->m_paymentFrequencyPage->ui->m_paymentFrequencyUnitEdit->setCurrentIndex(d->ui->m_paymentFrequencyPage->ui->m_paymentFrequencyUnitEdit->findData(QVariant((int)d->m_schedule.occurrence()), Qt::UserRole, Qt::MatchExactly)); d->ui->m_durationPage->updateTermWidgets(d->m_account.term()); // the base payment (amortization and interest) is determined // by adding all splits that are not automatically calculated. // If the loan is a liability, we reverse the sign at the end MyMoneyMoney basePayment; MyMoneyMoney addPayment; d->m_transaction = d->m_schedule.transaction(); foreach (const MyMoneySplit& it_s, d->m_schedule.transaction().splits()) { MyMoneyAccount acc = file->account(it_s.accountId()); // if it's the split that references the source/dest // of the money, we check if we borrow or loan money if (paymentAccountId.isEmpty() && acc.isAssetLiability() && !acc.isLoan() && it_s.value() != MyMoneyMoney::autoCalc) { if (it_s.value().isNegative()) { setField("lendButton", false); setField("borrowButton", true); } else { setField("lendButton", true); setField("borrowButton", false); } // we keep the amount of the full payment and subtract the // base payment later to get information about the additional payment addPayment = it_s.value(); paymentAccountId = it_s.accountId(); MyMoneyPayee payee; if (!it_s.payeeId().isEmpty()) { try { payee = file->payee(it_s.payeeId()); setField("payeeEdit", payee.id()); } catch (const MyMoneyException &) { qWarning("Payee for schedule has been deleted"); } } // remove this split with one that will be replaced // later and has a phony id d->m_transaction.removeSplit(it_s); d->m_split.clearId(); d->m_transaction.addSplit(d->m_split); } if (it_s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { interestAccountId = it_s.accountId(); } if (it_s.value() != MyMoneyMoney::autoCalc) { basePayment += it_s.value(); } else { // remove the splits which should not show up // for additional fees d->m_transaction.removeSplit(it_s); } } if (field("borrowButton").toBool()) { basePayment = -basePayment; addPayment = -addPayment; } // now make adjustment to get the amount of the additional fees addPayment -= basePayment; // load account selection widgets now that we know if // we borrow or lend money d->loadAccountList(); int fraction = d->m_account.fraction(MyMoneyFile::instance()->security(d->m_account.currencyId())); //FIXME: port d->ui->m_paymentPage->ui->m_paymentEdit->loadText(basePayment.formatMoney(fraction)); d->ui->m_paymentEditPage->ui->m_newPaymentEdit->loadText(basePayment.formatMoney(fraction)); d->ui->m_paymentEditPage->ui->m_paymentLabel->setText(QString(" ") + basePayment.formatMoney(fraction)); setField("additionalCost", addPayment.formatMoney(fraction)); d->ui->m_interestCategoryPage->ui->m_interestAccountEdit->setSelected(interestAccountId); d->ui->m_schedulePage->ui->m_paymentAccountEdit->setSelected(paymentAccountId); setField("nextDueDateEdit", d->m_schedule.nextPayment()); int changeFrequencyUnit; int amt = d->m_account.interestChangeFrequency(&changeFrequencyUnit); if (amt != -1) { setField("interestFrequencyAmountEdit", amt); setField("interestFrequencyUnitEdit", changeFrequencyUnit); } // keep track, if the loan should be fully repayed d->m_fullyRepayLoan = d->m_account.finalPayment() < basePayment; d->updateLoanInfo(); } bool KEditLoanWizard::validateCurrentPage() { Q_D(KEditLoanWizard); auto dontLeavePage = false; //FIXME: port m_lastSelection QAbstractButton* button = d->ui->m_editSelectionPage->ui->m_selectionButtonGroup->button(d->m_lastSelection); if (currentPage() == d->ui->m_editSelectionPage) { if (button != 0 && d->m_lastSelection != d->ui->m_editSelectionPage->ui->m_selectionButtonGroup->checkedId()) { QString errMsg = i18n( "Your previous selection was \"%1\". If you select another option, " "KMyMoney will dismiss the changes you have just entered. " "Do you wish to proceed?", button->text()); if (KMessageBox::questionYesNo(this, errMsg) == KMessageBox::No) { dontLeavePage = true; } else { loadWidgets(d->m_account); } } if (!dontLeavePage) { // turn off all pages except the summary at the end // and the one's we need for the selected option // and load the widgets with the current values // general info d->m_pages.clearBit(Page_Name); d->m_pages.clearBit(Page_InterestType); d->m_pages.clearBit(Page_PreviousPayments); d->m_pages.clearBit(Page_RecordPayment); d->m_pages.clearBit(Page_VariableInterestDate); d->m_pages.clearBit(Page_FirstPayment); // loan calculation d->m_pages.clearBit(Page_PaymentEdit); d->m_pages.clearBit(Page_InterestEdit); d->m_pages.clearBit(Page_PaymentFrequency); d->m_pages.clearBit(Page_InterestCalculation); d->m_pages.clearBit(Page_LoanAmount); d->m_pages.clearBit(Page_Interest); d->m_pages.clearBit(Page_Duration); d->m_pages.clearBit(Page_Payment); d->m_pages.clearBit(Page_FinalPayment); d->m_pages.clearBit(Page_CalculationOverview); // payment d->m_pages.clearBit(Page_InterestCategory); d->m_pages.clearBit(Page_AdditionalFees); d->m_pages.clearBit(Page_Schedule); d->m_pages.setBit(Page_Summary); // Attributes d->m_pages.clearBit(Page_LoanAttributes); d->m_pages.setBit(Page_EffectiveDate); if (page(Page_Summary) != 0) { removePage(Page_Summary); } if (field("editInterestRateButton").toBool()) { d->m_pages.setBit(Page_PaymentFrequency); d->m_pages.setBit(Page_InterestType); d->m_pages.setBit(Page_VariableInterestDate); d->m_pages.setBit(Page_PaymentEdit); d->m_pages.setBit(Page_InterestEdit); d->m_pages.setBit(Page_InterestCategory); d->m_pages.setBit(Page_Schedule); d->m_pages.setBit(Page_SummaryEdit); } else if (field("editOtherCostButton").toBool()) { d->m_pages.setBit(Page_PaymentFrequency); d->m_pages.setBit(Page_AdditionalFees); d->m_pages.setBit(Page_InterestCategory); d->m_pages.setBit(Page_Schedule); d->m_pages.setBit(Page_SummaryEdit); } else if (field("editOtherInfoButton").toBool()) { d->m_pages.setBit(Page_Name); d->m_pages.setBit(Page_InterestCalculation); d->m_pages.setBit(Page_Interest); d->m_pages.setBit(Page_Duration); d->m_pages.setBit(Page_Payment); d->m_pages.setBit(Page_FinalPayment); d->m_pages.setBit(Page_CalculationOverview); d->m_pages.setBit(Page_InterestCategory); d->m_pages.setBit(Page_AdditionalFees); d->m_pages.setBit(Page_Schedule); d->m_pages.clearBit(Page_SummaryEdit); setPage(Page_Summary, d->ui->m_summaryPage); d->m_pages.setBit(Page_Summary); } else if (field("editAttributesButton").toBool()) { d->m_pages.setBit(Page_LoanAttributes); d->m_pages.clearBit(Page_EffectiveDate); } else { qWarning("%s,%d: This should never happen", __FILE__, __LINE__); } d->m_lastSelection = d->ui->m_editSelectionPage->ui->m_selectionButtonGroup->checkedId(); } // if(!dontLeavePage) } else if (currentPage() == d->ui->m_additionalFeesPage) { if (field("editOtherCostButton").toBool()) { d->updateLoanInfo(); updateEditSummary(); } } else if (currentPage() == d->ui->m_interestEditPage) { // copy the necessary data to the widgets used for calculation //FIXME: port to fields d->ui->m_interestPage->ui->m_interestRateEdit->setValue(field("newInterestRateEdit").value()); d->ui->m_paymentPage->ui->m_paymentEdit->setValue(field("newPaymentEdit").value()); // if interest rate and payment amount is given, then force // the term to be recalculated. The final payment is adjusted to // 0 if the loan was ment to be fully repayed d->ui->m_durationPage->updateTermWidgets(d->m_account.term()); if (field("interestRateEditValid").toBool() && field("paymentEditValid").toBool()) { // if there's an amortization going on, we can evaluate // the new term. If the amortization is 0 (interest only // payments) then we keep the term as entered by the user. if (field("loanAmountEdit").value() != field("finalPaymentEdit").value()) { setField("durationValueEdit", 0); } if (d->m_fullyRepayLoan) d->ui->m_finalPaymentPage->ui->m_finalPaymentEdit->loadText(MyMoneyMoney().formatMoney(d->m_account.fraction(MyMoneyFile::instance()->security(d->m_account.currencyId())))); } /* // we need to calculate the balance at the time of the change // in order to accurately recalculate the term. A special // situation arises, when we keep track of all payments and // the full loan is not yet paid out. In this case, we take the // the loan amount minus all amortization payments as the current // balance. // FIXME: This needs some more thoughts. We leave it out for // now and always calculate with the full loan amount. MyMoneyMoney balance = m_account.openingBalance(); QList list; QList::ConstIterator it; MyMoneySplit split; MyMoneyTransactionFilter filter(m_account.id()); filter.setDateFilter(QDate(), m_effectiveChangeDateEdit->date().addDays(-1)); list = MyMoneyFile::instance()->transactionList(filter); for(it = list.begin(); it != list.end(); ++it) { try { split = (*it).splitByAccount(m_account.id()); balance += split.value(); } catch (const MyMoneyException &e) { // account is not referenced within this transaction } } m_loanAmountEdit->setText(balance.formatMoney()); */ // now re-calculate the figures dontLeavePage = !d->calculateLoan(); // reset the original loan amount to the widget //FIXME: port to fields d->ui->m_loanAmountPage->ui->m_loanAmountEdit->setValue(d->m_account.loanAmount()); if (!dontLeavePage) { d->updateLoanInfo(); updateEditSummary(); } } if (!dontLeavePage) dontLeavePage = ! KNewLoanWizard::validateCurrentPage(); // These might have been set by KNewLoanWizard d->m_pages.clearBit(Page_PreviousPayments); d->m_pages.clearBit(Page_RecordPayment); if (dontLeavePage) return false; // we never need to show this page if (currentPage() == d->ui->m_previousPaymentsPage) dontLeavePage = KNewLoanWizard::validateCurrentPage(); return ! dontLeavePage; } void KEditLoanWizard::updateEditSummary() { Q_D(KEditLoanWizard); // calculate the number of affected transactions MyMoneyTransactionFilter filter(d->m_account.id()); filter.setDateFilter(field("effectiveChangeDateEdit").toDate(), QDate()); int count = 0; QList list; list = MyMoneyFile::instance()->transactionList(filter); foreach (const MyMoneyTransaction& it, list) { int match = 0; foreach (const MyMoneySplit& it_s, it.splits()) { // we only count those transactions that have an interest // and amortization part if (it_s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) match |= 0x01; if (it_s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)) match |= 0x02; } if (match == 0x03) ++count; } setField("affectedPayments", QString().sprintf("%d", count)); } const MyMoneySchedule KEditLoanWizard::schedule() const { Q_D(const KEditLoanWizard); MyMoneySchedule sched = d->m_schedule; sched.setTransaction(transaction()); sched.setOccurrence(eMyMoney::Schedule::Occurrence(field("paymentFrequencyUnitEdit").toInt())); if (field("nextDueDateEdit").toDate() < d->m_schedule.startDate()) sched.setStartDate(field("nextDueDateEdit").toDate()); return sched; } const MyMoneyAccount KEditLoanWizard::account() const { Q_D(const KEditLoanWizard); MyMoneyAccountLoan acc(d->m_account); if (field("interestOnReceptionButton").toBool()) acc.setInterestCalculation(MyMoneyAccountLoan::paymentReceived); else acc.setInterestCalculation(MyMoneyAccountLoan::paymentDue); auto file = MyMoneyFile::instance(); QString institution = d->ui->m_loanAttributesPage->ui->m_qcomboboxInstitutions->currentText(); if (institution != i18n("(No Institution)")) { const auto list = file->institutionList(); for (const auto& testInstitution : list) { if (testInstitution.name() == institution) { acc.setInstitutionId(testInstitution.id()); break; } } } else { acc.setInstitutionId(QString()); } acc.setFixedInterestRate(field("fixedInterestButton").toBool()); acc.setFinalPayment(field("finalPaymentEdit").value()); acc.setTerm(d->ui->m_durationPage->term()); acc.setPeriodicPayment(field("paymentEdit").value()); acc.setInterestRate(field("effectiveChangeDateEdit").toDate(), field("interestRateEdit").value()); acc.setPayee(field("payeeEdit").toString()); if (field("variableInterestButton").toBool()) { acc.setNextInterestChange(field("interestChangeDateEdit").toDate()); acc.setInterestChangeFrequency(field("interestFrequencyAmountEdit").toInt(), field("interestFrequencyUnitEdit").toInt()); } return acc; } const MyMoneyTransaction KEditLoanWizard::transaction() const { Q_D(const KEditLoanWizard); auto t = d->transaction(); auto s = t.splitByAccount(QString("Phony-ID")); s.setAccountId(d->m_account.id()); t.modifySplit(s); return t; }