diff --git a/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp b/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp index 9a46d4add..ce32eed68 100644 --- a/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp +++ b/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp @@ -1,717 +1,733 @@ /*************************************************************************** kreportconfigurationdlg.cpp - description ------------------- begin : Mon Jun 21 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kreportconfigurationfilterdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include #include #include #include #include "ui_kfindtransactiondlgdecl.h" #include #include #include #include #include #include +#include KReportConfigurationFilterDlg::KReportConfigurationFilterDlg( MyMoneyReport report, QWidget *parent) : KFindTransactionDlg(parent), m_tabRowColPivot(0), m_tabRowColQuery(0), m_tabChart(0), m_tabRange(0), m_initialState(report), m_currentState(report) { // // Rework labeling // setWindowTitle(i18n("Report Configuration")); delete m_ui->TextLabel1; // // Rework the buttons // // the Apply button is always enabled disconnect(SIGNAL(selectionNotEmpty(bool))); m_ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); KGuiItem::assign(m_ui->buttonBox->button(QDialogButtonBox::Apply), KStandardGuiItem::ok()); m_ui->buttonBox->button(QDialogButtonBox::Apply)->setToolTip(i18nc("@info:tooltip for report configuration apply button", "Apply the configuration changes to the report")); // // Add new tabs // m_tabGeneral = new ReportTabGeneral(m_ui->m_criteriaTab); m_ui->m_criteriaTab->insertTab(0, m_tabGeneral, i18nc("General tab", "General")); if (m_initialState.reportType() == MyMoneyReport::ePivotTable) { m_tabRowColPivot = new ReportTabRowColPivot(m_ui->m_criteriaTab); m_ui->m_criteriaTab->insertTab(1, m_tabRowColPivot, i18n("Rows/Columns")); m_tabChart = new ReportTabChart(m_ui->m_criteriaTab); m_ui->m_criteriaTab->insertTab(2, m_tabChart, i18n("Chart")); m_tabRange = new ReportTabRange(m_ui->m_criteriaTab); m_ui->m_criteriaTab->insertTab(3, m_tabRange, i18n("Range")); // date tab is going to be replaced by range tab, so delete it m_ui->dateRangeLayout->removeWidget(m_dateRange); m_dateRange->deleteLater(); m_ui->m_criteriaTab->removeTab(m_ui->m_criteriaTab->indexOf(m_ui->m_dateTab)); m_ui->m_dateTab->deleteLater(); m_dateRange = m_tabRange->m_dateRange; connect(m_tabRange->ui->m_comboColumns, SIGNAL(activated(int)), this, SLOT(slotColumnTypeChanged(int))); connect(m_tabRange->ui->m_comboColumns, SIGNAL(activated(int)), this, SLOT(slotUpdateColumnsCombo())); connect(m_tabChart->ui->m_logYaxis, SIGNAL(stateChanged(int)), this, SLOT(slotLogAxisChanged(int))); connect(m_tabRowColPivot->ui->m_comboRows, SIGNAL(activated(int)), this, SLOT(slotRowTypeChanged(int))); connect(m_tabRowColPivot->ui->m_comboRows, SIGNAL(activated(int)), this, SLOT(slotUpdateColumnsCombo())); //control the state of the includeTransfer check connect(m_ui->m_categoriesView, SIGNAL(stateChanged()), this, SLOT(slotUpdateCheckTransfers())); } else if (m_initialState.reportType() == MyMoneyReport::eQueryTable) { // eInvestmentHoldings is a special-case report, and you cannot configure the // rows & columns of that report. if (m_initialState.rowType() < MyMoneyReport::eAccountByTopAccount) { m_tabRowColQuery = new ReportTabRowColQuery(m_ui->m_criteriaTab); m_ui->m_criteriaTab->insertTab(1, m_tabRowColQuery, i18n("Rows/Columns")); } + if (m_initialState.queryColumns() & MyMoneyReport::eQCcapitalgain) { + m_tabCapitalGain = new ReportTabCapitalGain(m_ui->m_criteriaTab); + m_ui->m_criteriaTab->insertTab(1, m_tabCapitalGain, i18n("Report")); + } } m_ui->m_criteriaTab->setCurrentIndex(m_ui->m_criteriaTab->indexOf(m_tabGeneral)); m_ui->m_criteriaTab->setMinimumSize(500, 200); QList list = MyMoneyFile::instance()->budgetList(); QList::const_iterator it_b; for (it_b = list.constBegin(); it_b != list.constEnd(); ++it_b) { m_budgets.push_back(*it_b); } // // Now set up the widgets with proper values // slotReset(); } KReportConfigurationFilterDlg::~KReportConfigurationFilterDlg() { } void KReportConfigurationFilterDlg::slotSearch() { // setup the filter from the dialog widgets setupFilter(); // Copy the m_filter over to the filter part of m_currentConfig. m_currentState.assignFilter(m_filter); // Then extract the report properties m_currentState.setName(m_tabGeneral->ui->m_editName->text()); m_currentState.setComment(m_tabGeneral->ui->m_editComment->text()); m_currentState.setConvertCurrency(m_tabGeneral->ui->m_checkCurrency->isChecked()); m_currentState.setFavorite(m_tabGeneral->ui->m_checkFavorite->isChecked()); m_currentState.setSkipZero(m_tabGeneral->ui->m_skipZero->isChecked()); if (m_tabRowColPivot) { MyMoneyReport::EDetailLevel dl[4] = { MyMoneyReport::eDetailAll, MyMoneyReport::eDetailTop, MyMoneyReport::eDetailGroup, MyMoneyReport::eDetailTotal }; m_currentState.setDetailLevel(dl[m_tabRowColPivot->ui->m_comboDetail->currentIndex()]); // modify the rowtype only if the widget is enabled if (m_tabRowColPivot->ui->m_comboRows->isEnabled()) { MyMoneyReport::ERowType rt[2] = { MyMoneyReport::eExpenseIncome, MyMoneyReport::eAssetLiability }; m_currentState.setRowType(rt[m_tabRowColPivot->ui->m_comboRows->currentIndex()]); } m_currentState.setShowingRowTotals(false); if (m_tabRowColPivot->ui->m_comboRows->currentIndex() == 0) m_currentState.setShowingRowTotals(m_tabRowColPivot->ui->m_checkTotalColumn->isChecked()); m_currentState.setIncludingSchedules(m_tabRowColPivot->ui->m_checkScheduled->isChecked()); m_currentState.setIncludingTransfers(m_tabRowColPivot->ui->m_checkTransfers->isChecked()); m_currentState.setIncludingUnusedAccounts(m_tabRowColPivot->ui->m_checkUnused->isChecked()); if (m_tabRowColPivot->ui->m_comboBudget->isEnabled()) { m_currentState.setBudget(m_budgets[m_tabRowColPivot->ui->m_comboBudget->currentItem()].id(), m_initialState.rowType() == MyMoneyReport::eBudgetActual); } else { m_currentState.setBudget(QString(), false); } //set moving average days if (m_tabRowColPivot->ui->m_movingAverageDays->isEnabled()) { m_currentState.setMovingAverageDays(m_tabRowColPivot->ui->m_movingAverageDays->value()); } } else if (m_tabRowColQuery) { MyMoneyReport::ERowType rtq[8] = { MyMoneyReport::eCategory, MyMoneyReport::eTopCategory, MyMoneyReport::eTag, MyMoneyReport::ePayee, MyMoneyReport::eAccount, MyMoneyReport::eTopAccount, MyMoneyReport::eMonth, MyMoneyReport::eWeek }; m_currentState.setRowType(rtq[m_tabRowColQuery->ui->m_comboOrganizeBy->currentIndex()]); unsigned qc = MyMoneyReport::eQCnone; if (m_currentState.queryColumns() & MyMoneyReport::eQCloan) // once a loan report, always a loan report qc = MyMoneyReport::eQCloan; if (m_tabRowColQuery->ui->m_checkNumber->isChecked()) qc |= MyMoneyReport::eQCnumber; if (m_tabRowColQuery->ui->m_checkPayee->isChecked()) qc |= MyMoneyReport::eQCpayee; if (m_tabRowColQuery->ui->m_checkTag->isChecked()) qc |= MyMoneyReport::eQCtag; if (m_tabRowColQuery->ui->m_checkCategory->isChecked()) qc |= MyMoneyReport::eQCcategory; if (m_tabRowColQuery->ui->m_checkMemo->isChecked()) qc |= MyMoneyReport::eQCmemo; if (m_tabRowColQuery->ui->m_checkAccount->isChecked()) qc |= MyMoneyReport::eQCaccount; if (m_tabRowColQuery->ui->m_checkReconciled->isChecked()) qc |= MyMoneyReport::eQCreconciled; if (m_tabRowColQuery->ui->m_checkAction->isChecked()) qc |= MyMoneyReport::eQCaction; if (m_tabRowColQuery->ui->m_checkShares->isChecked()) qc |= MyMoneyReport::eQCshares; if (m_tabRowColQuery->ui->m_checkPrice->isChecked()) qc |= MyMoneyReport::eQCprice; if (m_tabRowColQuery->ui->m_checkBalance->isChecked()) qc |= MyMoneyReport::eQCbalance; m_currentState.setQueryColumns(static_cast(qc)); m_currentState.setTax(m_tabRowColQuery->ui->m_checkTax->isChecked()); m_currentState.setInvestmentsOnly(m_tabRowColQuery->ui->m_checkInvestments->isChecked()); m_currentState.setLoansOnly(m_tabRowColQuery->ui->m_checkLoans->isChecked()); m_currentState.setDetailLevel(m_tabRowColQuery->ui->m_checkHideSplitDetails->isChecked() ? MyMoneyReport::eDetailNone : MyMoneyReport::eDetailAll); m_currentState.setHideTransactions(m_tabRowColQuery->ui->m_checkHideTransactions->isChecked()); } if (m_tabChart) { MyMoneyReport::EChartType ct[5] = { MyMoneyReport::eChartLine, MyMoneyReport::eChartBar, MyMoneyReport::eChartStackedBar, MyMoneyReport::eChartPie, MyMoneyReport::eChartRing }; m_currentState.setChartType(ct[m_tabChart->ui->m_comboType->currentIndex()]); m_currentState.setChartCHGridLines(m_tabChart->ui->m_checkCHGridLines->isChecked()); m_currentState.setChartSVGridLines(m_tabChart->ui->m_checkSVGridLines->isChecked()); m_currentState.setChartDataLabels(m_tabChart->ui->m_checkValues->isChecked()); m_currentState.setChartByDefault(m_tabChart->ui->m_checkShowChart->isChecked()); m_currentState.setChartLineWidth(m_tabChart->ui->m_lineWidth->value()); m_currentState.setLogYAxis(m_tabChart->ui->m_logYaxis->isChecked()); } if (m_tabRange) { m_currentState.setDataRangeStart(m_tabRange->ui->m_dataRangeStart->text()); m_currentState.setDataRangeEnd(m_tabRange->ui->m_dataRangeEnd->text()); m_currentState.setDataMajorTick(m_tabRange->ui->m_dataMajorTick->text()); m_currentState.setDataMinorTick(m_tabRange->ui->m_dataMinorTick->text()); m_currentState.setYLabelsPrecision(m_tabRange->ui->m_yLabelsPrecision->value()); m_currentState.setDataFilter((MyMoneyReport::dataOptionE)m_tabRange->ui->m_dataLock->currentIndex()); MyMoneyReport::EColumnType ct[6] = { MyMoneyReport::eDays, MyMoneyReport::eWeeks, MyMoneyReport::eMonths, MyMoneyReport::eBiMonths, MyMoneyReport::eQuarters, MyMoneyReport::eYears }; bool dy[6] = { true, true, false, false, false, false }; m_currentState.setColumnType(ct[m_tabRange->ui->m_comboColumns->currentIndex()]); //TODO (Ace) This should be implicit in the call above. MMReport needs fixin' m_currentState.setColumnsAreDays(dy[m_tabRange->ui->m_comboColumns->currentIndex()]); } // setup the date lock MyMoneyTransactionFilter::dateOptionE range = m_dateRange->m_ui->m_dateRange->currentItem(); m_currentState.setDateFilter(range); + if (m_tabCapitalGain) { + m_currentState.setTermSeparator(m_tabCapitalGain->ui->m_termSeparator->date()); + m_currentState.setShowSTLTCapitalGains(m_tabCapitalGain->ui->m_showSTLTCapitalGains->isChecked()); + m_currentState.setSettlementPeriod(m_tabCapitalGain->ui->m_settlementPeriod->value()); + } done(true); } void KReportConfigurationFilterDlg::slotRowTypeChanged(int row) { m_tabRowColPivot->ui->m_checkTotalColumn->setEnabled(row == 0); } void KReportConfigurationFilterDlg::slotColumnTypeChanged(int row) { if ((m_tabRowColPivot->ui->m_comboBudget->isEnabled() && row < 2)) { m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotUpdateColumnsCombo() { const int monthlyIndex = 2; const int incomeExpenseIndex = 0; const bool isIncomeExpenseForecast = m_currentState.isIncludingForecast() && m_tabRowColPivot->ui->m_comboRows->currentIndex() == incomeExpenseIndex; if (isIncomeExpenseForecast && m_tabRange->ui->m_comboColumns->currentIndex() != monthlyIndex) { m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotLogAxisChanged(int state) { if (state == Qt::Checked) m_tabRange->setRangeLogarythmic(true); else m_tabRange->setRangeLogarythmic(false); } void KReportConfigurationFilterDlg::slotReset() { // // Set up the widget from the initial filter // m_currentState = m_initialState; // // Report Properties // m_tabGeneral->ui->m_editName->setText(m_initialState.name()); m_tabGeneral->ui->m_editComment->setText(m_initialState.comment()); m_tabGeneral->ui->m_checkCurrency->setChecked(m_initialState.isConvertCurrency()); m_tabGeneral->ui->m_checkFavorite->setChecked(m_initialState.isFavorite()); if (m_initialState.isIncludingPrice() || m_initialState.isSkippingZero()) { m_tabGeneral->ui->m_skipZero->setChecked(m_initialState.isSkippingZero()); } else { m_tabGeneral->ui->m_skipZero->setEnabled(false); } if (m_tabRowColPivot) { KComboBox *combo = m_tabRowColPivot->ui->m_comboDetail; switch (m_initialState.detailLevel()) { case MyMoneyReport::eDetailNone: case MyMoneyReport::eDetailEnd: case MyMoneyReport::eDetailAll: combo->setCurrentItem(i18nc("All accounts", "All"), false); break; case MyMoneyReport::eDetailTop: combo->setCurrentItem(i18n("Top-Level"), false); break; case MyMoneyReport::eDetailGroup: combo->setCurrentItem(i18n("Groups"), false); break; case MyMoneyReport::eDetailTotal: combo->setCurrentItem(i18n("Totals"), false); break; } combo = m_tabRowColPivot->ui->m_comboRows; switch (m_initialState.rowType()) { case MyMoneyReport::eExpenseIncome: case MyMoneyReport::eBudget: case MyMoneyReport::eBudgetActual: combo->setCurrentItem(i18n("Income & Expenses"), false); // income / expense break; default: combo->setCurrentItem(i18n("Assets & Liabilities"), false); // asset / liability break; } m_tabRowColPivot->ui->m_checkTotalColumn->setChecked(m_initialState.isShowingRowTotals()); slotRowTypeChanged(combo->currentIndex()); //load budgets combo if (m_initialState.rowType() == MyMoneyReport::eBudget || m_initialState.rowType() == MyMoneyReport::eBudgetActual) { m_tabRowColPivot->ui->m_comboRows->setEnabled(false); m_tabRowColPivot->ui->m_budgetFrame->setEnabled(!m_budgets.empty()); int i = 0; for (QVector::const_iterator it_b = m_budgets.constBegin(); it_b != m_budgets.constEnd(); ++it_b) { m_tabRowColPivot->ui->m_comboBudget->insertItem((*it_b).name(), i); //set the current selected item if ((m_initialState.budget() == "Any" && (*it_b).budgetStart().year() == QDate::currentDate().year()) || m_initialState.budget() == (*it_b).id()) m_tabRowColPivot->ui->m_comboBudget->setCurrentItem(i); i++; } } //set moving average days spinbox QSpinBox *spinbox = m_tabRowColPivot->ui->m_movingAverageDays; spinbox->setEnabled(m_initialState.isIncludingMovingAverage()); if (m_initialState.isIncludingMovingAverage()) { spinbox->setValue(m_initialState.movingAverageDays()); } m_tabRowColPivot->ui->m_checkScheduled->setChecked(m_initialState.isIncludingSchedules()); m_tabRowColPivot->ui->m_checkTransfers->setChecked(m_initialState.isIncludingTransfers()); m_tabRowColPivot->ui->m_checkUnused->setChecked(m_initialState.isIncludingUnusedAccounts()); } else if (m_tabRowColQuery) { KComboBox *combo = m_tabRowColQuery->ui->m_comboOrganizeBy; switch (m_initialState.rowType()) { case MyMoneyReport::eNoColumns: case MyMoneyReport::eCategory: combo->setCurrentItem(i18n("Categories"), false); break; case MyMoneyReport::eTopCategory: combo->setCurrentItem(i18n("Top Categories"), false); break; case MyMoneyReport::eTag: combo->setCurrentItem(i18n("Tags"), false); break; case MyMoneyReport::ePayee: combo->setCurrentItem(i18n("Payees"), false); break; case MyMoneyReport::eAccount: combo->setCurrentItem(i18n("Accounts"), false); break; case MyMoneyReport::eTopAccount: combo->setCurrentItem(i18n("Top Accounts"), false); break; case MyMoneyReport::eMonth: combo->setCurrentItem(i18n("Month"), false); break; case MyMoneyReport::eWeek: combo->setCurrentItem(i18n("Week"), false); break; default: throw MYMONEYEXCEPTION("KReportConfigurationFilterDlg::slotReset(): QueryTable report has invalid rowtype"); } unsigned qc = m_initialState.queryColumns(); m_tabRowColQuery->ui->m_checkNumber->setChecked(qc & MyMoneyReport::eQCnumber); m_tabRowColQuery->ui->m_checkPayee->setChecked(qc & MyMoneyReport::eQCpayee); m_tabRowColQuery->ui->m_checkTag->setChecked(qc & MyMoneyReport::eQCtag); m_tabRowColQuery->ui->m_checkCategory->setChecked(qc & MyMoneyReport::eQCcategory); m_tabRowColQuery->ui->m_checkMemo->setChecked(qc & MyMoneyReport::eQCmemo); m_tabRowColQuery->ui->m_checkAccount->setChecked(qc & MyMoneyReport::eQCaccount); m_tabRowColQuery->ui->m_checkReconciled->setChecked(qc & MyMoneyReport::eQCreconciled); m_tabRowColQuery->ui->m_checkAction->setChecked(qc & MyMoneyReport::eQCaction); m_tabRowColQuery->ui->m_checkShares->setChecked(qc & MyMoneyReport::eQCshares); m_tabRowColQuery->ui->m_checkPrice->setChecked(qc & MyMoneyReport::eQCprice); m_tabRowColQuery->ui->m_checkBalance->setChecked(qc & MyMoneyReport::eQCbalance); m_tabRowColQuery->ui->m_checkTax->setChecked(m_initialState.isTax()); m_tabRowColQuery->ui->m_checkInvestments->setChecked(m_initialState.isInvestmentsOnly()); m_tabRowColQuery->ui->m_checkLoans->setChecked(m_initialState.isLoansOnly()); m_tabRowColQuery->ui->m_checkHideTransactions->setChecked(m_initialState.isHideTransactions()); m_tabRowColQuery->ui->m_checkHideSplitDetails->setEnabled(!m_initialState.isHideTransactions()); m_tabRowColQuery->ui->m_checkHideSplitDetails->setChecked (m_initialState.detailLevel() == MyMoneyReport::eDetailNone || m_initialState.isHideTransactions()); } if (m_tabChart) { KMyMoneyGeneralCombo* combo = m_tabChart->ui->m_comboType; switch (m_initialState.chartType()) { case MyMoneyReport::eChartNone: combo->setCurrentItem(MyMoneyReport::eChartLine); break; case MyMoneyReport::eChartLine: case MyMoneyReport::eChartBar: case MyMoneyReport::eChartStackedBar: case MyMoneyReport::eChartPie: case MyMoneyReport::eChartRing: combo->setCurrentItem(m_initialState.chartType()); break; case MyMoneyReport::eChartEnd: throw MYMONEYEXCEPTION("KReportConfigurationFilterDlg::slotReset(): Report has invalid charttype"); } m_tabChart->ui->m_checkCHGridLines->setChecked(m_initialState.isChartCHGridLines()); m_tabChart->ui->m_checkSVGridLines->setChecked(m_initialState.isChartSVGridLines()); m_tabChart->ui->m_checkValues->setChecked(m_initialState.isChartDataLabels()); m_tabChart->ui->m_checkShowChart->setChecked(m_initialState.isChartByDefault()); m_tabChart->ui->m_lineWidth->setValue(m_initialState.chartLineWidth()); m_tabChart->ui->m_logYaxis->setChecked(m_initialState.isLogYAxis()); } if (m_tabRange) { m_tabRange->ui->m_dataRangeStart->setText(m_initialState.dataRangeStart()); m_tabRange->ui->m_dataRangeEnd->setText(m_initialState.dataRangeEnd()); m_tabRange->ui->m_dataMajorTick->setText(m_initialState.dataMajorTick()); m_tabRange->ui->m_dataMinorTick->setText(m_initialState.dataMinorTick()); m_tabRange->ui->m_yLabelsPrecision->setValue(m_initialState.yLabelsPrecision()); m_tabRange->ui->m_dataLock->setCurrentIndex((int)m_initialState.dataFilter()); KComboBox *combo = m_tabRange->ui->m_comboColumns; if (m_initialState.isColumnsAreDays()) { switch (m_initialState.columnType()) { case MyMoneyReport::eNoColumns: case MyMoneyReport::eDays: combo->setCurrentItem(i18nc("@item the columns will display daily data", "Daily"), false); break; case MyMoneyReport::eWeeks: combo->setCurrentItem(i18nc("@item the columns will display weekly data", "Weekly"), false); break; default: break; } } else { switch (m_initialState.columnType()) { case MyMoneyReport::eNoColumns: case MyMoneyReport::eMonths: combo->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); break; case MyMoneyReport::eBiMonths: combo->setCurrentItem(i18nc("@item the columns will display bi-monthly data", "Bi-Monthly"), false); break; case MyMoneyReport::eQuarters: combo->setCurrentItem(i18nc("@item the columns will display quarterly data", "Quarterly"), false); break; case MyMoneyReport::eYears: combo->setCurrentItem(i18nc("@item the columns will display yearly data", "Yearly"), false); break; default: break; } } } + if (m_tabCapitalGain) { + m_tabCapitalGain->ui->m_termSeparator->setDate(m_initialState.termSeparator()); + m_tabCapitalGain->ui->m_showSTLTCapitalGains->setChecked(m_initialState.isShowingSTLTCapitalGains()); + m_tabCapitalGain->ui->m_settlementPeriod->setValue(m_initialState.settlementPeriod()); + } + // // Text Filter // QRegExp textfilter; if (m_initialState.textFilter(textfilter)) { m_ui->m_textEdit->setText(textfilter.pattern()); m_ui->m_caseSensitive->setChecked(Qt::CaseSensitive == textfilter.caseSensitivity()); m_ui->m_regExp->setChecked(QRegExp::RegExp == textfilter.patternSyntax()); m_ui->m_textNegate->setCurrentIndex(m_initialState.isInvertingText()); } // // Type & State Filters // int type; if (m_initialState.firstType(type)) m_ui->m_typeBox->setCurrentIndex(type); int state; if (m_initialState.firstState(state)) m_ui->m_stateBox->setCurrentIndex(state); // // Number Filter // QString nrFrom, nrTo; if (m_initialState.numberFilter(nrFrom, nrTo)) { if (nrFrom == nrTo) { m_ui->m_nrEdit->setEnabled(true); m_ui->m_nrFromEdit->setEnabled(false); m_ui->m_nrToEdit->setEnabled(false); m_ui->m_nrEdit->setText(nrFrom); m_ui->m_nrFromEdit->setText(QString()); m_ui->m_nrToEdit->setText(QString()); m_ui->m_nrButton->setChecked(true); m_ui->m_nrRangeButton->setChecked(false); } else { m_ui->m_nrEdit->setEnabled(false); m_ui->m_nrFromEdit->setEnabled(true); m_ui->m_nrToEdit->setEnabled(false); m_ui->m_nrEdit->setText(QString()); m_ui->m_nrFromEdit->setText(nrFrom); m_ui->m_nrToEdit->setText(nrTo); m_ui->m_nrButton->setChecked(false); m_ui->m_nrRangeButton->setChecked(true); } } else { m_ui->m_nrEdit->setEnabled(true); m_ui->m_nrFromEdit->setEnabled(false); m_ui->m_nrToEdit->setEnabled(false); m_ui->m_nrEdit->setText(QString()); m_ui->m_nrFromEdit->setText(QString()); m_ui->m_nrToEdit->setText(QString()); m_ui->m_nrButton->setChecked(true); m_ui->m_nrRangeButton->setChecked(false); } // // Amount Filter // MyMoneyMoney from, to; if (m_initialState.amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); if (from == to) { m_ui->m_amountEdit->setEnabled(true); m_ui->m_amountFromEdit->setEnabled(false); m_ui->m_amountToEdit->setEnabled(false); m_ui->m_amountEdit->loadText(QString::number(from.toDouble())); m_ui->m_amountFromEdit->loadText(QString()); m_ui->m_amountToEdit->loadText(QString()); m_ui->m_amountButton->setChecked(true); m_ui->m_amountRangeButton->setChecked(false); } else { m_ui->m_amountEdit->setEnabled(false); m_ui->m_amountFromEdit->setEnabled(true); m_ui->m_amountToEdit->setEnabled(true); m_ui->m_amountEdit->loadText(QString()); m_ui->m_amountFromEdit->loadText(QString::number(from.toDouble())); m_ui->m_amountToEdit->loadText(QString::number(to.toDouble())); m_ui->m_amountButton->setChecked(false); m_ui->m_amountRangeButton->setChecked(true); } } else { m_ui->m_amountEdit->setEnabled(true); m_ui->m_amountFromEdit->setEnabled(false); m_ui->m_amountToEdit->setEnabled(false); m_ui->m_amountEdit->loadText(QString()); m_ui->m_amountFromEdit->loadText(QString()); m_ui->m_amountToEdit->loadText(QString()); m_ui->m_amountButton->setChecked(true); m_ui->m_amountRangeButton->setChecked(false); } // // Payees Filter // QStringList payees; if (m_initialState.payees(payees)) { if (payees.empty()) { m_ui->m_emptyPayeesButton->setChecked(true); } else { selectAllItems(m_ui->m_payeesView, false); selectItems(m_ui->m_payeesView, payees, true); } } else { selectAllItems(m_ui->m_payeesView, true); } // // Tags Filter // QStringList tags; if (m_initialState.tags(tags)) { if (tags.empty()) { m_ui->m_emptyTagsButton->setChecked(true); } else { selectAllItems(m_ui->m_tagsView, false); selectItems(m_ui->m_tagsView, tags, true); } } else { selectAllItems(m_ui->m_tagsView, true); } // // Accounts Filter // QStringList accounts; if (m_initialState.accounts(accounts)) { m_ui->m_accountsView->selectAllItems(false); m_ui->m_accountsView->selectItems(accounts, true); } else m_ui->m_accountsView->selectAllItems(true); // // Categories Filter // if (m_initialState.categories(accounts)) { m_ui->m_categoriesView->selectAllItems(false); m_ui->m_categoriesView->selectItems(accounts, true); } else m_ui->m_categoriesView->selectAllItems(true); // // Date Filter // // the following call implies a call to slotUpdateSelections, // that's why we call it last m_initialState.updateDateFilter(); QDate dateFrom, dateTo; if (m_initialState.dateFilter(dateFrom, dateTo)) { if (m_initialState.isDateUserDefined()) { m_dateRange->m_ui->m_dateRange->setCurrentItem(MyMoneyTransactionFilter::userDefined); m_dateRange->m_ui->m_fromDate->setDate(dateFrom); m_dateRange->m_ui->m_toDate->setDate(dateTo); } else { m_dateRange->m_ui->m_fromDate->setDate(dateFrom); m_dateRange->m_ui->m_toDate->setDate(dateTo); m_dateRange->slotDateChanged(); } } else { m_dateRange->m_ui->m_dateRange->setCurrentItem(MyMoneyTransactionFilter::allDates); m_dateRange->slotDateRangeChanged(MyMoneyTransactionFilter::allDates); } slotRightSize(); } void KReportConfigurationFilterDlg::slotDateChanged() { if (m_dateRange->m_ui->m_dateRange->currentItem() != MyMoneyTransactionFilter::userDefined) { m_dateRange->slotDateChanged(); } slotUpdateSelections(); } void KReportConfigurationFilterDlg::slotShowHelp() { KHelpClient::invokeHelp("details.reports.config"); } //TODO Fix the reports and engine to include transfers even if categories are filtered - bug #1523508 void KReportConfigurationFilterDlg::slotUpdateCheckTransfers() { QCheckBox* cb = m_tabRowColPivot->ui->m_checkTransfers; if (!m_ui->m_categoriesView->allItemsSelected()) { cb->setChecked(false); cb->setDisabled(true); } else { cb->setEnabled(true); } } diff --git a/kmymoney/dialogs/kreportconfigurationfilterdlg.h b/kmymoney/dialogs/kreportconfigurationfilterdlg.h index e30a13752..56f512e67 100644 --- a/kmymoney/dialogs/kreportconfigurationfilterdlg.h +++ b/kmymoney/dialogs/kreportconfigurationfilterdlg.h @@ -1,94 +1,95 @@ /*************************************************************************** kreportconfigurationdlg.h - description ------------------- begin : Mon Jun 21 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KREPORTCONFIGURATIONFILTERDLG_H #define KREPORTCONFIGURATIONFILTERDLG_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kfindtransactiondlg.h" #include "mymoneyreport.h" #include "mymoneybudget.h" #include "reporttabimpl.h" class ReportTabGeneral; class ReportTabRowColPivot; class ReportTabRowColQuery; class ReportTabChart; class ReportTabRange; class MyMoneyBudget; /** * @author Ace Jones */ class KReportConfigurationFilterDlg : public KFindTransactionDlg { Q_OBJECT public: explicit KReportConfigurationFilterDlg(MyMoneyReport report, QWidget *parent = 0); ~KReportConfigurationFilterDlg(); const MyMoneyReport& getConfig() const { return m_currentState; } protected: QPointer m_tabGeneral; QPointer m_tabRowColPivot; QPointer m_tabRowColQuery; QPointer m_tabChart; QPointer m_tabRange; + QPointer m_tabCapitalGain; MyMoneyReport m_initialState; MyMoneyReport m_currentState; protected slots: void slotRowTypeChanged(int); void slotColumnTypeChanged(int); void slotReset(); void slotSearch(); void slotShowHelp(); /** * This is to enable/disable the check to Include Transfers based on whether Categories are filtered or not. * This is because if Categories are filtered, transfers will not be included anyway */ virtual void slotDateChanged(); void slotUpdateCheckTransfers(); void slotUpdateColumnsCombo(); void slotLogAxisChanged(int state); private: QVector m_budgets; }; #endif diff --git a/kmymoney/mymoney/mymoneyreport.cpp b/kmymoney/mymoney/mymoneyreport.cpp index b45f23cd7..29c6bfcf1 100644 --- a/kmymoney/mymoney/mymoneyreport.cpp +++ b/kmymoney/mymoney/mymoneyreport.cpp @@ -1,838 +1,856 @@ /*************************************************************************** mymoneyreport.cpp ------------------- begin : Sun July 4 2004 copyright : (C) 2004-2005 by Ace Jones email : acejones@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneyreport.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" const QStringList MyMoneyReport::kRowTypeText = QString("none,assetliability,expenseincome,category,topcategory,account,tag,payee,month,week,topaccount,topaccount-account,equitytype,accounttype,institution,budget,budgetactual,schedule,accountinfo,accountloaninfo,accountreconcile,cashflow").split(','); const QStringList MyMoneyReport::kColumnTypeText = QString("none,months,bimonths,quarters,4,5,6,weeks,8,9,10,11,years").split(','); // if you add names here, don't forget to update the bitmap for EQueryColumns // and shift the bit for eQCend one position to the left const QStringList MyMoneyReport::kQueryColumnsText = QString("none,number,payee,category,tag,memo,account,reconcileflag,action,shares,price,performance,loan,balance,capitalgain").split(','); const MyMoneyReport::EReportType MyMoneyReport::kTypeArray[] = { eNoReport, ePivotTable, ePivotTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, ePivotTable, ePivotTable, eInfoTable, eInfoTable, eInfoTable, eQueryTable, eQueryTable, eNoReport }; const QStringList MyMoneyReport::kDetailLevelText = QString("none,all,top,group,total,invalid").split(','); const QStringList MyMoneyReport::kChartTypeText = QString("none,line,bar,pie,ring,stackedbar,invalid").split(','); // This should live in mymoney/mymoneytransactionfilter.h const QStringList kTypeText = QString("all,payments,deposits,transfers,none").split(','); const QStringList kStateText = QString("all,notreconciled,cleared,reconciled,frozen,none").split(','); const QStringList kDateLockText = QString("alldates,untiltoday,currentmonth,currentyear,monthtodate,yeartodate,yeartomonth,lastmonth,lastyear,last7days,last30days,last3months,last6months,last12months,next7days,next30days,next3months,next6months,next12months,userdefined,last3tonext3months,last11Months,currentQuarter,lastQuarter,nextQuarter,currentFiscalYear,lastFiscalYear,today,next18months").split(','); const QStringList kDataLockText = QString("automatic,userdefined").split(','); const QStringList kAccountTypeText = QString("unknown,checkings,savings,cash,creditcard,loan,certificatedep,investment,moneymarket,asset,liability,currency,income,expense,assetloan,stock,equity,invalid").split(','); MyMoneyReport::MyMoneyReport() : m_name("Unconfigured Pivot Table Report"), m_detailLevel(eDetailNone), m_hideTransactions(false), m_convertCurrency(true), m_favorite(false), m_tax(false), m_investments(false), m_loans(false), m_reportType(kTypeArray[eExpenseIncome]), m_rowType(eExpenseIncome), m_columnType(eMonths), m_columnsAreDays(false), m_queryColumns(eQCnone), m_dateLock(MyMoneyTransactionFilter::userDefined), m_accountGroupFilter(false), m_chartType(eChartLine), m_chartDataLabels(true), m_chartCHGridLines(true), m_chartSVGridLines(true), m_chartByDefault(false), m_logYaxis(false), m_dataLock(MyMoneyReport::automatic), m_includeSchedules(false), m_includeTransfers(false), m_includeBudgetActuals(false), m_includeUnusedAccounts(false), m_showRowTotals(false), m_includeForecast(false), m_includeMovingAverage(false), m_movingAverageDays(0), m_includePrice(false), m_includeAveragePrice(false), m_mixedTime(false), m_currentDateColumn(0), + m_settlementPeriod(3), + m_showSTLTCapitalGains(false), + m_tseparator(QDate::currentDate().addYears(-1)), m_skipZero(false) { m_chartLineWidth = m_lineWidth; } MyMoneyReport::MyMoneyReport(const QString& id, const MyMoneyReport& right) : MyMoneyObject(id), m_movingAverageDays(0), m_currentDateColumn(0) { *this = right; setId(id); } MyMoneyReport::MyMoneyReport(ERowType _rt, unsigned _ct, dateOptionE _dl, EDetailLevel _ss, const QString& _name, const QString& _comment) : m_name(_name), m_comment(_comment), m_detailLevel(_ss), m_hideTransactions(false), m_convertCurrency(true), m_favorite(false), m_tax(false), m_investments(false), m_loans(false), m_reportType(kTypeArray[_rt]), m_rowType(_rt), m_columnsAreDays(false), m_queryColumns(eQCnone), m_dateLock(_dl), m_accountGroupFilter(false), m_chartType(eChartLine), m_chartDataLabels(true), m_chartCHGridLines(true), m_chartSVGridLines(true), m_chartByDefault(false), m_logYaxis(false), m_dataLock(MyMoneyReport::automatic), m_includeSchedules(false), m_includeTransfers(false), m_includeBudgetActuals(false), m_includeUnusedAccounts(false), m_showRowTotals(false), m_includeForecast(false), m_includeMovingAverage(false), m_movingAverageDays(0), m_includePrice(false), m_includeAveragePrice(false), m_mixedTime(false), m_currentDateColumn(0), + m_settlementPeriod(3), + m_showSTLTCapitalGains(false), + m_tseparator(QDate::currentDate().addYears(-1)), m_skipZero(false) { //set initial values m_chartLineWidth = m_lineWidth; //set report type if (m_reportType == ePivotTable) m_columnType = static_cast(_ct); if (m_reportType == eQueryTable) m_queryColumns = static_cast(_ct); setDateFilter(_dl); //throw exception if the type is inconsistent if ((_rt > static_cast(sizeof(kTypeArray) / sizeof(kTypeArray[0]))) || (m_reportType == eNoReport)) throw MYMONEYEXCEPTION("Invalid report type"); //add the corresponding account groups if (_rt == MyMoneyReport::eAssetLiability) { addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); m_showRowTotals = true; } if (_rt == MyMoneyReport::eExpenseIncome) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); m_showRowTotals = true; } //FIXME take this out once we have sorted out all issues regarding budget of assets and liabilities -- asoliverez@gmail.com if (_rt == MyMoneyReport::eBudget || _rt == MyMoneyReport::eBudgetActual) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); } if (_rt == MyMoneyReport::eAccountInfo) { addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); } //cash flow reports show splits for all account groups if (_rt == MyMoneyReport::eCashFlow) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); } } MyMoneyReport::MyMoneyReport(const QDomElement& node) : MyMoneyObject(node), m_currentDateColumn(0) { // properly initialize the object before reading it *this = MyMoneyReport(); if (!read(node)) clearId(); } void MyMoneyReport::clear() { m_accountGroupFilter = false; m_accountGroups.clear(); MyMoneyTransactionFilter::clear(); } void MyMoneyReport::validDateRange(QDate& _db, QDate& _de) { _db = fromDate(); _de = toDate(); // if either begin or end date are invalid we have one of the following // possible date filters: // // a) begin date not set - first transaction until given end date // b) end date not set - from given date until last transaction // c) both not set - first transaction until last transaction // // If there is no transaction in the engine at all, we use the current // year as the filter criteria. if (!_db.isValid() || !_de.isValid()) { QList list = MyMoneyFile::instance()->transactionList(*this); QDate tmpBegin, tmpEnd; if (!list.isEmpty()) { qSort(list); // try to use the post dates tmpBegin = list.front().postDate(); tmpEnd = list.back().postDate(); // if the post dates are not valid try the entry dates if (!tmpBegin.isValid()) tmpBegin = list.front().entryDate(); if (!tmpEnd.isValid()) tmpEnd = list.back().entryDate(); } // make sure that we leave this function with valid dates no mather what if (!tmpBegin.isValid() || !tmpEnd.isValid() || tmpBegin > tmpEnd) { tmpBegin = QDate(QDate::currentDate().year(), 1, 1); // the first date in the file tmpEnd = QDate(QDate::currentDate().year(), 12, 31); // the last date in the file } if (!_db.isValid()) _db = tmpBegin; if (!_de.isValid()) _de = tmpEnd; } if (_db > _de) _db = _de; } void MyMoneyReport::setRowType(ERowType _rt) { m_rowType = _rt; m_reportType = kTypeArray[_rt]; m_accountGroupFilter = false; m_accountGroups.clear(); if (_rt == MyMoneyReport::eAssetLiability) { addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); } if (_rt == MyMoneyReport::eExpenseIncome) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); } } bool MyMoneyReport::accountGroups(QList& list) const { bool result = m_accountGroupFilter; if (result) { QList::const_iterator it_group = m_accountGroups.begin(); while (it_group != m_accountGroups.end()) { list += (*it_group); ++it_group; } } return result; } void MyMoneyReport::addAccountGroup(MyMoneyAccount::accountTypeE type) { if (!m_accountGroups.isEmpty() && type != MyMoneyAccount::UnknownAccountType) { if (m_accountGroups.contains(type)) return; } m_accountGroupFilter = true; if (type != MyMoneyAccount::UnknownAccountType) m_accountGroups.push_back(type); } bool MyMoneyReport::includesAccountGroup(MyMoneyAccount::accountTypeE type) const { bool result = (! m_accountGroupFilter) || (isIncludingTransfers() && m_rowType == MyMoneyReport::eExpenseIncome) || m_accountGroups.contains(type); return result; } bool MyMoneyReport::includes(const MyMoneyAccount& acc) const { bool result = false; if (includesAccountGroup(acc.accountGroup())) { switch (acc.accountGroup()) { case MyMoneyAccount::Income: case MyMoneyAccount::Expense: if (isTax()) result = (acc.value("Tax") == "Yes") && includesCategory(acc.id()); else result = includesCategory(acc.id()); break; case MyMoneyAccount::Asset: case MyMoneyAccount::Liability: if (isLoansOnly()) result = acc.isLoan() && includesAccount(acc.id()); else if (isInvestmentsOnly()) result = acc.isInvest() && includesAccount(acc.id()); else if (isIncludingTransfers() && m_rowType == MyMoneyReport::eExpenseIncome) // If transfers are included, ONLY include this account if it is NOT // included in the report itself!! result = ! includesAccount(acc.id()); else result = includesAccount(acc.id()); break; default: result = includesAccount(acc.id()); } } return result; } void MyMoneyReport::write(QDomElement& e, QDomDocument *doc, bool anonymous) const { // No matter what changes, be sure to have a 'type' attribute. Only change // the major type if it becomes impossible to maintain compatibility with // older versions of the program as new features are added to the reports. // Feel free to change the minor type every time a change is made here. writeBaseXML(*doc, e); if (anonymous) { e.setAttribute("name", m_id); e.setAttribute("comment", QString(m_comment).fill('x')); } else { e.setAttribute("name", m_name); e.setAttribute("comment", m_comment); } e.setAttribute("group", m_group); e.setAttribute("convertcurrency", m_convertCurrency); e.setAttribute("favorite", m_favorite); e.setAttribute("tax", m_tax); e.setAttribute("investments", m_investments); e.setAttribute("loans", m_loans); e.setAttribute("hidetransactions", m_hideTransactions); e.setAttribute("rowtype", kRowTypeText[m_rowType]); e.setAttribute("datelock", kDateLockText[m_dateLock]); e.setAttribute("datalock", kDataLockText[m_dataLock]); e.setAttribute("includeschedules", m_includeSchedules); e.setAttribute("columnsaredays", m_columnsAreDays); e.setAttribute("includestransfers", m_includeTransfers); if (!m_budgetId.isEmpty()) e.setAttribute("budget", m_budgetId); e.setAttribute("includesactuals", m_includeBudgetActuals); e.setAttribute("includeunused", m_includeUnusedAccounts); e.setAttribute("includesforecast", m_includeForecast); e.setAttribute("includesprice", m_includePrice); e.setAttribute("includesaverageprice", m_includeAveragePrice); e.setAttribute("mixedtime", m_mixedTime); e.setAttribute("includesmovingaverage", m_includeMovingAverage); if (m_includeMovingAverage) e.setAttribute("movingaveragedays", m_movingAverageDays); if (m_chartType < 0 || m_chartType >= kChartTypeText.size()) { qDebug("m_chartType out of bounds with %d on report of type %d. Default to none.", m_chartType, m_reportType); e.setAttribute("charttype", kChartTypeText[0]); } else { e.setAttribute("charttype", kChartTypeText[m_chartType]); } e.setAttribute("chartdatalabels", m_chartDataLabels); e.setAttribute("chartchgridlines", m_chartCHGridLines); e.setAttribute("chartsvgridlines", m_chartSVGridLines); e.setAttribute("chartbydefault", m_chartByDefault); e.setAttribute("chartlinewidth", m_chartLineWidth); e.setAttribute("logYaxis", m_logYaxis); e.setAttribute("dataRangeStart", m_dataRangeStart); e.setAttribute("dataRangeEnd", m_dataRangeEnd); e.setAttribute("dataMajorTick", m_dataMajorTick); e.setAttribute("dataMinorTick", m_dataMinorTick); e.setAttribute("yLabelsPrecision", m_yLabelsPrecision); e.setAttribute("dataLock", m_dataLock); e.setAttribute("skipZero", m_skipZero); e.setAttribute("detail", kDetailLevelText[m_detailLevel]); if (m_reportType == ePivotTable) { e.setAttribute("type", "pivottable 1.15"); e.setAttribute("columntype", kColumnTypeText[m_columnType]); e.setAttribute("showrowtotals", m_showRowTotals); } else if (m_reportType == eQueryTable) { e.setAttribute("type", "querytable 1.14"); QStringList columns; unsigned qc = m_queryColumns; unsigned it_qc = eQCbegin; unsigned index = 1; while (it_qc != eQCend) { if (qc & it_qc) columns += kQueryColumnsText[index]; it_qc *= 2; index++; } e.setAttribute("querycolumns", columns.join(",")); + + if (m_queryColumns & eQCcapitalgain) { + e.setAttribute("settlementperiod", m_settlementPeriod); + e.setAttribute("showSTLTCapitalGains", m_showSTLTCapitalGains); + e.setAttribute("tseparator", m_tseparator.toString(Qt::ISODate)); + } } else if (m_reportType == eInfoTable) { e.setAttribute("type", "infotable 1.0"); e.setAttribute("showrowtotals", m_showRowTotals); } // // Text Filter // QRegExp textfilter; if (textFilter(textfilter)) { QDomElement f = doc->createElement("TEXT"); f.setAttribute("pattern", textfilter.pattern()); f.setAttribute("casesensitive", (textfilter.caseSensitivity() == Qt::CaseSensitive) ? 1 : 0); f.setAttribute("regex", (textfilter.patternSyntax() == QRegExp::Wildcard) ? 1 : 0); f.setAttribute("inverttext", m_invertText); e.appendChild(f); } // // Type & State Filters // QList typelist; if (types(typelist) && ! typelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_type = typelist.constBegin(); while (it_type != typelist.constEnd()) { QDomElement p = doc->createElement("TYPE"); p.setAttribute("type", kTypeText[*it_type]); e.appendChild(p); ++it_type; } } QList statelist; if (states(statelist) && ! statelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_state = statelist.constBegin(); while (it_state != statelist.constEnd()) { QDomElement p = doc->createElement("STATE"); p.setAttribute("state", kStateText[*it_state]); e.appendChild(p); ++it_state; } } // // Number Filter // QString nrFrom, nrTo; if (numberFilter(nrFrom, nrTo)) { QDomElement f = doc->createElement("NUMBER"); f.setAttribute("from", nrFrom); f.setAttribute("to", nrTo); e.appendChild(f); } // // Amount Filter // MyMoneyMoney from, to; if (amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); QDomElement f = doc->createElement("AMOUNT"); f.setAttribute("from", from.toString()); f.setAttribute("to", to.toString()); e.appendChild(f); } // // Payees Filter // QStringList payeelist; if (payees(payeelist)) { if (payeelist.empty()) { QDomElement p = doc->createElement("PAYEE"); e.appendChild(p); } else { // iterate over payees, and add each one QStringList::const_iterator it_payee = payeelist.constBegin(); while (it_payee != payeelist.constEnd()) { QDomElement p = doc->createElement("PAYEE"); p.setAttribute("id", *it_payee); e.appendChild(p); ++it_payee; } } } // // Tags Filter // QStringList taglist; if (tags(taglist)) { if (taglist.empty()) { QDomElement p = doc->createElement("TAG"); e.appendChild(p); } else { // iterate over tags, and add each one QStringList::const_iterator it_tag = taglist.constBegin(); while (it_tag != taglist.constEnd()) { QDomElement p = doc->createElement("TAG"); p.setAttribute("id", *it_tag); e.appendChild(p); ++it_tag; } } } // // Account Groups Filter // QList accountgrouplist; if (accountGroups(accountgrouplist)) { // iterate over accounts, and add each one QList::const_iterator it_group = accountgrouplist.constBegin(); while (it_group != accountgrouplist.constEnd()) { QDomElement p = doc->createElement("ACCOUNTGROUP"); p.setAttribute("group", kAccountTypeText[*it_group]); e.appendChild(p); ++it_group; } } // // Accounts Filter // QStringList accountlist; if (accounts(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement("ACCOUNT"); p.setAttribute("id", *it_account); e.appendChild(p); ++it_account; } } // // Categories Filter // accountlist.clear(); if (categories(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement("CATEGORY"); p.setAttribute("id", *it_account); e.appendChild(p); ++it_account; } } // // Date Filter // if (m_dateLock == MyMoneyTransactionFilter::userDefined) { QDate dateFrom, dateTo; if (dateFilter(dateFrom, dateTo)) { QDomElement f = doc->createElement("DATES"); if (dateFrom.isValid()) f.setAttribute("from", dateFrom.toString(Qt::ISODate)); if (dateTo.isValid()) f.setAttribute("to", dateTo.toString(Qt::ISODate)); e.appendChild(f); } } } bool MyMoneyReport::read(const QDomElement& e) { // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any report ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // report types supported in this version, of course) bool result = false; if ( "REPORT" == e.tagName() && ( (e.attribute("type").indexOf("pivottable 1.") == 0) || (e.attribute("type").indexOf("querytable 1.") == 0) || (e.attribute("type").indexOf("infotable 1.") == 0) ) ) { result = true; clear(); int i; m_name = e.attribute("name"); m_comment = e.attribute("comment", "Extremely old report"); //set report type if (!e.attribute("type").indexOf("pivottable")) { m_reportType = MyMoneyReport::ePivotTable; } else if (!e.attribute("type").indexOf("querytable")) { m_reportType = MyMoneyReport::eQueryTable; } else if (!e.attribute("type").indexOf("infotable")) { m_reportType = MyMoneyReport::eInfoTable; } else { m_reportType = MyMoneyReport::eNoReport; } // Removed the line that screened out loading reports that are called // "Default Report". It's possible for the user to change the comment // to this, and we'd hate for it to break as a result. m_group = e.attribute("group"); m_id = e.attribute("id"); //check for reports with older settings which didn't have the detail attribute if (e.hasAttribute("detail")) { i = kDetailLevelText.indexOf(e.attribute("detail", "all")); if (i != -1) m_detailLevel = static_cast(i); } else if (e.attribute("showsubaccounts", "0").toUInt()) { //set to show all accounts m_detailLevel = eDetailAll; } else { //set to show the top level account instead m_detailLevel = eDetailTop; } m_convertCurrency = e.attribute("convertcurrency", "1").toUInt(); m_favorite = e.attribute("favorite", "0").toUInt(); m_tax = e.attribute("tax", "0").toUInt(); m_investments = e.attribute("investments", "0").toUInt(); m_loans = e.attribute("loans", "0").toUInt(); m_hideTransactions = e.attribute("hidetransactions", "0").toUInt(); m_includeSchedules = e.attribute("includeschedules", "0").toUInt(); m_columnsAreDays = e.attribute("columnsaredays", "0").toUInt(); m_includeTransfers = e.attribute("includestransfers", "0").toUInt(); if (e.hasAttribute("budget")) m_budgetId = e.attribute("budget"); m_includeBudgetActuals = e.attribute("includesactuals", "0").toUInt(); m_includeUnusedAccounts = e.attribute("includeunused", "0").toUInt(); m_includeForecast = e.attribute("includesforecast", "0").toUInt(); m_includePrice = e.attribute("includesprice", "0").toUInt(); m_includeAveragePrice = e.attribute("includesaverageprice", "0").toUInt(); m_mixedTime = e.attribute("mixedtime", "0").toUInt(); m_includeMovingAverage = e.attribute("includesmovingaverage", "0").toUInt(); m_skipZero = e.attribute("skipZero", "0").toUInt(); if (m_includeMovingAverage) m_movingAverageDays = e.attribute("movingaveragedays", "1").toUInt(); //only load chart data if it is a pivot table m_chartType = static_cast(0); if (m_reportType == ePivotTable) { i = kChartTypeText.indexOf(e.attribute("charttype")); if (i >= 0) m_chartType = static_cast(i); // if it is invalid, set to first type if (m_chartType >= eChartEnd) m_chartType = eChartLine; m_chartDataLabels = e.attribute("chartdatalabels", "1").toUInt(); m_chartCHGridLines = e.attribute("chartchgridlines", "1").toUInt(); m_chartSVGridLines = e.attribute("chartsvgridlines", "1").toUInt(); m_chartByDefault = e.attribute("chartbydefault", "0").toUInt(); m_chartLineWidth = e.attribute("chartlinewidth", QString(m_lineWidth)).toUInt(); m_logYaxis = e.attribute("logYaxis", "0").toUInt(); m_dataRangeStart = e.attribute("dataRangeStart", "0"); m_dataRangeEnd = e.attribute("dataRangeEnd", "0"); m_dataMajorTick = e.attribute("dataMajorTick", "0"); m_dataMinorTick = e.attribute("dataMinorTick", "0"); m_yLabelsPrecision = e.attribute("yLabelsPrecision", "2").toUInt(); } else { m_chartDataLabels = true; m_chartCHGridLines = true; m_chartSVGridLines = true; m_chartByDefault = false; m_chartLineWidth = 1; m_logYaxis = false; } QString datelockstr = e.attribute("datelock", "userdefined"); // Handle the pivot 1.2/query 1.1 case where the values were saved as // numbers bool ok = false; i = datelockstr.toUInt(&ok); if (!ok) { i = kDateLockText.indexOf(datelockstr); if (i == -1) i = MyMoneyTransactionFilter::userDefined; } setDateFilter(static_cast(i)); QString datalockstr = e.attribute("dataLock", "userdefined"); ok = false; i = datalockstr.toUInt(&ok); if (!ok) { i = kDataLockText.indexOf(datalockstr); if (i == -1) i = MyMoneyReport::userDefined; } setDataFilter(static_cast(i)); i = kRowTypeText.indexOf(e.attribute("rowtype", "expenseincome")); if (i != -1) { setRowType(static_cast(i)); // recent versions of KMyMoney always showed a total column for // income/expense reports. We turn it on for backward compatibility // here. If the total column is turned off, the flag will be reset // in the next step if (i == eExpenseIncome) m_showRowTotals = true; } if (e.hasAttribute("showrowtotals")) m_showRowTotals = e.attribute("showrowtotals").toUInt(); i = kColumnTypeText.indexOf(e.attribute("columntype", "months")); if (i != -1) setColumnType(static_cast(i)); unsigned qc = 0; QStringList columns = e.attribute("querycolumns", "none").split(','); QStringList::const_iterator it_column = columns.constBegin(); while (it_column != columns.constEnd()) { i = kQueryColumnsText.indexOf(*it_column); if (i > 0) qc |= (1 << (i - 1)); ++it_column; } setQueryColumns(static_cast(qc)); + if (m_reportType == eQueryTable && m_queryColumns & eQCcapitalgain) { + m_showSTLTCapitalGains = e.attribute("showSTLTCapitalGains", "0").toUInt(); + m_settlementPeriod = e.attribute("settlementperiod", "3").toUInt(); + m_tseparator = QDate::fromString(e.attribute("tseparator", QDate::currentDate().addYears(-1).toString(Qt::ISODate)),Qt::ISODate); + } + QDomNode child = e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if ("TEXT" == c.tagName() && c.hasAttribute("pattern")) { setTextFilter(QRegExp(c.attribute("pattern"), c.attribute("casesensitive", "1").toUInt() ? Qt::CaseSensitive : Qt::CaseInsensitive, c.attribute("regex", "1").toUInt() ? QRegExp::Wildcard : QRegExp::RegExp), c.attribute("inverttext", "0").toUInt()); } if ("TYPE" == c.tagName() && c.hasAttribute("type")) { i = kTypeText.indexOf(c.attribute("type")); if (i != -1) addType(i); } if ("STATE" == c.tagName() && c.hasAttribute("state")) { i = kStateText.indexOf(c.attribute("state")); if (i != -1) addState(i); } if ("NUMBER" == c.tagName()) { setNumberFilter(c.attribute("from"), c.attribute("to")); } if ("AMOUNT" == c.tagName()) { setAmountFilter(MyMoneyMoney(c.attribute("from", "0/100")), MyMoneyMoney(c.attribute("to", "0/100"))); } if ("DATES" == c.tagName()) { QDate from, to; if (c.hasAttribute("from")) from = QDate::fromString(c.attribute("from"), Qt::ISODate); if (c.hasAttribute("to")) to = QDate::fromString(c.attribute("to"), Qt::ISODate); MyMoneyTransactionFilter::setDateFilter(from, to); } if ("PAYEE" == c.tagName()) { addPayee(c.attribute("id")); } if ("TAG" == c.tagName()) { addTag(c.attribute("id")); } if ("CATEGORY" == c.tagName() && c.hasAttribute("id")) { addCategory(c.attribute("id")); } if ("ACCOUNT" == c.tagName() && c.hasAttribute("id")) { addAccount(c.attribute("id")); } if ("ACCOUNTGROUP" == c.tagName() && c.hasAttribute("group")) { i = kAccountTypeText.indexOf(c.attribute("group")); if (i != -1) addAccountGroup(static_cast(i)); } child = child.nextSibling(); } } return result; } void MyMoneyReport::writeXML(QDomDocument& document, QDomElement& parent) const { QDomElement el = document.createElement("REPORT"); write(el, &document, false); parent.appendChild(el); } bool MyMoneyReport::hasReferenceTo(const QString& id) const { QStringList list; // collect all ids accounts(list); categories(list); payees(list); tags(list); return (list.contains(id) > 0); } int MyMoneyReport::m_lineWidth = 2; void MyMoneyReport::setLineWidth(int width) { m_lineWidth = width; } diff --git a/kmymoney/mymoney/mymoneyreport.h b/kmymoney/mymoney/mymoneyreport.h index 5ef237205..462e9d0ab 100644 --- a/kmymoney/mymoney/mymoneyreport.h +++ b/kmymoney/mymoney/mymoneyreport.h @@ -1,791 +1,820 @@ /*************************************************************************** mymoneyreport.h ------------------- begin : Sun July 4 2004 copyright : (C) 2004-2005 by Ace Jones email : acejones@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef MYMONEYREPORT_H #define MYMONEYREPORT_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include class QDomElement; class QDomDocument; // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include /** * This class defines a report within the MyMoneyEngine. The report class * contains all the configuration parameters needed to run a report, plus * XML serialization. * * A report is a transactionfilter, so any report can specify which * transactions it's interested down to the most minute level of detail. * It extends the transactionfilter by providing identification (name, * comments, group type, etc) as well as layout information (what kind * of layout should be used, how the rows & columns should be presented, * currency converted, etc.) * * As noted above, this class only provides a report DEFINITION. The * generation and presentation of the report itself are left to higher * level classes. * * @author Ace Jones */ class KMM_MYMONEY_EXPORT MyMoneyReport: public MyMoneyObject, public MyMoneyTransactionFilter { public: // When adding a new row type, be sure to add a corresponding entry in kTypeArray enum ERowType { eNoRows = 0, eAssetLiability, eExpenseIncome, eCategory, eTopCategory, eAccount, eTag, ePayee, eMonth, eWeek, eTopAccount, eAccountByTopAccount, eEquityType, eAccountType, eInstitution, eBudget, eBudgetActual, eSchedule, eAccountInfo, eAccountLoanInfo, eAccountReconcile, eCashFlow}; enum EReportType { eNoReport = 0, ePivotTable, eQueryTable, eInfoTable }; enum EColumnType { eNoColumns = 0, eDays = 1, eMonths = 1, eBiMonths = 2, eQuarters = 3, eWeeks = 7, eYears = 12 }; // if you add bits to this bitmask, start with the value currently assigned to eQCend and update its value afterwards // also don't forget to add column names to kQueryColumnsText in mymoneyreport.cpp enum EQueryColumns { eQCnone = 0x0, eQCbegin = 0x1, eQCnumber = 0x1, eQCpayee = 0x2, eQCcategory = 0x4, eQCtag = 0x8, eQCmemo = 0x10, eQCaccount = 0x20, eQCreconciled = 0x40, eQCaction = 0x80, eQCshares = 0x100, eQCprice = 0x200, eQCperformance = 0x400, eQCloan = 0x800, eQCbalance = 0x1000, eQCcapitalgain = 0x2000, eQCend = 0x4000 }; enum EDetailLevel { eDetailNone = 0, eDetailAll, eDetailTop, eDetailGroup, eDetailTotal, eDetailEnd }; enum EChartType { eChartNone = 0, eChartLine, eChartBar, eChartPie, eChartRing, eChartStackedBar, eChartEnd }; enum dataOptionE { automatic = 0, userDefined, dataOptionCount }; static const QStringList kRowTypeText; static const QStringList kColumnTypeText; static const QStringList kQueryColumnsText; static const QStringList kDetailLevelText; static const QStringList kChartTypeText; static const EReportType kTypeArray[]; public: MyMoneyReport(); MyMoneyReport(ERowType _rt, unsigned _ct, dateOptionE _dl, EDetailLevel _ss, const QString& _name, const QString& _comment); MyMoneyReport(const QString& id, const MyMoneyReport& right); /** * This constructor creates an object based on the data found in the * QDomElement referenced by @p node. If problems arise, the @p id of * the object is cleared (see MyMoneyObject::clearId()). */ MyMoneyReport(const QDomElement& node); // Simple get operations const QString& name() const { return m_name; } bool isShowingRowTotals() const { return (m_showRowTotals); } EReportType reportType() const { return m_reportType; } ERowType rowType() const { return m_rowType; } EColumnType columnType() const { return m_columnType; } bool isRunningSum() const { return (m_rowType == eAssetLiability); } bool isConvertCurrency() const { return m_convertCurrency; } unsigned columnPitch() const { return static_cast(m_columnType); } bool isShowingColumnTotals() const { return m_convertCurrency; } const QString& comment() const { return m_comment; } EQueryColumns queryColumns() const { return m_queryColumns; } const QString& group() const { return m_group; } bool isFavorite() const { return m_favorite; } bool isTax() const { return m_tax; } bool isInvestmentsOnly() const { return m_investments; } bool isLoansOnly() const { return m_loans; } EDetailLevel detailLevel() const { return m_detailLevel; } bool isHideTransactions() const { return m_hideTransactions; } EChartType chartType() const { return m_chartType; } bool isChartDataLabels() const { return m_chartDataLabels; } bool isChartCHGridLines() const { return m_chartCHGridLines; } bool isChartSVGridLines() const { return m_chartSVGridLines; } bool isChartByDefault() const { return m_chartByDefault; } uint chartLineWidth() const { return m_chartLineWidth; } bool isLogYAxis() const { return m_logYaxis; } const QString& dataRangeStart() const { return m_dataRangeStart; } const QString& dataRangeEnd() const { return m_dataRangeEnd; } const QString& dataMajorTick() const { return m_dataMajorTick; } const QString& dataMinorTick() const { return m_dataMinorTick; } uint yLabelsPrecision() const { return m_yLabelsPrecision; } bool isIncludingSchedules() const { return m_includeSchedules; } bool isColumnsAreDays() const { return m_columnsAreDays; } bool isIncludingTransfers() const { return m_includeTransfers; } bool isIncludingUnusedAccounts() const { return m_includeUnusedAccounts; } bool hasBudget() const { return !m_budgetId.isEmpty(); } const QString& budget() const { return m_budgetId; } bool isIncludingBudgetActuals() const { return m_includeBudgetActuals; } bool isIncludingForecast() const { return m_includeForecast; } bool isIncludingMovingAverage() const { return m_includeMovingAverage; } int movingAverageDays() const { return m_movingAverageDays; } bool isIncludingPrice() const { return m_includePrice; } bool isIncludingAveragePrice() const { return m_includeAveragePrice; } bool isDateUserDefined() const { return m_dateLock == MyMoneyTransactionFilter::userDefined; } bool isDataUserDefined() const { return m_dataLock == MyMoneyReport::userDefined; } bool isMixedTime() const { return m_mixedTime; } int currentDateColumn() const { return m_currentDateColumn; } + uint settlementPeriod() const { + return m_settlementPeriod; + } + bool isShowingSTLTCapitalGains() const { + return m_showSTLTCapitalGains; + } + const QDate& termSeparator() const { + return m_tseparator; + } /** * @see #m_skipZero */ bool isSkippingZero() const { return m_skipZero; } // Simple set operations void setName(const QString& _s) { m_name = _s; } void setConvertCurrency(bool _f) { m_convertCurrency = _f; } void setRowType(ERowType _rt); void setColumnType(EColumnType _ct) { m_columnType = _ct; } void setComment(const QString& _comment) { m_comment = _comment; } void setGroup(const QString& _group) { m_group = _group; } void setFavorite(bool _f) { m_favorite = _f; } void setQueryColumns(EQueryColumns _qc) { m_queryColumns = _qc; } void setTax(bool _f) { m_tax = _f; } void setInvestmentsOnly(bool _f) { m_investments = _f; if (_f) m_loans = false; } void setLoansOnly(bool _f) { m_loans = _f; if (_f) m_investments = false; } void setDetailLevel(EDetailLevel _detail) { m_detailLevel = _detail; } void setHideTransactions(bool _f) { m_hideTransactions = _f; } void setChartType(EChartType _type) { m_chartType = _type; } void setChartDataLabels(bool _f) { m_chartDataLabels = _f; } void setChartCHGridLines(bool _f) { m_chartCHGridLines = _f; } void setChartSVGridLines(bool _f) { m_chartSVGridLines = _f; } void setChartByDefault(bool _f) { m_chartByDefault = _f; } void setChartLineWidth(uint _f) { m_chartLineWidth = _f; } void setLogYAxis(bool _f) { m_logYaxis = _f; } void setDataRangeStart(const QString& _f) { m_dataRangeStart = _f; } void setDataRangeEnd(const QString& _f) { m_dataRangeEnd = _f; } void setDataMajorTick(const QString& _f) { m_dataMajorTick = _f; } void setDataMinorTick(const QString& _f) { m_dataMinorTick = _f; } void setYLabelsPrecision(int _f) { m_yLabelsPrecision = _f; } void setIncludingSchedules(bool _f) { m_includeSchedules = _f; } void setColumnsAreDays(bool _f) { m_columnsAreDays = _f; } void setIncludingTransfers(bool _f) { m_includeTransfers = _f; } void setIncludingUnusedAccounts(bool _f) { m_includeUnusedAccounts = _f; } void setShowingRowTotals(bool _f) { m_showRowTotals = _f; } void setIncludingBudgetActuals(bool _f) { m_includeBudgetActuals = _f; } void setIncludingForecast(bool _f) { m_includeForecast = _f; } void setIncludingMovingAverage(bool _f) { m_includeMovingAverage = _f; } void setMovingAverageDays(int _days) { m_movingAverageDays = _days; } void setIncludingPrice(bool _f) { m_includePrice = _f; } void setIncludingAveragePrice(bool _f) { m_includeAveragePrice = _f; } void setMixedTime(bool _f) { m_mixedTime = _f; } void setCurrentDateColumn(int _f) { m_currentDateColumn = _f; } + void setSettlementPeriod(uint _days) { + m_settlementPeriod = _days; + } + void setShowSTLTCapitalGains(bool _f) { + m_showSTLTCapitalGains = _f; + } + void setTermSeparator(const QDate& _date) { + m_tseparator = _date; + } /** * @see #m_skipZero */ void setSkipZero(int _f) { m_skipZero = _f; } /** * Sets the budget used for this report * * @param _budget The ID of the budget to use, or an empty string * to indicate a budget is NOT included * @param _fa Whether to display actual data alongside the budget. * Setting to false means the report displays ONLY the budget itself. * @warning For now, the budget ID is ignored. The budget id is * simply checked for any non-empty string, and if so, hasBudget() * will return true. */ void setBudget(const QString& _budget, bool _fa = true) { m_budgetId = _budget; m_includeBudgetActuals = _fa; } /** * This method allows you to clear the underlying transaction filter */ void clear(); /** * This method allows you to set the underlying transaction filter * * @param _filter The filter which should replace the existing transaction * filter. */ void assignFilter(const MyMoneyTransactionFilter& _filter) { MyMoneyTransactionFilter::operator=(_filter); } /** * Set the underlying date filter and LOCK that filter to the specified * range. For example, if @p _u is "CurrentMonth", this report should always * be updated to the current month no matter when the report is run. * * This updating is not entirely automatic, you should update it yourself by * calling updateDateFilter. * * @param _u The date range constant (MyMoneyTransactionFilter::dateRangeE) * which this report should be locked to. */ void setDateFilter(dateOptionE _u) { m_dateLock = _u; if (_u != MyMoneyTransactionFilter::userDefined) MyMoneyTransactionFilter::setDateFilter(_u); } void setDataFilter(dataOptionE _u) { m_dataLock = _u; } /** * Set the underlying date filter using the start and end dates provided. * Note that this does not LOCK to any range like setDateFilter(unsigned) * above. It is just a reimplementation of the MyMoneyTransactionFilter * version. * * @param _db The inclusive begin date of the date range * @param _de The inclusive end date of the date range */ void setDateFilter(const QDate& _db, const QDate& _de) { MyMoneyTransactionFilter::setDateFilter(_db, _de); } /** * Set the underlying date filter using the 'date lock' property. * * Always call this function before executing the report to be sure that * the date filters properly match the plain-language 'date lock'. * * For example, if the report is date-locked to "Current Month", and the * last time you loaded or ran the report was in August, but it's now * September, this function will update the date range to be September, * as is proper. */ void updateDateFilter() { if (m_dateLock != MyMoneyTransactionFilter::userDefined) MyMoneyTransactionFilter::setDateFilter(m_dateLock); } MyMoneyReport::dataOptionE dataFilter() { return m_dataLock; } /** * Retrieves a VALID beginning & ending date for this report. * * The underlying date filter can return en empty QDate() for either the * begin or end date or both. This is typically unacceptable for reports, * which need the REAL begin and end date. * * This function gets the underlying date filter range, and if either is * an empty QDate(), it determines the missing date from looking at all * the transactions which match the underlying filter, and returning the * date of the first or last transaction (as appropriate). * * @param _db The inclusive begin date of the date range * @param _de The inclusive end date of the date range */ void validDateRange(QDate& _db, QDate& _de); /** * This method turns on the account group filter and adds the * @p type to the list of allowed groups. * * Note that account group filtering is handled differently * than all the filters of the underlying class. This filter * is meant to be applied to individual splits of matched * transactions AFTER the underlying filter is used to find * the matching transactions. * * @param type the account group to add to the allowed groups list */ void addAccountGroup(MyMoneyAccount::accountTypeE type); /** * This method returns whether an account group filter has been set, * and if so, it returns all the account groups set in the filter. * * @param list list to append account groups into * @return return true if an account group filter has been set */ bool accountGroups(QList& list) const; /** * This method returns whether the specified account group * is allowed by the account groups filter. * * @param type group to append account groups into * @return return true if an account group filter has been set */ bool includesAccountGroup(MyMoneyAccount::accountTypeE type) const; /** * This method is used to test whether a specific account * passes the accountGroup test and either the Account or * Category test, depending on which sort of Account it is. * * The m_tax and m_investments properties are also considered. * * @param acc the account in question * @return true if account is in filter set, false otherwise */ bool includes(const MyMoneyAccount& acc) const; /** * This method writes this report to the DOM element @p e, * within the DOM document @p doc. * * @param e The element which should be populated with info from this report * @param doc The document which we can use to create new sub-elements * if needed * @param anonymous Whether the sensitive parts of the report should be * masked */ void write(QDomElement& e, QDomDocument *doc, bool anonymous = false) const; /** * This method reads a report from the DOM element @p e, and * populates this report with the results. * * @param e The element from which the report should be read * * @return bool True if a report was successfully loaded from the * element @p e. If false is returned, the contents of this report * object are undefined. */ bool read(const QDomElement& e); /** * This method creates a QDomElement for the @p document * under the parent node @p parent. (This version overwrites the * MMObject base class.) * * @param document reference to QDomDocument * @param parent reference to QDomElement parent node */ virtual void writeXML(QDomDocument& document, QDomElement& parent) 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; /** * This method allows to modify the default lineWidth for graphs. * The default is 2. */ static void setLineWidth(int width); private: /** * The user-assigned name of the report */ QString m_name; /** * The user-assigned comment for the report, in case they want to make * additional notes for themselves about the report. */ QString m_comment; /** * Where to group this report amongst the others in the UI view. This * should be assigned by the UI system. */ QString m_group; /** * How much detail to show in the accounts */ enum EDetailLevel m_detailLevel; /** * Whether to show transactions or just totals. */ bool m_hideTransactions; /** * Whether to convert all currencies to the base currency of the file (true). * If this is false, it's up to the report generator to decide how to handle * the currency. */ bool m_convertCurrency; /** * Whether this is one of the users' favorite reports */ bool m_favorite; /** * Whether this report should only include categories marked as "Tax"="Yes" */ bool m_tax; /** * Whether this report should only include investment accounts */ bool m_investments; /** * Whether this report should only include loan accounts * Applies only to querytable reports. Mutually exclusive with * m_investments. */ bool m_loans; /** * What sort of algorithm should be used to run the report */ enum EReportType m_reportType; /** * What sort of values should show up on the ROWS of this report */ enum ERowType m_rowType; /** * What sort of values should show up on the COLUMNS of this report, * in the case of a 'PivotTable' report. Really this is used more as a * QUANTITY of months or days. Whether it's months or days is determined * by m_columnsAreDays. */ enum EColumnType m_columnType; /** * Whether the base unit of columns of this report is days. Only applies to * 'PivotTable' reports. If false, then columns are months or multiples thereof. */ bool m_columnsAreDays; /** * What sort of values should show up on the COLUMNS of this report, * in the case of a 'QueryTable' report */ enum EQueryColumns m_queryColumns; /** * The plain-language description of what the date range should be locked * to. 'userDefined' means NO locking, in any other case, the report * will be adjusted to match the date lock. So if the date lock is * 'currentMonth', the start and end dates of the underlying filter will * be updated to whatever the current month is. This updating happens * automatically when the report is loaded, and should also be done * manually by calling updateDateFilter() before generating the report */ dateOptionE m_dateLock; /** * Which account groups should be included in the report. This filter * is applied to the individual splits AFTER a transaction has been * matched using the underlying filter. */ QList m_accountGroups; /** * Whether an account group filter has been set (see m_accountGroups) */ bool m_accountGroupFilter; /** * What format should be used to draw this report as a chart */ enum EChartType m_chartType; /** * Whether the value of individual data points should be drawn on the chart */ bool m_chartDataLabels; /** * Whether grid lines should be drawn on the chart */ bool m_chartCHGridLines; bool m_chartSVGridLines; /** * Whether this report should be shown as a chart by default (otherwise it * should be shown as a textual report) */ bool m_chartByDefault; /** * Width of the chart lines */ uint m_chartLineWidth; /** * Whether Y axis is logarithmic or linear */ bool m_logYaxis; /** * Y data range */ QString m_dataRangeStart; QString m_dataRangeEnd; /** * Y data range division */ QString m_dataMajorTick; QString m_dataMinorTick; /** * Y labels precision */ uint m_yLabelsPrecision; /** * Whether data range should be calculated automatically or is user defined */ dataOptionE m_dataLock; /** * Whether to include scheduled transactions */ bool m_includeSchedules; /** * Whether to include transfers. Only applies to Income/Expense reports */ bool m_includeTransfers; /** * The id of the budget associated with this report. */ QString m_budgetId; /** * Whether this report should print the actual data to go along with * the budget. This is only valid if the report has a budget. */ bool m_includeBudgetActuals; /** * Whether this report should include all accounts and not only * accounts with transactions. */ bool m_includeUnusedAccounts; /** * Whether this report should include columns for row totals */ bool m_showRowTotals; /** * Whether this report should include forecast balance */ bool m_includeForecast; /** * Whether this report should include moving average */ bool m_includeMovingAverage; /** * The amount of days that spans each moving average */ int m_movingAverageDays; /** * Whether this report should include prices */ bool m_includePrice; /** * Whether this report should include moving average prices */ bool m_includeAveragePrice; /** * Make the actual and forecast lines display as one */ bool m_mixedTime; /** * This stores the column for the current date * This value is calculated dinamically and thus it is not saved in the file */ int m_currentDateColumn; - + /** + * Time in days between the settlement date and the transaction date. + */ + uint m_settlementPeriod; + /** + * Controls showing short-term and long-term capital gains. + */ + bool m_showSTLTCapitalGains; + /** + * Date separating shot-term from long-term gains. + */ + QDate m_tseparator; /** * This member keeps the current setting for line graphs lineWidth. * @sa setLineWidth() */ static int m_lineWidth; /** * This option is for investments reports only which * show prices instead of balances as all other reports do. *

* Select this option to include prices for the given period (week, month, * quarter, ...) only. *

*

* If this option is off the last existing price is shown for a period, if * it is on, in a table the value is '0' shown and in a chart a linear * interpolation for the missing values will be performed. *
Example: *
There are prices for January and March, but there is no price for * February. *

    *
  • OFF: shows the price for February as the last price of * January *
  • ON: in a table the value is '0', in a chart a linear * interpolation for the February-price will be performed * (so it makes a kind of average-value using the January- and the * March-price in the chart) *
*

*/ bool m_skipZero; }; /** * Make it possible to hold @ref MyMoneyReport objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyReport) #endif // MYMONEYREPORT_H diff --git a/kmymoney/reports/listtable.cpp b/kmymoney/reports/listtable.cpp index a6acc5d5b..90d87b6c1 100644 --- a/kmymoney/reports/listtable.cpp +++ b/kmymoney/reports/listtable.cpp @@ -1,613 +1,619 @@ /*************************************************************************** listtable.cpp ------------------- begin : Sat 28 jun 2008 copyright : (C) 2004-2005 by Ace Jones 2008 by Alvaro Soliverez (C) 2017 Ł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 "listtable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // This is just needed for i18n(). Once I figure out how to handle i18n // without using this macro directly, I'll be freed of KDE dependency. // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kmymoneyglobalsettings.h" #include "reportdebug.h" namespace reports { QStringList ListTable::TableRow::m_sortCriteria; // **************************************************************************** // // ListTable implementation // // **************************************************************************** bool ListTable::TableRow::operator< (const TableRow& _compare) const { bool result = false; QStringList::const_iterator it_criterion = m_sortCriteria.constBegin(); while (it_criterion != m_sortCriteria.constEnd()) { if (this->operator[](*it_criterion) < _compare[ *it_criterion ]) { result = true; break; } else if (this->operator[](*it_criterion) > _compare[ *it_criterion ]) break; ++it_criterion; } return result; } // needed for KDE < 3.2 implementation of qHeapSort bool ListTable::TableRow::operator<= (const TableRow& _compare) const { return (!(_compare < *this)); } bool ListTable::TableRow::operator== (const TableRow& _compare) const { return (!(*this < _compare) && !(_compare < *this)); } bool ListTable::TableRow::operator> (const TableRow& _compare) const { return (_compare < *this); } /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ ListTable::ListTable(const MyMoneyReport& _report): ReportTable(), m_config(_report) { } void ListTable::render(QString& result, QString& csv) const { MyMoneyFile* file = MyMoneyFile::instance(); result = ""; csv = ""; result += QString("

%1

\n").arg(m_config.name()); csv += "\"Report: " + m_config.name() + "\"\n"; //actual dates of the report result += QString("
"); if (!m_config.fromDate().isNull()) { result += i18nc("Report date range", "%1 through %2", QLocale().toString(m_config.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config.toDate(), QLocale::ShortFormat)); result += QString("
\n"); result += QString("
 
\n"); csv += i18nc("Report date range", "%1 through %2", QLocale().toString(m_config.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config.toDate(), QLocale::ShortFormat)); csv += QString("\n"); } result += QString("
"); if (m_config.isConvertCurrency()) { result += i18n("All currencies converted to %1" , file->baseCurrency().name()); csv += i18n("All currencies converted to %1\n" , file->baseCurrency().name()); } else { result += i18n("All values shown in %1 unless otherwise noted" , file->baseCurrency().name()); csv += i18n("All values shown in %1 unless otherwise noted\n" , file->baseCurrency().name()); } result += QString("
\n"); result += QString("
 
\n"); // retrieve the configuration parameters from the report definition. // the things that we care about for query reports are: // how to group the rows, what columns to display, and what field // to subtotal on QStringList groups = m_group.split(','); QStringList columns = m_columns.split(','); if (!m_subtotal.isEmpty() && m_subtotal.split(',').count() == 1) // constructPerformanceRow has subtotal columns already in columns columns += m_subtotal; QStringList postcolumns = m_postcolumns.split(','); if (!m_postcolumns.isEmpty()) // prevent creation of empty column columns += postcolumns; // // Table header // QMap i18nHeaders; i18nHeaders["postdate"] = i18n("Date"); i18nHeaders["value"] = i18n("Amount"); i18nHeaders["number"] = i18n("Num"); i18nHeaders["payee"] = i18n("Payee"); i18nHeaders["tag"] = i18n("Tags"); i18nHeaders["category"] = i18n("Category"); i18nHeaders["account"] = i18n("Account"); i18nHeaders["memo"] = i18n("Memo"); i18nHeaders["topcategory"] = i18n("Top Category"); i18nHeaders["categorytype"] = i18n("Category Type"); i18nHeaders["month"] = i18n("Month"); i18nHeaders["week"] = i18n("Week"); i18nHeaders["reconcileflag"] = i18n("Reconciled"); i18nHeaders["action"] = i18n("Action"); i18nHeaders["shares"] = i18n("Shares"); i18nHeaders["price"] = i18n("Price"); i18nHeaders["latestprice"] = i18n("Price"); i18nHeaders["netinvvalue"] = i18n("Net Value"); i18nHeaders["buys"] = i18n("Buys"); i18nHeaders["sells"] = i18n("Sells"); + i18nHeaders["buysST"] = i18n("Short-term Buys"); + i18nHeaders["sellsST"] = i18n("Short-term Sells"); + i18nHeaders["buysLT"] = i18n("Long-term Buys"); + i18nHeaders["sellsLT"] = i18n("Long-term Sells"); i18nHeaders["reinvestincome"] = i18n("Dividends Reinvested"); i18nHeaders["cashincome"] = i18n("Dividends Paid Out"); i18nHeaders["startingbal"] = i18n("Starting Balance"); i18nHeaders["endingbal"] = i18n("Ending Balance"); i18nHeaders["return"] = i18n("Annualized Return"); i18nHeaders["returninvestment"] = i18n("Return On Investment"); i18nHeaders["fees"] = i18n("Fees"); i18nHeaders["interest"] = i18n("Interest"); i18nHeaders["payment"] = i18n("Payment"); i18nHeaders["balance"] = i18n("Balance"); i18nHeaders["type"] = i18n("Type"); i18nHeaders["name"] = i18nc("Account name", "Name"); i18nHeaders["nextduedate"] = i18n("Next Due Date"); i18nHeaders["occurence"] = i18n("Occurrence"); // krazy:exclude=spelling i18nHeaders["paymenttype"] = i18n("Payment Method"); i18nHeaders["institution"] = i18n("Institution"); i18nHeaders["description"] = i18n("Description"); i18nHeaders["openingdate"] = i18n("Opening Date"); i18nHeaders["currencyname"] = i18n("Currency"); i18nHeaders["balancewarning"] = i18n("Balance Early Warning"); i18nHeaders["maxbalancelimit"] = i18n("Balance Max Limit"); i18nHeaders["creditwarning"] = i18n("Credit Early Warning"); i18nHeaders["maxcreditlimit"] = i18n("Credit Max Limit"); i18nHeaders["tax"] = i18n("Tax"); i18nHeaders["favorite"] = i18n("Preferred"); i18nHeaders["loanamount"] = i18n("Loan Amount"); i18nHeaders["interestrate"] = i18n("Interest Rate"); i18nHeaders["nextinterestchange"] = i18n("Next Interest Change"); i18nHeaders["periodicpayment"] = i18n("Periodic Payment"); i18nHeaders["finalpayment"] = i18n("Final Payment"); i18nHeaders["currentbalance"] = i18n("Current Balance"); i18nHeaders["capitalgain"] = i18n("Capital Gain"); + i18nHeaders["capitalgainST"] = i18n("Short-term Gain"); + i18nHeaders["capitalgainLT"] = i18n("Long-term Gain"); // the list of columns which represent money, so we can display them correctly - QStringList moneyColumns = QString("value,shares,price,latestprice,netinvvalue,buys,sells,cashincome,reinvestincome,startingbal,fees,interest,payment,balance,balancewarning,maxbalancelimit,creditwarning,maxcreditlimit,loanamount,periodicpayment,finalpayment,currentbalance,startingbal,endingbal,capitalgain").split(','); + QStringList moneyColumns = QString("value,shares,price,latestprice,netinvvalue,buys,buysST,buysLT,sells,sellsST,sellsLT,cashincome,reinvestincome,startingbal,fees,interest,payment,balance,balancewarning,maxbalancelimit,creditwarning,maxcreditlimit,loanamount,periodicpayment,finalpayment,currentbalance,startingbal,endingbal,capitalgain,capitalgainST,capitalgainLT").split(','); // the list of columns which represent shares, which is like money except the // transaction currency will not be displayed QStringList sharesColumns = QString("shares").split(','); // the list of columns which represent a percentage, so we can display them correctly QStringList percentColumns = QString("return,returninvestment,interestrate").split(','); // the list of columns which represent dates, so we can display them correctly QStringList dateColumns = QString("postdate,entrydate,nextduedate,openingdate,nextinterestchange").split(','); result += "\n"; QStringList::const_iterator it_column = columns.constBegin(); while (it_column != columns.constEnd()) { QString i18nName = i18nHeaders[*it_column]; if (i18nName.isEmpty()) i18nName = *it_column; result += ""; csv += i18nName + ','; ++it_column; } result += "\n"; csv = csv.left(csv.length() - 1); csv += '\n'; // initialize group names to empty, so any group will have to display its header QStringList prevGrpNames; for (int i = 0; i < groups.count(); ++i) { prevGrpNames.append(QString()); } // // Rows // bool row_odd = true; bool isLowestGroupTotal = true; // hack to inform whether to put separator line or not // ***DV*** MyMoneyMoney startingBalance; MyMoneyMoney balanceChange = MyMoneyMoney(); for (QList::const_iterator it_row = m_rows.begin(); it_row != m_rows.end(); ++it_row) { /** rank can be: * 0 - opening balance * 1 - major split of transaction * 2 - minor split of transaction * 3 - closing balance * 4 - first totals row * 5 - middle totals row */ const QString rowRank = (*it_row)["rank"]; // detect whether any of groups changed and display new group header in that case for (int i = 0; i < groups.count(); ++i) { QString curGrpName = (*it_row).value(groups.at(i)); if (curGrpName.isEmpty()) // it could be grand total continue; if (prevGrpNames.at(i) != curGrpName) { // group header of lowest group doesn't bring any useful information // if hide transaction is enabled, so don't display it int lowestGroup = groups.count() - 1; if (!m_config.isHideTransactions() || i != lowestGroup) { row_odd = true; result += "" "\n"; csv += "\"" + curGrpName + "\"\n"; } if (i == lowestGroup) // lowest group has been switched... isLowestGroupTotal = true; // ...so expect lowest group total prevGrpNames.replace(i, curGrpName); } } bool need_label = true; QString tlink; // link information to account and transaction if (!m_config.isHideTransactions() || rowRank == "4" || rowRank == "5") { // if hide transaction is enabled display only total rows i.e. rank = 4 || rank = 5 if (rowRank == "0" || rowRank == "3") { // skip the opening and closing balance row, // if the balance column is not shown // rank = 0 for opening balance, rank = 3 for closing balance if (!columns.contains("balance")) continue; result += QString("").arg((* it_row)["id"]); // ***DV*** } else if (rowRank == "1") { row_odd = ! row_odd; tlink = QString("id=%1&tid=%2") .arg((* it_row)["accountid"], (* it_row)["id"]); result += QString("").arg(row_odd ? "row-odd " : "row-even"); } else if (rowRank == "2") result += QString("").arg(row_odd ? "item1" : "item0"); else if (rowRank == "4" || rowRank == "5") { QList::const_iterator nextRow = std::next(it_row); if ((m_config.rowType() == MyMoneyReport::eTag)) //If we order by Tags don't show the Grand total as we can have multiple tags per transaction continue; else if (rowRank == "4") { if (nextRow != m_rows.end()) { if (isLowestGroupTotal && m_config.isHideTransactions()) { result += QString(""); isLowestGroupTotal = false; } else if ((*nextRow)["rank"] == "5") result += QString(""); else result += QString(""); } else result += QString(""); } else if (rowRank == "5") { if (nextRow != m_rows.end()) { if ((*nextRow)["rank"] == "5") result += QString(""); else result += QString(""); } } else result += QString(""); } else result += QString("").arg(row_odd ? "row-odd " : "row-even"); } else continue; // // Columns // QStringList::const_iterator it_column = columns.constBegin(); while (it_column != columns.constEnd()) { QString data = (*it_row)[*it_column]; // ***DV*** if (rowRank == "2") { if (* it_column == "value") data = (* it_row)["split"]; else if (*it_column == "postdate" || *it_column == "number" || *it_column == "payee" || *it_column == "tag" || *it_column == "action" || *it_column == "shares" || *it_column == "price" || *it_column == "nextduedate" || *it_column == "balance" || *it_column == "account" || *it_column == "name") data = ""; } // ***DV*** else if (rowRank == "0" || rowRank == "3") { if (*it_column == "balance") { data = (* it_row)["balance"]; if ((* it_row)["id"] == "A") { // opening balance? startingBalance = MyMoneyMoney(data); balanceChange = MyMoneyMoney(); } } if (need_label) { if ((*it_column == "payee") || (*it_column == "category") || (*it_column == "memo")) { if (!(*it_row)["shares"].isEmpty()) { data = ((* it_row)["id"] == "A") ? i18n("Initial Market Value") : i18n("Ending Market Value"); } else { data = ((* it_row)["id"] == "A") ? i18n("Opening Balance") : i18n("Closing Balance"); } need_label = false; } } } // The 'balance' column is calculated at render-time // but not printed on split lines else if (*it_column == "balance" && rowRank == "1") { // Take the balance off the deepest group iterator balanceChange += MyMoneyMoney((*it_row).value("value", "0")); data = (balanceChange + startingBalance).toString(); } else if ((rowRank == "4" || rowRank == "5")) { // display total title but only if first column doesn't contain any data if (it_column == columns.constBegin() && data.isEmpty()) { result += "")); ++it_column; continue; } else if (!m_subtotal.contains(*it_column)) { // don't display e.g. account in totals row result.append(QLatin1Literal("")); ++it_column; continue; } } // Figure out how to render the value in this column, depending on // what its properties are. // // TODO: This and the i18n headings are handled // as a set of parallel vectors. Would be much better to make a single // vector of a properties class. QString tlinkBegin, tlinkEnd; if (!tlink.isEmpty()) { tlinkBegin = QString("").arg(tlink); tlinkEnd = QLatin1String(""); } QString currencyID = (*it_row)["currency"]; if (currencyID.isEmpty()) currencyID = file->baseCurrency().id(); int fraction = file->currency(currencyID).smallestAccountFraction(); if (m_config.isConvertCurrency()) // don't show currency id, if there is only single currency currencyID.clear(); if (sharesColumns.contains(*it_column)) { if (data.isEmpty()) { result += QString(""); csv += "\"\","; } else { int sharesPrecision = MyMoneyMoney::denomToPrec(file->security(file->account((*it_row)["accountid"]).currencyId()).smallestAccountFraction()); result += QString("").arg(MyMoneyMoney(data).formatMoney("", sharesPrecision), tlinkBegin, tlinkEnd); csv += "\"" + MyMoneyMoney(data).formatMoney("", sharesPrecision, false) + "\","; } } else if (moneyColumns.contains(*it_column)) { if (data.isEmpty()) { result += QString("") .arg((*it_column == "value") ? " class=\"value\"" : ""); csv += "\"\","; } else if (MyMoneyMoney(data) == MyMoneyMoney::autoCalc) { result += QString("%3%2%4") .arg((*it_column == "value") ? " class=\"value\"" : "") .arg(i18n("Calculated"), tlinkBegin, tlinkEnd); csv += "\"" + i18n("Calculated") + "\","; } else if (*it_column == "price") { int pricePrecision = file->security(file->account((*it_row)["accountid"]).currencyId()).pricePrecision(); result += QString("") .arg(MyMoneyMoney(data).formatMoney(QString(), pricePrecision), currencyID, tlinkBegin, tlinkEnd); csv += "\"" + currencyID + " " + MyMoneyMoney(data).formatMoney(QString(), pricePrecision, false) + "\","; } else { result += QString("%4%2 %3%5") .arg((*it_column == "value") ? " class=\"value\"" : "") .arg(currencyID) .arg(MyMoneyMoney(data).formatMoney(fraction)) .arg(tlinkBegin, tlinkEnd); csv += "\"" + currencyID + " " + MyMoneyMoney(data).formatMoney(fraction, false) + "\","; } } else if (percentColumns.contains(*it_column)) { if (data.isEmpty()) { result += QString(""); csv += "\"\","; } else { data = (MyMoneyMoney(data) * MyMoneyMoney(100, 1)).formatMoney(fraction); result += QString("").arg(data, tlinkBegin, tlinkEnd); csv += data + "%,"; } } else if (dateColumns.contains(*it_column)) { // do this before we possibly change data csv += "\"" + data + "\","; // if we have a locale() then use its date formatter if (!data.isEmpty()) { QDate qd = QDate::fromString(data, Qt::ISODate); data = QLocale().toString(qd, QLocale::ShortFormat); } result += QString("").arg(data, tlinkBegin, tlinkEnd); } else { result += QString("").arg(data, tlinkBegin, tlinkEnd); csv += "\"" + data + "\","; } ++it_column; tlink.clear(); } result += "\n"; csv = csv.left(csv.length() - 1); // remove final comma csv += '\n'; } result += "
" + i18nName + "
" + curGrpName + "
"; if (rowRank == "4") { if (!(*it_row)["depth"].isEmpty()) result += i18nc("Total balance", "Total") + ' ' + prevGrpNames.at((*it_row)["depth"].toInt()); else result += i18n("Grand Total"); } result.append(QLatin1Literal("%2%1%3%3%2 %1%4%2%1%%3%2%1%3%2%1%3
\n"; } QString ListTable::renderBody() const { QString html, csv; render(html, csv); return html; } QString ListTable::renderCSV() const { QString html, csv; render(html, csv); return csv; } void ListTable::dump(const QString& file, const QString& context) const { QFile g(file); g.open(QIODevice::WriteOnly | QIODevice::Text); if (! context.isEmpty()) QTextStream(&g) << context.arg(renderBody()); else QTextStream(&g) << renderBody(); g.close(); } void ListTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected. // In case we get called for a non investment only report we quit if (KMyMoneyGlobalSettings::expertMode() || !m_config.isInvestmentsOnly()) { return; } // get all investment subAccountsList but do not include those with zero balance // or those which had no transactions during the timeframe of the report QStringList accountIdList; QStringList subAccountsList; MyMoneyFile* file = MyMoneyFile::instance(); // get the report account filter if (!m_config.accounts(accountIdList) && m_config.isInvestmentsOnly()) { // this will only execute if this is an investment-only report QList accountList; file->accountList(accountList); QList::const_iterator it_ma; for (it_ma = accountList.constBegin(); it_ma != accountList.constEnd(); ++it_ma) { if ((*it_ma).accountType() == MyMoneyAccount::Investment) { accountIdList.append((*it_ma).id()); } } } QStringList::const_iterator it_a; for (it_a = accountIdList.constBegin(); it_a != accountIdList.constEnd(); ++it_a) { MyMoneyAccount acc = file->account(*it_a); if (acc.accountType() == MyMoneyAccount::Investment) { QStringList::const_iterator it_b; for (it_b = acc.accountList().constBegin(); it_b != acc.accountList().constEnd(); ++it_b) { if (!accountIdList.contains(*it_b)) { subAccountsList.append(*it_b); } } } } if (m_config.isInvestmentsOnly() && !m_config.isIncludingUnusedAccounts()) { // if the balance is not zero at the end, include the subaccount QStringList::iterator it_balance; for (it_balance = subAccountsList.begin(); it_balance != subAccountsList.end();) { if (!file->balance((*it_balance), m_config.toDate()).isZero()) { m_config.addAccount((*it_balance)); it_balance = subAccountsList.erase((it_balance)); } else { ++it_balance; } } // if there are transactions for that subaccount, include them MyMoneyTransactionFilter filter; filter.setDateFilter(m_config.fromDate(), m_config.toDate()); filter.addAccount(subAccountsList); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); //Check each split for a matching account for (; it_t != transactions.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); QList::const_iterator it_s = splits.begin(); for (; it_s != splits.end(); ++it_s) { const QString& accountId = (*it_s).accountId(); if (!(*it_s).shares().isZero() && subAccountsList.contains(accountId)) { subAccountsList.removeOne(accountId); m_config.addAccount(accountId); } } } } else { // if not an investment-only report or explicitly including unused accounts // add all investment subaccounts m_config.addAccount(subAccountsList); } } } diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/reports/querytable.cpp index dfa5aa4b5..7608bb494 100644 --- a/kmymoney/reports/querytable.cpp +++ b/kmymoney/reports/querytable.cpp @@ -1,1915 +1,2013 @@ /*************************************************************************** querytable.cpp ------------------- begin : Fri Jul 23 2004 copyright : (C) 2004-2005 by Ace Jones (C) 2007 Sascha Pfau (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /**************************************************************************** Contains code from the func_xirr and related methods of financial.cpp - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under GPLv2 or later. *****************************************************************************/ /*************************************************************************** * * * 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 "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "reportdebug.h" #include "kmymoneyglobalsettings.h" namespace reports { // **************************************************************************** // // CashFlowListItem implementation // // Cash flow analysis tools for investment reports // // **************************************************************************** QDate CashFlowListItem::m_sToday = QDate::currentDate(); MyMoneyMoney CashFlowListItem::NPV(double _rate) const { double T = static_cast(m_sToday.daysTo(m_date)) / 365.0; MyMoneyMoney result(m_value.toDouble() / pow(1 + _rate, T), 100); //qDebug() << "CashFlowListItem::NPV( " << _rate << " ) == " << result; return result; } // **************************************************************************** // // CashFlowList implementation // // Cash flow analysis tools for investment reports // // **************************************************************************** CashFlowListItem CashFlowList::mostRecent() const { CashFlowList dupe(*this); qSort(dupe); //qDebug() << " CashFlowList::mostRecent() == " << dupe.back().date().toString(Qt::ISODate); return dupe.back(); } MyMoneyMoney CashFlowList::NPV(double _rate) const { MyMoneyMoney result; const_iterator it_cash = constBegin(); while (it_cash != constEnd()) { result += (*it_cash).NPV(_rate); ++it_cash; } //qDebug() << "CashFlowList::NPV( " << _rate << " ) == " << result << "------------------------" << endl; return result; } double CashFlowList::calculateXIRR() const { double resultRate = 0.00001; double resultZero = 0.00000; //if ( args.count() > 2 ) // resultRate = calc->conv()->asFloat ( args[2] ).asFloat(); // check pairs and count >= 2 and guess > -1.0 //if ( args[0].count() != args[1].count() || args[1].count() < 2 || resultRate <= -1.0 ) // return Value::errorVALUE(); // define max epsilon static const double maxEpsilon = 1e-5; // max number of iterations static const int maxIter = 50; // Newton's method - try to find a res, with a accuracy of maxEpsilon double rateEpsilon, newRate, resultValue; int i = 0; bool contLoop; do { resultValue = xirrResult(resultRate); double resultDerive = xirrResultDerive(resultRate); //check what happens if xirrResultDerive is zero //Don't know if it is correct to dismiss the result if (resultDerive != 0) { newRate = resultRate - resultValue / resultDerive; } else { newRate = resultRate - resultValue; } rateEpsilon = fabs(newRate - resultRate); resultRate = newRate; contLoop = (rateEpsilon > maxEpsilon) && (fabs(resultValue) > maxEpsilon); } while (contLoop && (++i < maxIter)); if (contLoop) return resultZero; return resultRate; } double CashFlowList::xirrResult(double& rate) const { QDate date; double r = rate + 1.0; double res = 0.00000;//back().value().toDouble(); QList::const_iterator list_it = constBegin(); while (list_it != constEnd()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); if (e_i < 0) { res += val.toDouble() * pow(r, -e_i); } else { res += val.toDouble() / pow(r, e_i); } ++list_it; } return res; } double CashFlowList::xirrResultDerive(double& rate) const { QDate date; double r = rate + 1.0; double res = 0.00000; QList::const_iterator list_it = constBegin(); while (list_it != constEnd()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); res -= e_i * val.toDouble() / pow(r, e_i + 1.0); ++list_it; } return res; } double CashFlowList::IRR() const { double result = 0.0; // set 'today', which is the most recent of all dates in the list CashFlowListItem::setToday(mostRecent().date()); result = calculateXIRR(); return result; } MyMoneyMoney CashFlowList::total() const { MyMoneyMoney result; const_iterator it_cash = constBegin(); while (it_cash != constEnd()) { result += (*it_cash).value(); ++it_cash; } return result; } void CashFlowList::dumpDebug() const { const_iterator it_item = constBegin(); while (it_item != constEnd()) { qDebug() << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); ++it_item; } } // **************************************************************************** // // QueryTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void QueryTable::init() { switch (m_config.rowType()) { case MyMoneyReport::eAccountByTopAccount: case MyMoneyReport::eEquityType: case MyMoneyReport::eAccountType: case MyMoneyReport::eInstitution: constructAccountTable(); m_columns = "account"; break; case MyMoneyReport::eAccount: constructTransactionTable(); m_columns = "accountid,postdate"; break; case MyMoneyReport::ePayee: case MyMoneyReport::eTag: case MyMoneyReport::eMonth: case MyMoneyReport::eWeek: constructTransactionTable(); m_columns = "postdate,account"; break; case MyMoneyReport::eCashFlow: constructSplitsTable(); m_columns = "postdate"; break; default: constructTransactionTable(); m_columns = "postdate"; } // Sort the data to match the report definition m_subtotal = "value"; switch (m_config.rowType()) { case MyMoneyReport::eCashFlow: m_group = "categorytype,topcategory,category"; break; case MyMoneyReport::eCategory: m_group = "categorytype,topcategory,category"; break; case MyMoneyReport::eTopCategory: m_group = "categorytype,topcategory"; break; case MyMoneyReport::eTopAccount: m_group = "topaccount,account"; break; case MyMoneyReport::eAccount: m_group = "account"; break; case MyMoneyReport::eAccountReconcile: m_group = "account,reconcileflag"; break; case MyMoneyReport::ePayee: m_group = "payee"; break; case MyMoneyReport::eTag: m_group = "tag"; break; case MyMoneyReport::eMonth: m_group = "month"; break; case MyMoneyReport::eWeek: m_group = "week"; break; case MyMoneyReport::eAccountByTopAccount: m_group = "topaccount"; break; case MyMoneyReport::eEquityType: m_group = "equitytype"; break; case MyMoneyReport::eAccountType: m_group = "type"; break; case MyMoneyReport::eInstitution: m_group = "institution,topaccount"; break; default: throw MYMONEYEXCEPTION("QueryTable::QueryTable(): unhandled row type"); } QString sort = m_group + ",id,rank," + m_columns; switch (m_config.rowType()) { case MyMoneyReport::eAccountByTopAccount: case MyMoneyReport::eEquityType: case MyMoneyReport::eAccountType: case MyMoneyReport::eInstitution: m_columns = "account"; break; default: m_columns = "postdate"; } unsigned qc = m_config.queryColumns(); if (qc & MyMoneyReport::eQCnumber) m_columns += ",number"; if (qc & MyMoneyReport::eQCpayee) m_columns += ",payee"; if (qc & MyMoneyReport::eQCtag) m_columns += ",tag"; if (qc & MyMoneyReport::eQCcategory) m_columns += ",category"; if (qc & MyMoneyReport::eQCaccount) m_columns += ",account"; if (qc & MyMoneyReport::eQCreconciled) m_columns += ",reconcileflag"; if (qc & MyMoneyReport::eQCmemo) m_columns += ",memo"; if (qc & MyMoneyReport::eQCaction) m_columns += ",action"; if (qc & MyMoneyReport::eQCshares) m_columns += ",shares"; if (qc & MyMoneyReport::eQCprice) m_columns += ",price"; if (qc & MyMoneyReport::eQCperformance) { m_columns += ",startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment,endingbal"; m_subtotal = "startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment,endingbal"; } if (qc & MyMoneyReport::eQCcapitalgain) { m_columns += ",buys,sells,capitalgain"; m_subtotal = "buys,sells,capitalgain"; + if (m_config.isShowingSTLTCapitalGains()) { + m_columns += ",buysST,sellsST,capitalgainST,buysLT,sellsLT,capitalgainLT"; + m_subtotal += ",buysST,sellsST,capitalgainST,buysLT,sellsLT,capitalgainLT"; + } } if (qc & MyMoneyReport::eQCloan) { m_columns += ",payment,interest,fees"; m_postcolumns = "balance"; } if (qc & MyMoneyReport::eQCbalance) m_postcolumns = "balance"; TableRow::setSortCriteria(sort); qSort(m_rows); constructTotalRows(); // adds total rows to m_rows } void QueryTable::constructTotalRows() { if (m_rows.isEmpty()) return; // qSort places grand total at last position, because it doesn't belong to any group for (int i = 0; i < m_rows.count(); ++i) { if (m_rows.at(0)["rank"] == "4" || m_rows.at(0)["rank"] == "5") // it should be unlikely that total row is at the top of rows, so... m_rows.move(0, m_rows.count() - 1 - i); // ...move it at the bottom else break; } MyMoneyFile* file = MyMoneyFile::instance(); QStringList subtotals = m_subtotal.split(','); QStringList groups = m_group.split(','); QStringList columns = m_columns.split(','); if (!m_subtotal.isEmpty() && subtotals.count() == 1) columns.append(m_subtotal); QStringList postcolumns = m_postcolumns.split(','); if (!m_postcolumns.isEmpty()) columns.append(postcolumns); QMap>> totalCurrency; QList> totalGroups; QMap totalsValues; // initialize all total values under summed columns to be zero foreach (auto subtotal, subtotals) { totalsValues.insert(subtotal, MyMoneyMoney()); } totalsValues.insert("rows_count", MyMoneyMoney()); // create total groups containing totals row for each group totalGroups.append(totalsValues); // prepend with extra group for grand total for (int j = 0; j < groups.count(); ++j) { totalGroups.append(totalsValues); } QList stashedTotalRows; int iCurrentRow, iNextRow; for (iCurrentRow = 0; iCurrentRow < m_rows.count();) { iNextRow = iCurrentRow + 1; // total rows are useless at summing so remove whole block of them at once while (iNextRow != m_rows.count() && (m_rows.at(iNextRow)["rank"] == "4" || m_rows.at(iNextRow)["rank"] == "5")) { stashedTotalRows.append(m_rows.takeAt(iNextRow)); // ...but stash them just in case } bool lastRow = (iNextRow == m_rows.count()); // sum all subtotal values for lowest group QString currencyID = m_rows.at(iCurrentRow).value("currency"); if (m_rows.at(iCurrentRow)["rank"] == "1") { // don't sum up on balance (rank = 0 || rank = 3) and minor split (rank = 2) foreach (auto subtotal, subtotals) { if (!totalCurrency.contains(currencyID)) totalCurrency[currencyID].append(totalGroups); totalCurrency[currencyID].last()[subtotal] += MyMoneyMoney(m_rows.at(iCurrentRow)[subtotal]); } totalCurrency[currencyID].last()["rows_count"] += MyMoneyMoney::ONE; } // iterate over groups from the lowest to the highest to find group change for (int i = groups.count() - 1; i >= 0 ; --i) { // if any of groups from next row changes (or next row is the last row), then it's time to put totals row if (lastRow || m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(i)]) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { if (!MyMoneyMoney((*currencyGrp).at(i + 1).value("rows_count")).isZero()) { // if no rows summed up, then no totals row TableRow totalsRow; // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values QMap::iterator upperGrp = (*currencyGrp)[i].begin(); QMap::iterator lowerGrp = (*currencyGrp)[i + 1].begin(); while(upperGrp != (*currencyGrp)[i].end()) { totalsRow[lowerGrp.key()] = lowerGrp.value().toString(); // fill totals row with subtotal values... (*upperGrp) += (*lowerGrp); // (*lowerGrp) = MyMoneyMoney(); ++upperGrp; ++lowerGrp; } // custom total values calculations foreach (auto subtotal, subtotals) { if (subtotal == "returninvestment") totalsRow[subtotal] = helperROI((*currencyGrp).at(i + 1).value("buys"), (*currencyGrp).at(i + 1).value("sells"), (*currencyGrp).at(i + 1).value("startingbal"), (*currencyGrp).at(i + 1).value("endingbal"), (*currencyGrp).at(i + 1).value("cashincome")).toString(); else if (subtotal == "price") totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(i + 1).value("price") / (*currencyGrp).at(i + 1).value("rows_count")).toString(); } // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { if (stashedTotalRows.at(j).value("currency") != currencyID) continue; foreach (auto subtotal, subtotals) { if (subtotal == "return") totalsRow["return"] = stashedTotalRows.takeAt(j)["return"]; } break; } } (*currencyGrp).replace(i + 1, totalsValues); for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = m_rows.at(iCurrentRow)[groups.at(j)]; // ...and identification } QString currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow["currency"] = currencyID; if (isMainCurrencyTotal) { totalsRow["rank"] = "4"; isMainCurrencyTotal = false; } else totalsRow["rank"] = "5"; totalsRow["depth"] = QString::number(i); totalsRow.remove("rows_count"); m_rows.insert(iNextRow++, totalsRow); // iCurrentRow and iNextRow can diverge here by more than one } ++currencyGrp; } } } // code to put grand total row if (lastRow) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { TableRow totalsRow; QMap::const_iterator grandTotalGrp = (*currencyGrp)[0].constBegin(); while(grandTotalGrp != (*currencyGrp)[0].constEnd()) { totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString(); ++grandTotalGrp; } foreach (auto subtotal, subtotals) { if (subtotal == "returninvestment") totalsRow[subtotal] = helperROI((*currencyGrp).at(0).value("buys"), (*currencyGrp).at(0).value("sells"), (*currencyGrp).at(0).value("startingbal"), (*currencyGrp).at(0).value("endingbal"), (*currencyGrp).at(0).value("cashincome")).toString(); else if (subtotal == "price") totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(0).value("price") / (*currencyGrp).at(0).value("rows_count")).toString(); } if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { foreach (auto subtotal, subtotals) { if (subtotal == "return") totalsRow["return"] = stashedTotalRows.takeAt(j)["return"]; } } } for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = QString(); // no identification } QString currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow["currency"] = currencyID; if (isMainCurrencyTotal) { totalsRow["rank"] = "4"; isMainCurrencyTotal = false; } else totalsRow["rank"] = "5"; totalsRow["depth"] = ""; m_rows.append(totalsRow); ++currencyGrp; } break; // no use to loop further } iCurrentRow = iNextRow; // iCurrent makes here a leap forward by at least one } } void QueryTable::constructTransactionTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); bool use_transfers; bool use_summary; bool hide_details; bool tag_special_case = false; switch (m_config.rowType()) { case MyMoneyReport::eCategory: case MyMoneyReport::eTopCategory: use_summary = false; use_transfers = false; hide_details = false; break; case MyMoneyReport::ePayee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); break; case MyMoneyReport::eTag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); break; } // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; QList tagIdListCache; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["month"] = qS["month"] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA["week"] = qS["week"] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (report.isConvertCurrency()) qA["currency"] = qS["currency"] = file->baseCurrency().id(); else qA["currency"] = qS["currency"] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc = (* it_split).accountId(); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // skip this transaction if we didn't find a valid base account - see the above description // for the base account's description - if we don't find it avoid a crash by skipping the transaction if (myBegin == splits.end()) continue; // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & MyMoneyReport::eQCloan) { ReportAccount splitAcc = (*it_split).accountId(); loan_special_case = splitAcc.isLoan(); } bool include_me = true; bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only QString a_fullname = ""; QString a_memo = ""; int pass = 1; QString myBeginCurrency; QString baseCurrency = file->baseCurrency().id(); QMap xrMap; // container for conversion rates from given currency to myBeginCurrency do { MyMoneyMoney xr; ReportAccount splitAcc = (* it_split).accountId(); QString splitCurrency; if (splitAcc.isInvest()) splitCurrency = file->account(file->account((*it_split).accountId()).parentAccountId()).currencyId(); else splitCurrency = file->account((*it_split).accountId()).currencyId(); if (it_split == myBegin) myBeginCurrency = splitCurrency; //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); //convert to base currency if (m_config.isConvertCurrency()) { xr = xrMap.value(splitCurrency, xr); // check if there is conversion rate to myBeginCurrency already stored... if (xr == MyMoneyMoney()) // ...if not... xr = (*it_split).price(); // ...take conversion rate to myBeginCurrency from split else if (splitAcc.isInvest()) // if it's stock split... xr *= (*it_split).price(); // ...multiply it by stock price stored in split if (myBeginCurrency != baseCurrency) { // myBeginCurrency can differ from baseCurrency... MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); // ...so check conversion rate... if (price.isValid()) { xr *= price.rate(baseCurrency); // ...and multiply it by current price... qA["currency"] = qS["currency"] = baseCurrency; } else qA["currency"] = qS["currency"] = myBeginCurrency; // ...and set information about non-baseCurrency } } else if (splitAcc.isInvest()) xr = (*it_split).price(); else xr = MyMoneyMoney::ONE; if (it_split == myBegin) { include_me = m_config.includes(splitAcc); if (include_me) // track accts that will need opening and closing balances //FIXME in some cases it will show the opening and closing //balances but no transactions if the splits are all filtered out -- asoliverez accts.insert(splitAcc.id(), splitAcc); qA["account"] = splitAcc.name(); qA["accountid"] = splitAcc.id(); qA["topaccount"] = splitAcc.topParentName(); if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); qA["action"] = (*it_split).action(); qA["shares"] = shares.isZero() ? "" : shares.toString(); qA["price"] = shares.isZero() ? "" : xr.convertPrecision(splitAcc.currency().pricePrecision()).toString(); if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && shares.isNegative()) qA["action"] = "Sell"; qA["investaccount"] = splitAcc.parent().name(); MyMoneySplit stockSplit = (*it_split); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySecurity security; MyMoneySplit::investTransactionTypeE transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), stockSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!(assetAccountSplit == MyMoneySplit())) { for (it_split = splits.begin(); it_split != splits.end(); ++it_split) { if ((*it_split) == assetAccountSplit) { splitAcc = assetAccountSplit.accountId(); // switch over from stock split to asset split because amount in stock split doesn't take fees/interests into account myBegin = it_split; // set myBegin to asset split, so stock split can be listed in details under splits myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); if (m_config.isConvertCurrency()) { if (myBeginCurrency != baseCurrency) { MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); if (price.isValid()) { xr = price.rate(baseCurrency); qA["currency"] = qS["currency"] = baseCurrency; } else qA["currency"] = qS["currency"] = myBeginCurrency; } else xr = MyMoneyMoney::ONE; qA["price"] = shares.isZero() ? "" : (stockSplit.price() * xr / (*it_split).price()).toString(); // put conversion rate for all splits with this currency, so... // every split of transaction have the same conversion rate xrMap.insert(splitCurrency, MyMoneyMoney::ONE / (*it_split).price()); } else xr = (*it_split).price(); break; } } } } else qA["price"] = xr.toString(); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match(&(*it_split)); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA["payee"] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = ", "; } } qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate); qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA["number"] = (*it_split).number(); qA["memo"] = a_memo; qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); qS["reconciledate"] = qA["reconciledate"]; qS["reconcileflag"] = qA["reconcileflag"]; qS["number"] = qA["number"]; qS["topcategory"] = splitAcc.topParentName(); qS["categorytype"] = i18n("Transfer"); // only include the configured accounts if (include_me) { if (loan_special_case) { // put the principal amount in the "value" column and convert to lowest fraction qA["value"] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA["rank"] = '1'; qA["split"] = ""; } else { if ((splits.count() > 2) && use_summary) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA["rank"] = '1'; qA["category"] = i18n("[Split Transaction]"); qA["topcategory"] = i18nc("Split transaction", "Split"); qA["categorytype"] = i18nc("Split transaction", "Split"); m_rows += qA; } } } } else { if (include_me) { if (loan_special_case) { MyMoneyMoney value = (-(* it_split).shares() * xr).convert(fraction); if ((*it_split).action() == MyMoneySplit::ActionAmortization) { // put the payment in the "payment" column and convert to lowest fraction qA["payment"] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::ActionInterest) { // put the interest in the "interest" column and convert to lowest fraction qA["interest"] = value.toString(); } else if (splits.count() > 2) { // [dv: This comment carried from the original code. I am // not exactly clear on what it means or why we do this.] // Put the initial pay-in nowhere (that is, ignore it). This // is dangerous, though. The only way I can tell the initial // pay-in apart from fees is if there are only 2 splits in // the transaction. I wish there was a better way. } else { // accumulate everything else in the "fees" column MyMoneyMoney n0 = MyMoneyMoney(qA["fees"]); qA["fees"] = (n0 + value).toString(); } // we don't add qA here for a loan transaction. we'll add one // qA afer all of the split components have been processed. // (see below) } //--- special case to hide split transaction details else if (hide_details && (splits.count() > 2)) { // essentially, don't add any qA entries } //--- default case includes all transaction details else { //this is when the splits are going to be shown as children of the main split if ((splits.count() > 2) && use_summary) { qA["value"] = ""; //convert to lowest fraction qA["split"] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA["rank"] = '2'; } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category qA["split"] = ""; qA["rank"] = '1'; } qA ["memo"] = (*it_split).memo(); if (report.isConvertCurrency()) qS["currency"] = file->baseCurrency().id(); else qS["currency"] = splitAcc.currencyId(); if (! splitAcc.isIncomeExpense()) { qA["category"] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA["topcategory"] = splitAcc.topParentName(); qA["categorytype"] = i18n("Transfer"); } else { qA ["category"] = splitAcc.fullName(); qA ["topcategory"] = splitAcc.topParentName(); qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup()); } if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) { //if it matches the text of the main split of the transaction or //it matches this particular split, include it //otherwise, skip it //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match(&(*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match(&(*it_split))))) { if (tag_special_case) { if (!tagIdListCache.size()) qA["tag"] = i18n("[No Tag]"); else for (int i = 0; i < tagIdListCache.size(); i++) { qA["tag"] = file->tag(tagIdListCache[i]).name().simplified(); m_rows += qA; } } else { m_rows += qA; } } } } } if (m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) { // otherwise stock split is displayed twice in report if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); qS["rank"] = '1'; qS["account"] = splitAcc.name(); qS["accountid"] = splitAcc.id(); qS["topaccount"] = splitAcc.topParentName(); qS["category"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); qS["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS["memo"] = (*it_split).memo().isEmpty() ? a_memo : (*it_split).memo(); //FIXME-ALEX When is used this? I can't find in which condition we arrive here... maybe this code is useless? QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = '+'; } qS["payee"] = payee.isEmpty() ? qA["payee"] : file->payee(payee).name().simplified(); //check the specific split against the filter for text and amount //TODO this should be done at the engine, but I have no clear idea how -- asoliverez //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match(&(*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match(&(*it_split))))) { m_rows += qS; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } } } } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); // but terminate if this transaction has only a single split if (splits.count() < 2) break; //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case MyMoneyReport::eAccount: case MyMoneyReport::eTopAccount: break; // case MyMoneyReport::eCategory: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account = (* it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (m_config.isConvertCurrency()) qA["currency"] = file->baseCurrency().id(); else qA["currency"] = account.currency().id(); qA["accountid"] = account.id(); qA["account"] = account.name(); qA["topaccount"] = account.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA["rank"] = "0"; qA["price"] = startPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA["shares"] = startShares.toString(); } qA["postdate"] = strStartDate; qA["balance"] = startBalance.convert(fraction).toString(); qA["value"].clear(); qA["id"] = 'A'; m_rows += qA; //ending balance qA["price"] = endPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA["shares"] = endShares.toString(); } qA["postdate"] = strEndDate; qA["balance"] = endBalance.toString(); qA["rank"] = "3"; qA["id"] = 'Z'; m_rows += qA; } } MyMoneyMoney QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const { MyMoneyMoney returnInvestment; if (!buys.isZero() || !startingBal.isZero()) { returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys); returnInvestment = returnInvestment.convert(10000); } else returnInvestment = MyMoneyMoney(); // if no investment then no return on investment return returnInvestment; } MyMoneyMoney QueryTable::helperIRR(const CashFlowList &all) const { MyMoneyMoney annualReturn; try { double irr = all.IRR(); #ifdef Q_CC_MSVC annualReturn = MyMoneyMoney(_isnan(irr) ? 0 : irr, 10000); #else annualReturn = MyMoneyMoney(std::isnan(irr) ? 0 : irr, 10000); #endif } catch (QString e) { qDebug() << e; } return annualReturn; } void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity security; //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); // // Calculate performance // // The following columns are created: // Account, Value on , Buys, Sells, Income, Value on , Return% MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; MyMoneyMoney price; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); //calculate starting balance if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); } else { price = account.deepCurrencyPrice(startingDate); } //work around if there is no price for the starting balance if (!(file->balance(account.id(), startingDate)).isZero() && account.deepCurrencyPrice(startingDate) == MyMoneyMoney::ONE) { MyMoneyTransactionFilter filter; //get the transactions for the time before the report filter.setDateFilter(QDate(), startingDate); filter.addAccount(account.id()); filter.setReportAllSplits(true); QList startTransactions = file->transactionList(filter); if (startTransactions.size() > 0) { //get the last transaction MyMoneyTransaction startTrans = startTransactions.back(); MyMoneySplit s = startTrans.splitByAccount(account.id()); //get the price from the split of that account price = s.price(); if (m_config.isConvertCurrency()) price = price * account.baseCurrencyPrice(startingDate); } } if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); } else { price = account.deepCurrencyPrice(startingDate); } MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price; //convert to lowest fraction startingBal = startingBal.convert(fraction); //calculate ending balance if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); } else { price = account.deepCurrencyPrice(endingDate); } MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); CashFlowList buys; CashFlowList sells; CashFlowList reinvestincome; CashFlowList cashincome; report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); QList transactions = file->transactionList(report); QList::const_iterator it_transaction = transactions.constBegin(); while (it_transaction != transactions.constEnd()) { // s is the split for the stock account MyMoneySplit s = (*it_transaction).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySplit::investTransactionTypeE transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), s, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) { price = account.baseCurrencyPrice((*it_transaction).postDate()); //we only need base currency because the value is in deep currency } else { price = MyMoneyMoney::ONE; } MyMoneyMoney value = assetAccountSplit.value() * price; if (transactionType == MyMoneySplit::BuyShares) buys += CashFlowListItem((*it_transaction).postDate(), value); else if (transactionType == MyMoneySplit::SellShares) sells += CashFlowListItem((*it_transaction).postDate(), value); else if (transactionType == MyMoneySplit::ReinvestDividend) { value = interestSplits.first().value() * price; reinvestincome += CashFlowListItem((*it_transaction).postDate(), -value); } else if (transactionType == MyMoneySplit::Dividend || transactionType == MyMoneySplit::Yield) cashincome += CashFlowListItem((*it_transaction).postDate(), value); ++it_transaction; } // Note that reinvested dividends are not included , because these do not // represent a cash flow event. all += buys; all += sells; all += cashincome; all += CashFlowListItem(startingDate, -startingBal); all += CashFlowListItem(endingDate, endingBal); MyMoneyMoney buysTotal = buys.total(); MyMoneyMoney sellsTotal = sells.total(); MyMoneyMoney cashIncomeTotal = cashincome.total(); MyMoneyMoney reinvestIncomeTotal = reinvestincome.total(); MyMoneyMoney returnInvestment = helperROI(buysTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); MyMoneyMoney annualReturn = helperIRR(all); // check if there are any meaningfull values before adding them to results if (!(buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() && startingBal.isZero() && endingBal.isZero())) { result["return"] = annualReturn.toString(); result["returninvestment"] = returnInvestment.toString(); result["equitytype"] = KMyMoneyUtils::securityTypeToString(security.securityType()); result["buys"] = buysTotal.toString(); result["sells"] = sellsTotal.toString(); result["cashincome"] = cashIncomeTotal.toString(); result["reinvestincome"] = reinvestIncomeTotal.toString(); result["startingbal"] = startingBal.toString(); result["endingbal"] = endingBal.toString(); } } void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity security; MyMoneyMoney price; MyMoneyMoney sellValue; MyMoneyMoney buyValue; MyMoneyMoney sellShares; MyMoneyMoney buyShares; + MyMoneyMoney sellLongValue; + MyMoneyMoney buyLongValue; + MyMoneyMoney sellLongShares; + MyMoneyMoney buyLongShares; + // // Calculate capital gain // // The following columns are created: // Account, Buys, Sells, Capital Gain MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; QDate newStartingDate; QDate newEndingDate; + const bool isSTLT = report.isShowingSTLTCapitalGains(); + const int settlementPeriod = report.settlementPeriod(); + QDate termSeparator = report.termSeparator().addDays(-settlementPeriod); report.validDateRange(startingDate, endingDate); + // Saturday and Sunday aren't valid settlement dates + if (endingDate.dayOfWeek() == Qt::Saturday) + endingDate.addDays(-1); + else if (endingDate.dayOfWeek() == Qt::Sunday) + endingDate.addDays(-2); + + if (termSeparator.dayOfWeek() == Qt::Saturday) + termSeparator.addDays(-1); + else if (termSeparator.dayOfWeek() == Qt::Sunday) + termSeparator.addDays(-2); + + if (startingDate.daysTo(endingDate) <= settlementPeriod) // no days to check for + return; newStartingDate = startingDate; - newEndingDate = endingDate; - MyMoneyMoney endingShares = file->balance(account.id(), endingDate); // get how many shares there are over zero value + newEndingDate = endingDate.addDays(-settlementPeriod); + termSeparator = termSeparator.addDays(-settlementPeriod); + MyMoneyMoney endingShares = file->balance(account.id(), newEndingDate); // get how many shares there are over zero value bool reportedDateRange = true; // flag marking sell transactions between startingDate and endingDate report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); + report.setDateFilter(newStartingDate, newEndingDate); do { QList transactions = file->transactionList(report); for (QList::const_reverse_iterator it_t = transactions.crbegin(); it_t != transactions.crend(); ++it_t) { MyMoneySplit shareSplit = (*it_t).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySplit::investTransactionTypeE transactionType; KMyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice((*it_t).postDate()); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; - MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == MyMoneySplit::BuyShares) { if (endingShares.isZero()) { // add sold shares if (buyShares + shares > sellShares.abs()) { // add partially sold shares - buyValue += (((sellShares.abs() - buyShares)) / shares) * value; + MyMoneyMoney tempVal = (((sellShares.abs() - buyShares)) / shares) * value; + buyValue += tempVal; buyShares = sellShares.abs(); + if (isSTLT && (*it_t).postDate() < termSeparator) { + buyLongValue += tempVal; + buyLongShares = buyShares; + } } else { // add wholly sold shares buyValue += value; buyShares += shares; + if (isSTLT && (*it_t).postDate() < termSeparator) { + buyLongValue += value; + buyLongShares += shares; + } } } else if (endingShares >= shares) { // substract not-sold shares endingShares -= shares; } else { // substract partially not-sold shares - buyValue += ((shares - endingShares) / shares) * value; - buyShares += (shares - endingShares); - endingShares = MyMoneyMoney(0); + MyMoneyMoney tempVal = ((shares - endingShares) / shares) * value; + MyMoneyMoney tempVal2 = (shares - endingShares); + buyValue += tempVal; + buyShares += tempVal2; + if (isSTLT && (*it_t).postDate() < termSeparator) { + buyLongValue += tempVal; + buyLongShares += tempVal2; + } + endingShares = MyMoneyMoney(); } } else if (transactionType == MyMoneySplit::SellShares && reportedDateRange) { sellValue += value; sellShares += shares; } else if (transactionType == MyMoneySplit::SplitShares) { // shares variable is denominator of split ratio here sellShares /= shares; buyShares /= shares; + buyLongShares /= shares; } else if (transactionType == MyMoneySplit::AddShares) { // added shares, when sold give 100% capital gain if (endingShares.isZero()) { // add added shares if (buyShares + shares > sellShares.abs()) { // add partially added shares buyShares = sellShares.abs(); + if ((*it_t).postDate() < termSeparator) + buyLongShares = buyShares; } else { // add wholly added shares buyShares += shares; + if ((*it_t).postDate() < termSeparator) + buyLongShares += shares; } } else if (endingShares >= shares) { // substract not-added shares endingShares -= shares; } else { // substract partially not-added shares - buyShares += (shares - endingShares); - endingShares = MyMoneyMoney(0); + MyMoneyMoney tempVal = (shares - endingShares); + buyShares += tempVal; + if ((*it_t).postDate() < termSeparator) + buyLongShares += tempVal; + endingShares = MyMoneyMoney(); } } else if (transactionType == MyMoneySplit::RemoveShares && reportedDateRange) { // removed shares give no value in return so no capital gain on them sellShares += shares; } } reportedDateRange = false; newEndingDate = newStartingDate; newStartingDate = newStartingDate.addYears(-1); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier } while (!sellShares.isZero() && account.openingDate() <= newEndingDate && sellShares.abs() > buyShares.abs()); + // we've got buy value and no sell value of long-term shares, so get them + if (isSTLT && !buyLongShares.isZero()) { + newStartingDate = startingDate; + newEndingDate = endingDate.addDays(-settlementPeriod); + report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier + QList transactions = file->transactionList(report); + endingShares = buyLongShares; + + foreach (const auto transaction, transactions) { + MyMoneySplit shareSplit = transaction.splitByAccount(account.id()); + MyMoneySplit assetAccountSplit; + QList feeSplits; + QList interestSplits; + MyMoneySecurity currency; + MyMoneySplit::investTransactionTypeE transactionType; + KMyMoneyUtils::dissectTransaction(transaction, shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); + if (m_config.isConvertCurrency()) + price = account.baseCurrencyPrice(transaction.postDate()); //we only need base currency because the value is in deep currency + else + price = MyMoneyMoney::ONE; + MyMoneyMoney value = assetAccountSplit.value() * price; + MyMoneyMoney shares = shareSplit.shares(); + if (transactionType == MyMoneySplit::SellShares) { + if ((sellLongShares + shares).abs() >= buyLongShares) { // add partially sold long-term shares + sellLongValue += (sellLongShares.abs() - buyLongShares) / shares * value; + sellLongShares = buyLongShares; + break; + } else { // add wholly sold long-term shares + sellLongValue += value; + sellLongShares += shares; + } + } else if (transactionType == MyMoneySplit::RemoveShares) { + if ((sellLongShares + shares).abs() >= buyLongShares) { + sellLongShares = buyLongShares; + break; + } else + sellLongShares += shares; + } + } + } + // check if there are any meaningfull values before adding them to results if (!(buyValue.isZero() && sellValue.isZero())) { result["equitytype"] = KMyMoneyUtils::securityTypeToString(security.securityType()); result["buys"] = buyValue.toString(); result["sells"] = sellValue.toString(); result["capitalgain"] = (buyValue + sellValue).toString(); + if (isSTLT) { + result["buysLT"] = buyLongValue.toString(); + result["sellsLT"] = sellLongValue.toString(); + result["capitalgainLT"] = (buyLongValue + sellLongValue).toString(); + result["buysST"] = (buyValue - buyLongValue).toString(); + result["sellsST"] = (sellValue - sellLongValue).toString(); + result["capitalgainST"] = ((buyValue - buyLongValue) + (sellValue - sellLongValue)).toString(); + } } report.setDateFilter(startingDate, endingDate); // reset data filter for next security } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QMap> currencyCashFlow; // for total calculation QList accounts; file->accountList(accounts); for (auto it_account = accounts.constBegin(); it_account != accounts.constEnd(); ++it_account) { // Note, "Investment" accounts are never included in account rows because // they don't contain anything by themselves. In reports, they are only // useful as a "topaccount" aggregator of stock accounts if ((*it_account).isAssetLiability() && m_config.includes((*it_account)) && (*it_account).accountType() != MyMoneyAccount::Investment) { // don't add the account if it is closed. In fact, the business logic // should prevent that an account can be closed with a balance not equal // to zero, but we never know. MyMoneyMoney shares = file->balance((*it_account).id(), m_config.toDate()); if (shares.isZero() && (*it_account).isClosed()) continue; ReportAccount account(*it_account); TableRow qaccountrow; CashFlowList accountCashflow; // for total calculation switch(m_config.queryColumns()) { case MyMoneyReport::eQCperformance: { constructPerformanceRow(account, qaccountrow, accountCashflow); if (!qaccountrow.isEmpty()) { // assuming that that report is grouped by topaccount qaccountrow["topaccount"] = account.topParentName(); if (m_config.isConvertCurrency()) qaccountrow["currency"] = file->baseCurrency().id(); else qaccountrow["currency"] = account.currency().id(); if (!currencyCashFlow.value(qaccountrow.value("currency")).contains(qaccountrow.value("topaccount"))) currencyCashFlow[qaccountrow.value("currency")].insert(qaccountrow.value("topaccount"), accountCashflow); // create cashflow for unknown account... else currencyCashFlow[qaccountrow.value("currency")][qaccountrow.value("topaccount")] += accountCashflow; // ...or add cashflow for known account } break; } case MyMoneyReport::eQCcapitalgain: constructCapitalGainRow(account, qaccountrow); break; default: { //get fraction for account int fraction = account.currency().smallestAccountFraction() != -1 ? account.currency().smallestAccountFraction() : file->baseCurrency().smallestAccountFraction(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()); if (m_config.isConvertCurrency() && account.isForeignCurrency()) netprice *= account.baseCurrencyPrice(m_config.toDate()); // display currency is base currency, so set the price netprice = netprice.reduce(); shares = shares.reduce(); qaccountrow["price"] = netprice.convertPrecision(account.currency().pricePrecision()).toString(); qaccountrow["value"] = (netprice * shares).convert(fraction).toString(); qaccountrow["shares"] = shares.toString(); QString iid = account.institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && !account.isTopLevel()) iid = account.topParent().institutionId(); if (iid.isEmpty()) qaccountrow["institution"] = i18nc("No institution", "None"); else qaccountrow["institution"] = file->institution(iid).name(); qaccountrow["type"] = KMyMoneyUtils::accountTypeToString(account.accountType()); } } if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values continue; qaccountrow["rank"] = '1'; qaccountrow["account"] = account.name(); qaccountrow["accountid"] = account.id(); qaccountrow["topaccount"] = account.topParentName(); if (m_config.isConvertCurrency()) qaccountrow["currency"] = file->baseCurrency().id(); else qaccountrow["currency"] = account.currency().id(); m_rows.append(qaccountrow); } } if (m_config.queryColumns() == MyMoneyReport::eQCperformance) { TableRow qtotalsrow; qtotalsrow["rank"] = "4"; // add identification of row as total QMap currencyGrandCashFlow; QMap>::iterator currencyAccGrp = currencyCashFlow.begin(); while (currencyAccGrp != currencyCashFlow.end()) { // convert map of top accounts with cashflows to TableRow for (QMap::iterator topAccount = (*currencyAccGrp).begin(); topAccount != (*currencyAccGrp).end(); ++topAccount) { qtotalsrow["topaccount"] = topAccount.key(); qtotalsrow["return"] = helperIRR(topAccount.value()).toString(); qtotalsrow["currency"] = currencyAccGrp.key(); currencyGrandCashFlow[currencyAccGrp.key()] += topAccount.value(); // cumulative sum of cashflows of each topaccount m_rows.append(qtotalsrow); // rows aren't sorted yet, so no problem with adding them randomly at the end } ++currencyAccGrp; } QMap::iterator currencyGrp = currencyGrandCashFlow.begin(); qtotalsrow["topaccount"] = ""; // empty topaccount because it's grand cashflow while (currencyGrp != currencyGrandCashFlow.end()) { qtotalsrow["return"] = helperIRR(currencyGrp.value()).toString(); qtotalsrow["currency"] = currencyGrp.key(); m_rows.append(qtotalsrow); ++currencyGrp; } } } void QueryTable::constructSplitsTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["month"] = qS["month"] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA["week"] = qS["week"] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (report.isConvertCurrency()) qA["currency"] = qS["currency"] = file->baseCurrency().id(); else qA["currency"] = qS["currency"] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; //S_end = splits.end(); for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc = (* it_split).accountId(); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & MyMoneyReport::eQCloan) { ReportAccount splitAcc = (*it_split).accountId(); loan_special_case = splitAcc.isLoan(); } // There is a slight chance that at this point myBegin is still pointing to splits.end() if the // transaction only has income and expense splits (which should not happen). In that case, point // it to the first split if (myBegin == splits.end()) { myBegin = splits.begin(); } //the account of the beginning splits ReportAccount myBeginAcc = (*myBegin).accountId(); bool include_me = true; QString a_fullname = ""; QString a_memo = ""; int pass = 1; do { MyMoneyMoney xr; ReportAccount splitAcc = (* it_split).accountId(); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce(); } // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports if (splitAcc.isIncomeExpense()) { xr = -xr; } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); qA["action"] = (*it_split).action(); qA["shares"] = shares.isZero() ? "" : (*it_split).shares().toString(); qA["price"] = shares.isZero() ? "" : xr.convertPrecision(splitAcc.currency().pricePrecision()).toString(); if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && (*it_split).shares().isNegative()) qA["action"] = "Sell"; qA["investaccount"] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); qA["price"] = xr.convertPrecision(splitAcc.currency().pricePrecision()).toString(); qA["account"] = splitAcc.name(); qA["accountid"] = splitAcc.id(); qA["topaccount"] = splitAcc.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report? QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = ','; } qA["payee"] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate); qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA["number"] = (*it_split).number(); qA["memo"] = a_memo; qS["reconciledate"] = qA["reconciledate"]; qS["reconcileflag"] = qA["reconcileflag"]; qS["number"] = qA["number"]; qS["topcategory"] = splitAcc.topParentName(); // only include the configured accounts if (include_me) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); qA["rank"] = '1'; //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA["account"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", myBeginAcc.fullName()) : i18n("Transfer from %1", myBeginAcc.fullName()); } else if (it_split == myBegin) { //handle the main split if ((splits.count() > 2)) { //if it is the main split and has multiple splits, note that qA["account"] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.constBegin(); //there are supposed to be only 2 splits if we ever get here if (tempSplit == myBegin && splits.count() > 1) ++tempSplit; //show the name of the category, or "transfer to/from" if it as an account ReportAccount tempSplitAcc = (*tempSplit).accountId(); if (! tempSplitAcc.isIncomeExpense()) { qA["account"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA["account"] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA["account"] = myBeginAcc.fullName(); } //category data is always the one of the split qA ["category"] = splitAcc.fullName(); qA ["topcategory"] = splitAcc.topParentName(); qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup()); m_rows += qA; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case MyMoneyReport::eAccount: case MyMoneyReport::eTopAccount: break; // case MyMoneyReport::eCategory: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account = (* it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (m_config.isConvertCurrency()) qA["currency"] = file->baseCurrency().id(); else qA["currency"] = account.currency().id(); qA["accountid"] = account.id(); qA["account"] = account.name(); qA["topaccount"] = account.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA["rank"] = "0"; qA["price"] = startPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA["shares"] = startShares.toString(); } qA["postdate"] = strStartDate; qA["balance"] = startBalance.convert(fraction).toString(); qA["value"].clear(); qA["id"] = 'A'; m_rows += qA; qA["rank"] = "3"; //ending balance qA["price"] = endPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA["shares"] = endShares.toString(); } qA["postdate"] = strEndDate; qA["balance"] = endBalance.toString(); qA["id"] = 'Z'; m_rows += qA; } } } diff --git a/kmymoney/widgets/CMakeLists.txt b/kmymoney/widgets/CMakeLists.txt index 9934fd438..25ffe1a8c 100644 --- a/kmymoney/widgets/CMakeLists.txt +++ b/kmymoney/widgets/CMakeLists.txt @@ -1,198 +1,199 @@ ########### create links ############### set(kmymoney_STAT_HEADERS kaccounttemplateselector.h kbudgetvalues.h kguiutils.h kmymoneyaccountcombo.h kmymoneyaccountcompletion.h kmymoneyaccountselector.h kmymoneycategory.h kmymoneycombo.h kmymoneymvccombo.h kmymoneycompletion.h kmymoneycurrencyselector.h kmymoneydateinput.h kmymoneyedit.h kmymoneylineedit.h kmymoneyselector.h kmymoneytitlelabel.h kmymoneywizard.h register.h registeritem.h scheduledtransaction.h selectedtransaction.h stdtransactiondownloaded.h stdtransactionmatched.h transactioneditorcontainer.h transactionform.h transaction.h transactionsortoptionimpl.h reporttabimpl.h reportcontrolimpl.h kmymoneyvalidationfeedback.h onlinejobmessagesview.h kmymoneydateedit.h amountedit.h ) ########### Shared widget library ########### set(kmm_widgets_sources kmymoneydateinput.cpp kmymoneyvalidationfeedback.cpp styleditemdelegateforwarder.cpp kmymoneyedit.cpp kmymoneylineedit.cpp kmymoneytextedit.cpp kmymoneymvccombo.cpp kmymoneyselector.cpp kmymoneycalculator.cpp ktreewidgetfilterlinewidget.cpp kguiutils.cpp onlinejobmessagesview.cpp kmymoneydateedit.cpp amountedit.cpp ) ki18n_wrap_ui(kmm_widgets_sources kmymoneyvalidationfeedback.ui onlinejobmessagesview.ui ) add_library(kmm_widgets SHARED ${kmm_widgets_sources}) target_link_libraries(kmm_widgets PUBLIC KF5::TextWidgets KF5::KIOWidgets KF5::Completion KF5::Notifications KF5::ItemViews KF5::I18n Qt5::Gui Qt5::Sql Qt5::Core Alkimia::alkimia kmm_config kmm_mymoney ) set_target_properties(kmm_widgets PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} COMPILE_FLAGS "-DKMM_BUILD_WIDGETS_LIB" ) generate_export_header(kmm_widgets) install(TARGETS kmm_widgets ${INSTALL_TARGETS_DEFAULT_ARGS} ) ########### Basic Widget Library (kmymoney_base) STATIC ################# # Common sources for libkmymoney.so and libwidgets.a that do not # contain the KMM_DESIGNER flag set(_uncritial_common_sources fixedcolumntreeview.cpp kbudgetvalues.cpp kmymoneyaccountcombo.cpp kmymoneyaccountselector.cpp kmymoneyaccounttreeview.cpp kmymoneycombo.cpp kmymoneycompletion.cpp kmymoneytitlelabel.cpp kmymoneydateedit.cpp kmymoneywizard.cpp kpricetreeitem.cpp registeritem.cpp scheduledtransaction.cpp selectedtransaction.cpp stdtransactiondownloaded.cpp stdtransactionmatched.cpp transactionform.cpp transactionsortoptionimpl.cpp ) # sources that contain the KMM_DESIGNER flag set (_critial_common_sources kaccounttemplateselector.cpp kmymoneycurrencyselector.cpp kmymoneyaccountcompletion.cpp kmymoneycategory.cpp register.cpp transaction.cpp transactioneditorcontainer.cpp ) set (kmymoney_base_UI kbudgetvaluesdecl.ui transactionsortoptiondecl.ui kaccounttemplateselectordecl.ui ) ki18n_wrap_ui(kmymoney_base_ui_srcs ${kmymoney_base_UI}) set(_uncritial_common_sources ${_uncritial_common_sources} ${kmymoney_base_ui_srcs}) # in order to use add_dependencies, we need to add this custom target # for all generated header files. # (see http://www.vtk.org/Wiki/CMake_FAQ#How_can_I_add_a_dependency_to_a_source_file_which_is_generated_in_a_subdirectory.3F ) add_custom_target(generate_base_ui_srcs DEPENDS ${kmymoney_base_ui_srcs}) # We can compile the uncritical sources without KMM_DESIGNER flags add_library(kmymoney_base STATIC ${_uncritial_common_sources}) # TODO: fix dependencies target_link_libraries(kmymoney_base KF5::XmlGui KF5::TextWidgets KF5::IconThemes KF5::I18n KF5::ConfigWidgets KF5::ConfigCore KF5::Completion Qt5::Gui Qt5::Widgets Qt5::Sql Qt5::Xml Alkimia::alkimia) add_dependencies(kmymoney_base kmm_config) ########### QtDesigner Widget Library (kmymoneywidgets) ################# # we never link against this library, # but it is needed for uic and QtDesigner if( USE_QT_DESIGNER ) set(kmymoneywidgets_PART_SRCS ${CMAKE_CURRENT_BINARY_DIR}/kmymoneywidgets.cpp) kde4_add_widget_files(kmymoneywidgets_PART_SRCS kmymoney.widgets) set(kmymoneywidgets_PART_SRCS ${_critial_common_sources} ${kmymoneywidgets_PART_SRCS}) add_library(kmymoneywidgets MODULE ${kmymoneywidgets_PART_SRCS}) include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) # The option -DKMM_DESIGNER will leave away any code that breaks uic. set_target_properties(kmymoneywidgets PROPERTIES COMPILE_FLAGS "-DKMM_DESIGNER") # The qt-designer widget library shouldn't need to link against the # dialogs and converter libraries. If a widget references something # from one of these libraries it is most likely due to code that needs # to be excluded with a KMM_DESIGNER ifndef. target_link_libraries(kmymoneywidgets kmymoney_base kmm_mymoney kmymoney_common kmm_config models) install(TARGETS kmymoneywidgets DESTINATION ${QT_PLUGINS_DIR}/designer ) endif( USE_QT_DESIGNER ) ########### Widget Library (widgets) STATIC ################# set(libwidgets_a_SOURCES ${_critial_common_sources} kmymoneybriefschedule.cpp registersearchline.cpp transactioneditorcontainer.cpp reporttabimpl.cpp reportcontrolimpl.cpp daterangedlg.cpp ) set(libwidgets_a_UI kschedulebriefwidget.ui reportcontrol.ui reporttabgeneral.ui reporttabrowcolquery.ui reporttabrowcolpivot.ui reporttabrange.ui reporttabchart.ui + reporttabcapitalgain.ui daterangedlgdecl.ui ) # using uic on the above UI files DEPENDS on libkmymoney.so. If uic # does not find libkmymoney.so, gcc will fail compiling # kmymoneyreportconfigtab2decl.cpp and throw errors like "invalid use # of undefined type `struct KMyMoneyGeneralCombo'" ki18n_wrap_ui(widgets_ui_srcs ${libwidgets_a_UI}) add_custom_target(generate_widgets_ui_srcs DEPENDS ${widgets_ui_srcs}) add_library(widgets STATIC ${libwidgets_a_SOURCES} ${widgets_ui_srcs} ) target_link_libraries(widgets KF5::XmlGui kmymoney_base) add_dependencies(widgets kmm_config) ########### install files ############### install(FILES ${kmymoney_STAT_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kmm_widgets_export.h DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney COMPONENT Devel) diff --git a/kmymoney/widgets/reporttabcapitalgain.ui b/kmymoney/widgets/reporttabcapitalgain.ui new file mode 100644 index 000000000..e4bc4d277 --- /dev/null +++ b/kmymoney/widgets/reporttabcapitalgain.ui @@ -0,0 +1,112 @@ + + + ReportTabCapitalGain + + + + 0 + 0 + 441 + 102 + + + + Report Tab + + + + + + <p>On this tab, you set the basic properties of this report.</p> + + + + + + + + Time in days between the settlement date and the transaction date. + + + 30 + + + 3 + + + + + + + Settlement period + + + + + + + Before this date investments are counted as long-term investments. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Terms separator + + + + + + + + + + Show short-term and long-term capital gains + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 20 + + + + + + + + + + kMyMoneyDateInput + QWidget +
kmymoneydateinput.h
+
+
+ + +
diff --git a/kmymoney/widgets/reporttabimpl.cpp b/kmymoney/widgets/reporttabimpl.cpp index c9873b15b..9bdec8805 100644 --- a/kmymoney/widgets/reporttabimpl.cpp +++ b/kmymoney/widgets/reporttabimpl.cpp @@ -1,257 +1,271 @@ /* This file is part of the KDE project Copyright (C) 2009 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "reporttabimpl.h" #include "kmymoneyglobalsettings.h" #include #include #include "ui_reporttabgeneral.h" #include "ui_reporttabrowcolpivot.h" #include "ui_reporttabrowcolquery.h" #include "ui_reporttabchart.h" #include "ui_reporttabrange.h" +#include "ui_reporttabcapitalgain.h" #include #include "mymoney/mymoneyreport.h" ReportTabGeneral::ReportTabGeneral(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabGeneral; ui->setupUi(this); } ReportTabGeneral::~ReportTabGeneral() { delete ui; } ReportTabRowColPivot::ReportTabRowColPivot(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabRowColPivot; ui->setupUi(this); } ReportTabRowColPivot::~ReportTabRowColPivot() { delete ui; } ReportTabRowColQuery::ReportTabRowColQuery(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabRowColQuery; ui->setupUi(this); ui->buttonGroup1->setExclusive(false); ui->buttonGroup1->setId(ui->m_checkMemo, 0); ui->buttonGroup1->setId(ui->m_checkShares, 1); ui->buttonGroup1->setId(ui->m_checkPrice, 2); ui->buttonGroup1->setId(ui->m_checkReconciled, 3); ui->buttonGroup1->setId(ui->m_checkAccount, 4); ui->buttonGroup1->setId(ui->m_checkNumber, 5); ui->buttonGroup1->setId(ui->m_checkPayee, 6); ui->buttonGroup1->setId(ui->m_checkCategory, 7); ui->buttonGroup1->setId(ui->m_checkAction, 8); ui->buttonGroup1->setId(ui->m_checkBalance, 9); connect(ui->m_checkHideTransactions, SIGNAL(toggled(bool)), this, SLOT(slotHideTransactionsChanged(bool))); } void ReportTabRowColQuery::slotHideTransactionsChanged(bool checked) { if (checked) // toggle m_checkHideSplitDetails only if it's mandatory ui->m_checkHideSplitDetails->setChecked(checked); ui->m_checkHideSplitDetails->setEnabled(!checked); // hiding transactions without hiding splits isn't allowed } ReportTabRowColQuery::~ReportTabRowColQuery() { delete ui; } ReportTabChart::ReportTabChart(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabChart; ui->setupUi(this); ui->m_comboType->addItem(i18nc("type of graphic chart", "Line"), MyMoneyReport::eChartLine); ui->m_comboType->addItem(i18nc("type of graphic chart", "Bar"), MyMoneyReport::eChartBar); ui->m_comboType->addItem(i18nc("type of graphic chart", "Stacked Bar"), MyMoneyReport::eChartStackedBar); ui->m_comboType->addItem(i18nc("type of graphic chart", "Pie"), MyMoneyReport::eChartPie); ui->m_comboType->addItem(i18nc("type of graphic chart", "Ring"), MyMoneyReport::eChartRing); connect(ui->m_comboType, SIGNAL(currentIndexChanged(int)), this, SLOT(slotChartTypeChanged(int))); emit ui->m_comboType->currentIndexChanged(ui->m_comboType->currentIndex()); } ReportTabChart::~ReportTabChart() { delete ui; } void ReportTabChart::slotChartTypeChanged(int index) { if (index == MyMoneyReport::EChartType::eChartPie || index == MyMoneyReport::EChartType::eChartRing) { ui->m_checkCHGridLines->setText(i18n("Show circular grid lines")); ui->m_checkSVGridLines->setText(i18n("Show sagittal grid lines")); ui->m_logYaxis->setChecked(false); ui->m_logYaxis->setEnabled(false); } else { ui->m_checkCHGridLines->setText(i18n("Show horizontal grid lines")); ui->m_checkSVGridLines->setText(i18n("Show vertical grid lines")); ui->m_logYaxis->setEnabled(true); } } ReportTabRange::ReportTabRange(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabRange; ui->setupUi(this); m_dateRange = new DateRangeDlg(this->parentWidget()); ui->dateRangeGrid->addWidget(m_dateRange, 0, 0, 1, 2); connect(ui->m_yLabelsPrecision, SIGNAL(valueChanged(int)), this, SLOT(slotYLabelsPrecisionChanged(int))); emit ui->m_yLabelsPrecision->valueChanged(ui->m_yLabelsPrecision->value()); connect(ui->m_dataRangeStart, SIGNAL(editingFinished()), this, SLOT(slotEditingFinishedStart())); connect(ui->m_dataRangeEnd, SIGNAL(editingFinished()), this, SLOT(slotEditingFinishedEnd())); connect(ui->m_dataMajorTick, SIGNAL(editingFinished()), this, SLOT(slotEditingFinishedMajor())); connect(ui->m_dataMinorTick, SIGNAL(editingFinished()), this, SLOT(slotEditingFinishedMinor())); connect(ui->m_dataLock, SIGNAL(currentIndexChanged(int)), this, SLOT(slotDataLockChanged(int))); emit ui->m_dataLock->currentIndexChanged(ui->m_dataLock->currentIndex()); } ReportTabRange::~ReportTabRange() { delete ui; } void ReportTabRange::setRangeLogarythmic(bool set) { // major and minor tick have no influence if axis is logarithmic so hide them if (set) { ui->lblDataMajorTick->hide(); ui->lblDataMinorTick->hide(); ui->m_dataMajorTick->hide(); ui->m_dataMinorTick->hide(); } else { ui->lblDataMajorTick->show(); ui->lblDataMinorTick->show(); ui->m_dataMajorTick->show(); ui->m_dataMinorTick->show(); } } void ReportTabRange::slotEditingFinished(EDimension dim) { qreal dataRangeStart = locale().toDouble(ui->m_dataRangeStart->text()); qreal dataRangeEnd = locale().toDouble(ui->m_dataRangeEnd->text()); qreal dataMajorTick = locale().toDouble(ui->m_dataMajorTick->text()); qreal dataMinorTick = locale().toDouble(ui->m_dataMinorTick->text()); if (dataRangeEnd < dataRangeStart) { // end must be higher than start if (dim == eRangeEnd) { ui->m_dataRangeStart->setText(ui->m_dataRangeEnd->text()); dataRangeStart = dataRangeEnd; } else { ui->m_dataRangeEnd->setText(ui->m_dataRangeStart->text()); dataRangeEnd = dataRangeStart; } } if ((dataRangeStart != 0 || dataRangeEnd != 0)) { // if data range isn't going to be reset if ((dataRangeEnd - dataRangeStart) < dataMajorTick) // major tick cannot be greater than data range dataMajorTick = dataRangeEnd - dataRangeStart; if (dataMajorTick != 0 && // if major tick isn't going to be reset dataMajorTick < (dataRangeEnd - dataRangeStart) * 0.01) // constraint major tick to be greater or equal to 1% of data range dataMajorTick = (dataRangeEnd - dataRangeStart) * 0.01; // that should produce more than 256 Y labels in KReportChartView::slotNeedUpdate //set precision of major tick to be greater by 1 ui->m_dataMajorTick->setText(locale().toString(dataMajorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); } if (dataMajorTick < dataMinorTick) { // major tick must be higher than minor if (dim == eMinorTick) { ui->m_dataMajorTick->setText(ui->m_dataMinorTick->text()); dataMajorTick = dataMinorTick; } else { ui->m_dataMinorTick->setText(ui->m_dataMajorTick->text()); dataMinorTick = dataMajorTick; } } if (dataMinorTick < dataMajorTick * 0.1) { // constraint minor tick to be greater or equal to 10% of major tick, and set precision to be greater by 1 dataMinorTick = dataMajorTick * 0.1; ui->m_dataMinorTick->setText(locale().toString(dataMinorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); } } void ReportTabRange::slotEditingFinishedStart() { slotEditingFinished(eRangeStart); } void ReportTabRange::slotEditingFinishedEnd() { slotEditingFinished(eRangeEnd); } void ReportTabRange::slotEditingFinishedMajor() { slotEditingFinished(eMajorTick); } void ReportTabRange::slotEditingFinishedMinor() { slotEditingFinished(eMinorTick); } void ReportTabRange::slotYLabelsPrecisionChanged(const int& value) { ui->m_dataRangeStart->setValidator(0); ui->m_dataRangeEnd->setValidator(0); ui->m_dataMajorTick->setValidator(0); ui->m_dataMinorTick->setValidator(0); MyDoubleValidator *dblVal = new MyDoubleValidator(value); ui->m_dataRangeStart->setValidator(dblVal); ui->m_dataRangeEnd->setValidator(dblVal); MyDoubleValidator *dblVal2 = new MyDoubleValidator(value + 1); ui->m_dataMajorTick->setValidator(dblVal2); ui->m_dataMinorTick->setValidator(dblVal2); } void ReportTabRange::slotDataLockChanged(int index) { if (index == MyMoneyReport::dataOptionE::automatic) { ui->m_dataRangeStart->setText(QStringLiteral("0")); ui->m_dataRangeEnd->setText(QStringLiteral("0")); ui->m_dataMajorTick->setText(QStringLiteral("0")); ui->m_dataMinorTick->setText(QStringLiteral("0")); ui->m_dataRangeStart->setEnabled(false); ui->m_dataRangeEnd->setEnabled(false); ui->m_dataMajorTick->setEnabled(false); ui->m_dataMinorTick->setEnabled(false); } else { ui->m_dataRangeStart->setEnabled(true); ui->m_dataRangeEnd->setEnabled(true); ui->m_dataMajorTick->setEnabled(true); ui->m_dataMinorTick->setEnabled(true); } } + +ReportTabCapitalGain::ReportTabCapitalGain(QWidget *parent) + : QWidget(parent) +{ + ui = new Ui::ReportTabCapitalGain; + ui->setupUi(this); +} + +ReportTabCapitalGain::~ReportTabCapitalGain() +{ + delete ui; +} + diff --git a/kmymoney/widgets/reporttabimpl.h b/kmymoney/widgets/reporttabimpl.h index 27d71d4a1..f6718a103 100644 --- a/kmymoney/widgets/reporttabimpl.h +++ b/kmymoney/widgets/reporttabimpl.h @@ -1,132 +1,141 @@ /* This file is part of the KDE project Copyright (C) 2009 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef REPORTTABIMPL_H #define REPORTTABIMPL_H #include #include class DateRangeDlg; namespace Ui { class ReportTabGeneral; class ReportTabRowColPivot; class ReportTabRowColQuery; class ReportTabChart; class ReportTabRange; +class ReportTabCapitalGain; } class ReportTabGeneral : public QWidget { public: ReportTabGeneral(QWidget *parent); virtual ~ReportTabGeneral(); Ui::ReportTabGeneral* ui; }; class ReportTabRowColPivot : public QWidget { public: ReportTabRowColPivot(QWidget *parent); virtual ~ReportTabRowColPivot(); Ui::ReportTabRowColPivot* ui; }; class ReportTabRowColQuery : public QWidget { Q_OBJECT public: ReportTabRowColQuery(QWidget *parent); virtual ~ReportTabRowColQuery(); Ui::ReportTabRowColQuery* ui; private slots: void slotHideTransactionsChanged(bool checked); }; class ReportTabChart : public QWidget { Q_OBJECT public: ReportTabChart(QWidget *parent); virtual ~ReportTabChart(); Ui::ReportTabChart* ui; private slots: void slotChartTypeChanged(int index); }; class ReportTabRange : public QWidget { Q_OBJECT public: ReportTabRange(QWidget *parent); virtual ~ReportTabRange(); Ui::ReportTabRange* ui; DateRangeDlg *m_dateRange; void setRangeLogarythmic(bool set); private: enum EDimension { eRangeStart = 0, eRangeEnd, eMajorTick, eMinorTick}; private slots: void slotEditingFinished(EDimension dim); void slotEditingFinishedStart(); void slotEditingFinishedEnd(); void slotEditingFinishedMajor(); void slotEditingFinishedMinor(); void slotYLabelsPrecisionChanged(const int &value); void slotDataLockChanged(int index); }; +class ReportTabCapitalGain : public QWidget +{ +public: + ReportTabCapitalGain(QWidget *parent); + virtual ~ReportTabCapitalGain(); + Ui::ReportTabCapitalGain* ui; +}; + class MyDoubleValidator : public QDoubleValidator { public: MyDoubleValidator(int decimals, QObject * parent = 0) : QDoubleValidator(0, 0, decimals, parent) { } QValidator::State validate(QString &s, int &i) const { Q_UNUSED(i); if (s.isEmpty() || s == "-") { return QValidator::Intermediate; } QChar decimalPoint = locale().decimalPoint(); if(s.indexOf(decimalPoint) != -1) { int charsAfterPoint = s.length() - s.indexOf(decimalPoint) - 1; if (charsAfterPoint > decimals()) { return QValidator::Invalid; } } bool ok; locale().toDouble(s, &ok); if (ok) { return QValidator::Acceptable; } else { return QValidator::Invalid; } } }; #endif /* REPORTTABIMPL_H */