#include "dialog/AddressCheckerIndexDialog.h"
#include "dialog/BalanceDialog.h"
#include "dialog/DebugInfoDialog.h"
+#include "dialog/HistoryExportDialog.h"
#include "dialog/PasswordDialog.h"
#include "dialog/TxBroadcastDialog.h"
#include "dialog/TxConfAdvDialog.h"
}
void MainWindow::onExportHistoryCSV() {
- QString filePath = QFileDialog::getSaveFileName(this, "Save CSV file", QDir::homePath(), "CSV (*.csv)");
- if (filePath.isEmpty())
- return;
- if (!filePath.endsWith(".csv"))
- filePath += ".csv";
- QFileInfo fileInfo(filePath);
- QDir dir = fileInfo.absoluteDir();
-
- if (!dir.exists()) {
- Utils::showError(this, "Unable to export transaction history", QString("Path does not exist: %1").arg(dir.absolutePath()));
- return;
- }
-
- bool success = m_wallet->history()->writeCSV(filePath);
- if (!success) {
- Utils::showError(this, "Unable to export transaction history", QString("No permission to write to: %1").arg(filePath));
- return;
- }
-
- Utils::showInfo(this, "CSV export", QString("Transaction history exported to %1").arg(filePath));
+ HistoryExportDialog dialog{m_wallet, this};
+ dialog.exec();
}
void MainWindow::onImportHistoryDescriptionsCSV() {
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#include "HistoryExportDialog.h"
+#include "ui_HistoryExportDialog.h"
+
+#include <QFileDialog>
+
+#include "Utils.h"
+#include "WalletManager.h"
+#include "libwalletqt/Wallet.h"
+#include "TransactionHistory.h"
+#include "utils/AppData.h"
+
+HistoryExportDialog::HistoryExportDialog(Wallet *wallet, QWidget *parent)
+ : WindowModalDialog(parent)
+ , ui(new Ui::HistoryExportDialog)
+ , m_wallet(wallet)
+{
+ ui->setupUi(this);
+
+ connect(ui->btn_export, &QPushButton::clicked, this, &HistoryExportDialog::exportHistory);
+
+ connect(ui->radio_everything, &QRadioButton::toggled, [this](bool toggled) {
+ if (!toggled) return;
+ this->setEverything();
+ });
+
+ connect(ui->radio_YTD, &QRadioButton::toggled, [this](bool toggled) {
+ if (!toggled) return;
+ ui->date_min->setDate(QDate(QDate::currentDate().year(), 1, 1));
+ ui->date_max->setDate(QDate::currentDate());
+ });
+
+ connect(ui->radio_lastDays, &QRadioButton::toggled, [this](bool toggled) {
+ ui->spin_days->setEnabled(toggled);
+ if (!toggled) return;
+ ui->date_min->setDate(QDate::currentDate().addDays(-ui->spin_days->value() + 1));
+ ui->date_max->setDate(QDate::currentDate());
+ });
+
+ connect(ui->spin_days, &QSpinBox::valueChanged, [this] {
+ ui->date_min->setDate(QDate::currentDate().addDays(-ui->spin_days->value() + 1));
+ ui->date_max->setDate(QDate::currentDate());
+ });
+
+ connect(ui->radio_lastMonth, &QRadioButton::toggled, [this](bool toggled) {
+ if (!toggled) return;
+ ui->date_min->setDate(QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1).addMonths(-1));
+ ui->date_max->setDate(QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1).addDays(-1));
+ });
+
+ connect(ui->radio_lastYear, &QRadioButton::toggled, [this](bool toggled) {
+ if (!toggled) return;
+ ui->date_min->setDate(QDate(QDate::currentDate().year() - 1, 1, 1));
+ ui->date_max->setDate(QDate(QDate::currentDate().year(), 1, 1).addDays(-1));
+ });
+
+ connect(ui->radio_customRange, &QRadioButton::toggled, [this](bool toggled) {
+ ui->date_min->setEnabled(toggled);
+ ui->date_max->setEnabled(toggled);
+ });
+
+ this->setEverything();
+
+ this->adjustSize();
+}
+
+void HistoryExportDialog::setEverything()
+{
+ ui->date_min->setDate(QDate(2014,4,18));
+ ui->date_max->setDate(QDate::currentDate());
+}
+
+void HistoryExportDialog::exportHistory()
+{
+ QString wallet_name = m_wallet->walletName();
+ QString csv_file_name = QString("/history_export_%1.csv").arg(wallet_name);
+
+ QString filePath = QFileDialog::getSaveFileName(this, "Save CSV file", QDir::homePath() + csv_file_name, "CSV (*.csv)");
+ if (filePath.isEmpty())
+ return;
+ if (!filePath.endsWith(".csv"))
+ filePath += ".csv";
+ QFileInfo fileInfo(filePath);
+ QDir dir = fileInfo.absoluteDir();
+
+ if (!dir.exists()) {
+ Utils::showError(this, "Unable to export transaction history", QString("Path does not exist: %1").arg(dir.absolutePath()));
+ return;
+ }
+
+ auto num_transactions = m_wallet->history()->count();
+
+ QList<QPair<uint64_t, QString>> csvData;
+
+ for (int i = 0; i < num_transactions; i++) {
+ TransactionRow* tx = m_wallet->history()->transaction(i);
+
+ QDate minimumDate = ui->date_min->date();
+ if (tx->timestamp().date() < minimumDate) {
+ continue;
+ }
+
+ QDate maximumDate = ui->date_max->date();
+ if (tx->timestamp().date() > maximumDate) {
+ continue;
+ }
+
+ if (ui->check_excludePending->isChecked() && tx->isPending()) {
+ continue;
+ }
+
+ if (ui->check_excludeFailed->isChecked() && tx->isFailed()) {
+ continue;
+ }
+
+ if (ui->radio_incomingTransactions->isChecked() && tx->direction() != TransactionRow::Direction_In) {
+ continue;
+ }
+
+ if (ui->radio_outgoingTransactions->isChecked() && tx->direction() != TransactionRow::Direction_Out) {
+ continue;
+ }
+
+ if (ui->radio_coinbaseTransactions->isChecked() && !tx->isCoinbase()) {
+ continue;
+ }
+
+ QString date = QString("%1T%2Z").arg(tx->date(), tx->time());
+
+ QString direction = QString("");
+ if (tx->direction() == TransactionRow::Direction_In)
+ direction = QString("in");
+ else if (tx->direction() == TransactionRow::Direction_Out)
+ direction = QString("out");
+ else
+ continue; // skip TransactionInfo::Direction_Both
+
+ QString balanceDelta = WalletManager::displayAmount(abs(tx->balanceDelta()));
+ if (tx->direction() == TransactionRow::Direction_Out) {
+ balanceDelta = "-" + balanceDelta;
+ }
+
+ QString paymentId = tx->paymentId();
+ if (paymentId == "0000000000000000") {
+ paymentId = "";
+ }
+
+ const double usd_price = appData()->txFiatHistory->get(tx->timestamp().toString("yyyyMMdd"));
+ double fiat_price = usd_price * tx->amount();
+ QString fiatAmount = (usd_price > 0) ? QString::number(fiat_price, 'f', 2) : "?";
+
+ QString line = QString(R"(%1,%2,"%3",%4,"%5",%6,%7,%8,"%9","%10","%11","%12","%13")")
+ .arg(QString::number(tx->blockHeight()),
+ QString::number(tx->timestamp().toSecsSinceEpoch()),
+ date,
+ QString::number(tx->subaddrAccount()),
+ direction,
+ balanceDelta,
+ tx->displayAmount(),
+ tx->fee(),
+ tx->hash(),
+ tx->description(),
+ paymentId,
+ fiatAmount,
+ "USD");
+ csvData.append({tx->blockHeight(), line});
+ }
+
+ std::sort(csvData.begin(), csvData.end(), [](const QPair<uint64_t, QString> &tx1, const QPair<uint64_t, QString> &tx2){
+ return tx1.first < tx2.first;
+ });
+
+ QString csvString = "blockHeight,timestamp,date,accountIndex,direction,balanceDelta,amount,fee,txid,description,paymentId,fiatAmount,fiatCurrency";
+ for (const auto& data : csvData) {
+ csvString += "\n" + data.second;
+ }
+
+ bool success = Utils::fileWrite(filePath, csvString);
+
+ if (!success) {
+ Utils::showError(this, "Unable to export transaction history", QString("No permission to write to: %1").arg(filePath));
+ return;
+ }
+
+ Utils::showInfo(this, "CSV export", QString("Transaction history exported to:\n\n%1").arg(filePath));
+}
+
+HistoryExportDialog::~HistoryExportDialog() = default;
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef HISTORYEXPORTDIALOG_H
+#define HISTORYEXPORTDIALOG_H
+
+#include "components.h"
+
+namespace Ui {
+class HistoryExportDialog;
+}
+
+class Wallet;
+class HistoryExportDialog : public WindowModalDialog
+{
+ Q_OBJECT
+
+public:
+ explicit HistoryExportDialog(Wallet *wallet, QWidget *parent);
+ ~HistoryExportDialog() override;
+
+private:
+ void setEverything();
+ void exportHistory();
+
+ QScopedPointer<Ui::HistoryExportDialog> ui;
+ Wallet *m_wallet;
+};
+
+
+#endif //HISTORYEXPORTDIALOG_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>HistoryExportDialog</class>
+ <widget class="QDialog" name="HistoryExportDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>351</width>
+ <height>539</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>History Export</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Export range</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QRadioButton" name="radio_everything">
+ <property name="text">
+ <string>Everything</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_YTD">
+ <property name="text">
+ <string>Year to Date</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QRadioButton" name="radio_lastDays">
+ <property name="text">
+ <string>Last</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spin_days">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>99999</number>
+ </property>
+ <property name="value">
+ <number>90</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>days</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_lastMonth">
+ <property name="text">
+ <string>Last month</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_lastYear">
+ <property name="text">
+ <string>Last year</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_customRange">
+ <property name="text">
+ <string>Custom range</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Policy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>From</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDateEdit" name="date_min">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="displayFormat">
+ <string>yyyy-MM-dd</string>
+ </property>
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>To</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDateEdit" name="date_max">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="displayFormat">
+ <string>yyyy-MM-dd</string>
+ </property>
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>What to export</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QRadioButton" name="radio_allTransactions">
+ <property name="text">
+ <string>All transactions</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_incomingTransactions">
+ <property name="text">
+ <string>Incoming transactions</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_outgoingTransactions">
+ <property name="text">
+ <string>Outgoing transactions</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_coinbaseTransactions">
+ <property name="text">
+ <string>Coinbase (miner) transactions</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="check_excludePending">
+ <property name="text">
+ <string>Exclude pending transactions</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="check_excludeFailed">
+ <property name="text">
+ <string>Exclude failed transactions</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QPushButton" name="btn_export">
+ <property name="text">
+ <string>Export</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::StandardButton::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>HistoryExportDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>HistoryExportDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
m_rows.clear();
}
-bool TransactionHistory::writeCSV(const QString &path) {
- QString data;
- QReadLocker locker(&m_lock);
-
- auto transactions = m_rows;
-
- std::sort(transactions.begin(), transactions.end(), [](const TransactionRow *info1, const TransactionRow *info2){
- return info1->blockHeight() < info2->blockHeight();
- });
-
- for (const auto &info : transactions) {
- // collect column data
- QDateTime timeStamp = info->timestamp();
- double amount = info->amount();
-
- // calc historical fiat price
- QString fiatAmount;
- QString preferredFiatSymbol = conf()->get(Config::preferredFiatCurrency).toString();
- const double usd_price = appData()->txFiatHistory->get(timeStamp.toString("yyyyMMdd"));
- double fiat_price = usd_price * amount;
-
- if (preferredFiatSymbol != "USD")
- fiat_price = appData()->prices.convert("USD", preferredFiatSymbol, fiat_price);
- double fiat_rounded = ceil(Utils::roundSignificant(fiat_price, 3) * 100.0) / 100.0;
- if (usd_price != 0)
- fiatAmount = QString::number(fiat_rounded);
- else
- fiatAmount = "\"?\"";
-
- QString direction = QString("");
- if (info->direction() == TransactionRow::Direction_In)
- direction = QString("in");
- else if (info->direction() == TransactionRow::Direction_Out)
- direction = QString("out");
- else
- continue; // skip TransactionInfo::Direction_Both
-
- QString displayAmount = info->displayAmount();
- QString paymentId = info->paymentId();
- if (paymentId == "0000000000000000") {
- paymentId = "";
- }
-
- QString date = QString("%1T%2Z").arg(info->date(), info->time()); // ISO 8601
-
- QString balanceDelta = WalletManager::displayAmount(info->balanceDelta());
- if (info->direction() == TransactionRow::Direction_Out) {
- balanceDelta = "-" + balanceDelta;
- }
-
- // format and write
- QString line = QString("\n%1,%2,\"%3\",%4,\"%5\",%6,%7,%8,\"%9\",\"%10\",\"%11\",%12,\"%13\"")
- .arg(QString::number(info->blockHeight()), QString::number(timeStamp.toSecsSinceEpoch()), date,
- QString::number(info->subaddrAccount()), direction, balanceDelta, info->displayAmount(),
- info->fee(), info->hash(), info->description(), paymentId, fiatAmount, preferredFiatSymbol);
- data += line;
- }
-
- data = QString("blockHeight,timestamp,date,accountIndex,direction,balanceDelta,amount,fee,txid,description,paymentId,fiatAmount,fiatCurrency%1").arg(data);
- return Utils::fileWrite(path, data);
-}
-
QStringList parseCSVLine(const QString &line) {
QStringList result;
QString currentField;
bool locked() const;
void clearRows();
- bool writeCSV(const QString &path);
QString importLabelsFromCSV(const QString &fileName);
signals: