diff --git a/kmymoney/converter/mymoneystatementreader.cpp b/kmymoney/converter/mymoneystatementreader.cpp
index 34a721a50..86af7f472 100644
--- a/kmymoney/converter/mymoneystatementreader.cpp
+++ b/kmymoney/converter/mymoneystatementreader.cpp
@@ -1,1578 +1,1577 @@
/***************************************************************************
mymoneystatementreader.cpp
-------------------
begin : Mon Aug 30 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 "mymoneystatementreader.h"
#include
// ----------------------------------------------------------------------------
// QT Headers
#include
#include
#include
#include
#include
#include
// ----------------------------------------------------------------------------
// KDE Headers
#include
#include
#include
#include
#include
#include
// ----------------------------------------------------------------------------
// Project Headers
#include "mymoneyfile.h"
#include "mymoneyaccount.h"
#include "mymoneyprice.h"
#include "mymoneyexception.h"
#include "mymoneytransactionfilter.h"
#include "mymoneypayee.h"
#include "mymoneystatement.h"
#include "mymoneysecurity.h"
#include "kmymoneysettings.h"
#include "transactioneditor.h"
#include "stdtransactioneditor.h"
#include "kmymoneyedit.h"
-#include "kmymoneysettings.h"
#include "kaccountselectdlg.h"
#include "knewaccountwizard.h"
#include "transactionmatcher.h"
#include "kenterscheduledlg.h"
#include "kmymoneyaccountcombo.h"
#include "accountsmodel.h"
#include "models.h"
#include "existingtransactionmatchfinder.h"
#include "scheduledtransactionmatchfinder.h"
#include "dialogenums.h"
#include "mymoneyenums.h"
#include "modelenums.h"
#include "kmymoneyutils.h"
using namespace eMyMoney;
bool matchNotEmpty(const QString &l, const QString &r)
{
return !l.isEmpty() && QString::compare(l, r, Qt::CaseInsensitive) == 0;
}
class MyMoneyStatementReader::Private
{
public:
Private() :
transactionsCount(0),
transactionsAdded(0),
transactionsMatched(0),
transactionsDuplicate(0),
scannedCategories(false) {}
const QString& feeId(const MyMoneyAccount& invAcc);
const QString& interestId(const MyMoneyAccount& invAcc);
QString interestId(const QString& name);
QString expenseId(const QString& name);
QString feeId(const QString& name);
void assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in);
void setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate);
MyMoneyAccount lastAccount;
MyMoneyAccount m_account;
MyMoneyAccount m_brokerageAccount;
QList transactions;
QList payees;
int transactionsCount;
int transactionsAdded;
int transactionsMatched;
int transactionsDuplicate;
QMap uniqIds;
QMap securitiesBySymbol;
QMap securitiesByName;
bool m_skipCategoryMatching;
void (*m_progressCallback)(int, int, const QString&);
private:
void scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName);
/**
* This method tries to figure out the category to be used for fees and interest
* from previous transactions in the given @a investmentAccount and returns the
* ids of those categories in @a feesId and @a interestId. The last used category
* will be returned.
*/
void previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId);
QString nameToId(const QString&name, MyMoneyAccount& parent);
private:
QString m_feeId;
QString m_interestId;
bool scannedCategories;
};
const QString& MyMoneyStatementReader::Private::feeId(const MyMoneyAccount& invAcc)
{
scanCategories(m_feeId, invAcc, MyMoneyFile::instance()->expense(), i18n("_Fees"));
return m_feeId;
}
const QString& MyMoneyStatementReader::Private::interestId(const MyMoneyAccount& invAcc)
{
scanCategories(m_interestId, invAcc, MyMoneyFile::instance()->income(), i18n("_Dividend"));
return m_interestId;
}
QString MyMoneyStatementReader::Private::nameToId(const QString& name, MyMoneyAccount& parent)
{
// Adapted from KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal)
// Needed to find/create category:sub-categories
MyMoneyFile* file = MyMoneyFile::instance();
QString id = file->categoryToAccount(name, Account::Type::Unknown);
// if it does not exist, we have to create it
if (id.isEmpty()) {
MyMoneyAccount newAccount;
MyMoneyAccount parentAccount = parent;
newAccount.setName(name) ;
int pos;
// check for ':' in the name and use it as separator for a hierarchy
while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) {
QString part = newAccount.name().left(pos);
QString remainder = newAccount.name().mid(pos + 1);
const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, part);
if (existingAccount.id().isEmpty()) {
newAccount.setName(part);
newAccount.setAccountType(parentAccount.accountType());
file->addAccount(newAccount, parentAccount);
parentAccount = newAccount;
} else {
parentAccount = existingAccount;
}
newAccount.setParentAccountId(QString()); // make sure, there's no parent
newAccount.clearId(); // and no id set for adding
newAccount.removeAccountIds(); // and no sub-account ids
newAccount.setName(remainder);
}//end while
newAccount.setAccountType(parentAccount.accountType());
// make sure we have a currency. If none is assigned, we assume base currency
if (newAccount.currencyId().isEmpty())
newAccount.setCurrencyId(file->baseCurrency().id());
file->addAccount(newAccount, parentAccount);
id = newAccount.id();
}
return id;
}
QString MyMoneyStatementReader::Private::expenseId(const QString& name)
{
MyMoneyAccount parent = MyMoneyFile::instance()->expense();
return nameToId(name, parent);
}
QString MyMoneyStatementReader::Private::interestId(const QString& name)
{
MyMoneyAccount parent = MyMoneyFile::instance()->income();
return nameToId(name, parent);
}
QString MyMoneyStatementReader::Private::feeId(const QString& name)
{
MyMoneyAccount parent = MyMoneyFile::instance()->expense();
return nameToId(name, parent);
}
void MyMoneyStatementReader::Private::previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId)
{
feesId.clear();
interestId.clear();
MyMoneyFile* file = MyMoneyFile::instance();
try {
MyMoneyAccount acc = file->account(investmentAccount);
MyMoneyTransactionFilter filter(investmentAccount);
filter.setReportAllSplits(false);
// since we assume an investment account here, we need to collect the stock accounts as well
filter.addAccount(acc.accountList());
QList< QPair > list;
file->transactionList(list, filter);
QList< QPair >::const_iterator it_t;
for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) {
const MyMoneyTransaction& t = (*it_t).first;
MyMoneySplit s = (*it_t).second;
MyMoneyAccount acc = file->account(s.accountId());
// stock split shouldn't be fee or interest bacause it won't play nice with dissectTransaction
// it was caused by processTransactionEntry adding splits in wrong order != with manual transaction entering
if (acc.accountGroup() == Account::Type::Expense || acc.accountGroup() == Account::Type::Income) {
foreach (auto sNew , t.splits()) {
acc = file->account(sNew.accountId());
if (acc.accountGroup() != Account::Type::Expense && // shouldn't be fee
acc.accountGroup() != Account::Type::Income && // shouldn't be interest
(sNew.value() != sNew.shares() || // shouldn't be checking account...
(sNew.value() == sNew.shares() && sNew.price() != MyMoneyMoney::ONE))) { // ...but sometimes it may look like checking account
s = sNew;
break;
}
}
}
MyMoneySplit assetAccountSplit;
QList feeSplits;
QList interestSplits;
MyMoneySecurity security;
MyMoneySecurity currency;
eMyMoney::Split::InvestmentTransactionType transactionType;
KMyMoneyUtils::dissectTransaction(t, s, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType);
if (!feeSplits.isEmpty()) {
feesId = feeSplits.first().accountId();
if (!interestId.isEmpty())
break;
}
if (!interestSplits.isEmpty()) {
interestId = interestSplits.first().accountId();
if (!feesId.isEmpty())
break;
}
}
} catch (const MyMoneyException &) {
}
}
void MyMoneyStatementReader::Private::scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName)
{
if (!scannedCategories) {
previouslyUsedCategories(invAcc.id(), m_feeId, m_interestId);
scannedCategories = true;
}
if (id.isEmpty()) {
MyMoneyFile* file = MyMoneyFile::instance();
MyMoneyAccount acc = file->accountByName(defaultName);
// if it does not exist, we have to create it
if (acc.id().isEmpty()) {
MyMoneyAccount parent = parentAccount;
acc.setName(defaultName);
acc.setAccountType(parent.accountType());
acc.setCurrencyId(parent.currencyId());
file->addAccount(acc, parent);
}
id = acc.id();
}
}
void MyMoneyStatementReader::Private::assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in)
{
if (! t_in.m_strBankID.isEmpty()) {
// make sure that id's are unique from this point on by appending a -#
// postfix if needed
QString base(t_in.m_strBankID);
QString hash(base);
int idx = 1;
for (;;) {
QMap::const_iterator it;
it = uniqIds.constFind(hash);
if (it == uniqIds.constEnd()) {
uniqIds[hash] = true;
break;
}
hash = QString("%1-%2").arg(base).arg(idx);
++idx;
}
s.setBankID(hash);
}
}
void MyMoneyStatementReader::Private::setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate)
{
if (transactionAccount.currencyId() != splitAccount.currencyId()) {
// a currency converstion is needed assume that split has already a proper value
MyMoneyFile* file = MyMoneyFile::instance();
MyMoneySecurity toCurrency = file->security(splitAccount.currencyId());
MyMoneySecurity fromCurrency = file->security(transactionAccount.currencyId());
// get the price for the transaction's date
const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), postDate);
// if the price is valid calculate the shares
if (price.isValid()) {
const int fract = splitAccount.fraction(toCurrency);
const MyMoneyMoney &shares = s.value() * price.rate(toCurrency.id());
s.setShares(shares.convert(fract));
qDebug("Setting second split shares to %s", qPrintable(s.shares().formatMoney(toCurrency.id(), 2)));
} else {
qDebug("No price entry was found to convert from '%s' to '%s' on '%s'",
qPrintable(fromCurrency.tradingSymbol()), qPrintable(toCurrency.tradingSymbol()), qPrintable(postDate.toString(Qt::ISODate)));
}
}
}
MyMoneyStatementReader::MyMoneyStatementReader() :
d(new Private),
m_userAbort(false),
m_autoCreatePayee(false),
m_ft(0),
m_progressCallback(0)
{
m_askPayeeCategory = KMyMoneySettings::askForPayeeCategory();
}
MyMoneyStatementReader::~MyMoneyStatementReader()
{
delete d;
}
bool MyMoneyStatementReader::anyTransactionAdded() const
{
return (d->transactionsAdded != 0) ? true : false;
}
void MyMoneyStatementReader::setAutoCreatePayee(bool create)
{
m_autoCreatePayee = create;
}
void MyMoneyStatementReader::setAskPayeeCategory(bool ask)
{
m_askPayeeCategory = ask;
}
QStringList MyMoneyStatementReader::importStatement(const QString& url, bool silent, void(*callback)(int, int, const QString&))
{
QStringList summary;
MyMoneyStatement s;
if (MyMoneyStatement::readXMLFile(s, url))
summary = MyMoneyStatementReader::importStatement(s, silent, callback);
else
KMessageBox::error(nullptr, i18n("Error importing %1: This file is not a valid KMM statement file.", url), i18n("Invalid Statement"));
return summary;
}
QStringList MyMoneyStatementReader::importStatement(const MyMoneyStatement& s, bool silent, void(*callback)(int, int, const QString&))
{
auto result = false;
// keep a copy of the statement
if (KMyMoneySettings::logImportedStatements()) {
auto logFile = QString::fromLatin1("%1/kmm-statement-%2.txt").arg(KMyMoneySettings::logPath(),
QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyy-MM-dd hh-mm-ss")));
MyMoneyStatement::writeXMLFile(s, logFile);
}
// we use an object on the heap here, so that we can check the presence
// of it during slotUpdateActions() by looking at the pointer.
auto reader = new MyMoneyStatementReader;
reader->setAutoCreatePayee(true);
if (callback)
reader->setProgressCallback(callback);
// disable all standard widgets during the import
// setEnabled(false);
QStringList messages;
result = reader->import(s, messages);
auto transactionAdded = reader->anyTransactionAdded();
// get rid of the statement reader and tell everyone else
// about the destruction by setting the pointer to zero
delete reader;
if (callback)
callback(-1, -1, QString());
// re-enable all standard widgets
// setEnabled(true);
if (!silent && transactionAdded)
KMessageBox::informationList(nullptr,
i18n("The statement has been processed with the following results:"), messages, i18n("Statement stats"));
if (!result)
messages.clear();
return messages;
}
bool MyMoneyStatementReader::import(const MyMoneyStatement& s, QStringList& messages)
{
//
// For testing, save the statement to an XML file
// (uncomment this line)
//
//MyMoneyStatement::writeXMLFile(s, "Imported.Xml");
//
// Select the account
//
d->m_account = MyMoneyAccount();
d->m_brokerageAccount = MyMoneyAccount();
m_ft = new MyMoneyFileTransaction();
d->m_skipCategoryMatching = s.m_skipCategoryMatching;
// if the statement source left some information about
// the account, we use it to get the current data of it
if (!s.m_accountId.isEmpty()) {
try {
d->m_account = MyMoneyFile::instance()->account(s.m_accountId);
} catch (const MyMoneyException &) {
qDebug("Received reference '%s' to unknown account in statement", qPrintable(s.m_accountId));
}
}
if (d->m_account.id().isEmpty()) {
d->m_account.setName(s.m_strAccountName);
d->m_account.setNumber(s.m_strAccountNumber);
switch (s.m_eType) {
case eMyMoney::Statement::Type::Checkings:
d->m_account.setAccountType(Account::Type::Checkings);
break;
case eMyMoney::Statement::Type::Savings:
d->m_account.setAccountType(Account::Type::Savings);
break;
case eMyMoney::Statement::Type::Investment:
//testing support for investment statements!
//m_userAbort = true;
//KMessageBox::error(kmymoney, i18n("This is an investment statement. These are not supported currently."), i18n("Critical Error"));
d->m_account.setAccountType(Account::Type::Investment);
break;
case eMyMoney::Statement::Type::CreditCard:
d->m_account.setAccountType(Account::Type::CreditCard);
break;
default:
d->m_account.setAccountType(Account::Type::Unknown);
break;
}
// we ask the user only if we have some transactions to process
if (!m_userAbort && s.m_listTransactions.count() > 0)
m_userAbort = ! selectOrCreateAccount(Select, d->m_account);
}
// see if we need to update some values stored with the account
if (d->m_account.value("lastStatementBalance") != s.m_closingBalance.toString()
|| d->m_account.value("lastImportedTransactionDate") != s.m_dateEnd.toString(Qt::ISODate)) {
if (s.m_closingBalance != MyMoneyMoney::autoCalc) {
d->m_account.setValue("lastStatementBalance", s.m_closingBalance.toString());
if (s.m_dateEnd.isValid()) {
d->m_account.setValue("lastImportedTransactionDate", s.m_dateEnd.toString(Qt::ISODate));
}
}
try {
MyMoneyFile::instance()->modifyAccount(d->m_account);
} catch (const MyMoneyException &) {
qDebug("Updating account in MyMoneyStatementReader::startImport failed");
}
}
if (!d->m_account.name().isEmpty())
messages += i18n("Importing statement for account %1", d->m_account.name());
else if (s.m_listTransactions.count() == 0)
messages += i18n("Importing statement without transactions");
qDebug("Importing statement for '%s'", qPrintable(d->m_account.name()));
//
// Process the securities
//
signalProgress(0, s.m_listSecurities.count(), "Importing Statement ...");
int progress = 0;
QList::const_iterator it_s = s.m_listSecurities.begin();
while (it_s != s.m_listSecurities.end()) {
processSecurityEntry(*it_s);
signalProgress(++progress, 0);
++it_s;
}
signalProgress(-1, -1);
//
// Process the transactions
//
if (!m_userAbort) {
try {
qDebug("Processing transactions (%s)", qPrintable(d->m_account.name()));
signalProgress(0, s.m_listTransactions.count(), "Importing Statement ...");
int progress = 0;
QList::const_iterator it_t = s.m_listTransactions.begin();
while (it_t != s.m_listTransactions.end() && !m_userAbort) {
processTransactionEntry(*it_t);
signalProgress(++progress, 0);
++it_t;
}
qDebug("Processing transactions done (%s)", qPrintable(d->m_account.name()));
} catch (const MyMoneyException &e) {
if (e.what() == "USERABORT")
m_userAbort = true;
else
qDebug("Caught exception from processTransactionEntry() not caused by USERABORT: %s", qPrintable(e.what()));
}
signalProgress(-1, -1);
}
//
// process price entries
//
if (!m_userAbort) {
try {
signalProgress(0, s.m_listPrices.count(), "Importing Statement ...");
KMyMoneyUtils::processPriceList(s);
} catch (const MyMoneyException &e) {
if (e.what() == "USERABORT")
m_userAbort = true;
else
qDebug("Caught exception from processPriceEntry() not caused by USERABORT: %s", qPrintable(e.what()));
}
signalProgress(-1, -1);
}
bool rc = false;
// delete all payees created in vain
int payeeCount = d->payees.count();
QList::const_iterator it_p;
for (it_p = d->payees.constBegin(); it_p != d->payees.constEnd(); ++it_p) {
try {
MyMoneyFile::instance()->removePayee(*it_p);
--payeeCount;
} catch (const MyMoneyException &) {
// if we can't delete it, it must be in use which is ok for us
}
}
if (s.m_closingBalance.isAutoCalc()) {
messages += i18n(" Statement balance is not contained in statement.");
} else {
messages += i18n(" Statement balance on %1 is reported to be %2", s.m_dateEnd.toString(Qt::ISODate), s.m_closingBalance.formatMoney("", 2));
}
messages += i18n(" Transactions");
messages += i18np(" %1 processed", " %1 processed", d->transactionsCount);
messages += i18ncp("x transactions have been added", " %1 added", " %1 added", d->transactionsAdded);
messages += i18np(" %1 matched", " %1 matched", d->transactionsMatched);
messages += i18np(" %1 duplicate", " %1 duplicates", d->transactionsDuplicate);
messages += i18n(" Payees");
messages += i18ncp("x transactions have been created", " %1 created", " %1 created", payeeCount);
messages += QString();
// remove the Don't ask again entries
KSharedConfigPtr config = KSharedConfig::openConfig();
KConfigGroup grp = config->group(QString::fromLatin1("Notification Messages"));
QStringList::ConstIterator it;
for (it = m_dontAskAgain.constBegin(); it != m_dontAskAgain.constEnd(); ++it) {
grp.deleteEntry(*it);
}
config->sync();
m_dontAskAgain.clear();
rc = !m_userAbort;
// finish the transaction
if (rc)
m_ft->commit();
delete m_ft;
m_ft = 0;
qDebug("Importing statement for '%s' done", qPrintable(d->m_account.name()));
return rc;
}
void MyMoneyStatementReader::processSecurityEntry(const MyMoneyStatement::Security& sec_in)
{
// For a security entry, we will just make sure the security exists in the
// file. It will not get added to the investment account until it's called
// for in a transaction.
MyMoneyFile* file = MyMoneyFile::instance();
// check if we already have the security
// In a statement, we do not know what type of security this is, so we will
// not use type as a matching factor.
MyMoneySecurity security;
QList list = file->securityList();
QList::ConstIterator it = list.constBegin();
while (it != list.constEnd() && security.id().isEmpty()) {
if (matchNotEmpty(sec_in.m_strSymbol, (*it).tradingSymbol()) ||
matchNotEmpty(sec_in.m_strName, (*it).name())) {
security = *it;
}
++it;
}
// if the security was not found, we have to create it while not forgetting
// to setup the type
if (security.id().isEmpty()) {
security.setName(sec_in.m_strName);
security.setTradingSymbol(sec_in.m_strSymbol);
security.setTradingCurrency(file->baseCurrency().id());
security.setValue("kmm-security-id", sec_in.m_strId);
security.setValue("kmm-online-source", "Stooq");
security.setSecurityType(Security::Type::Stock);
MyMoneyFileTransaction ft;
try {
file->addSecurity(security);
ft.commit();
qDebug() << "Created " << security.name() << " with id " << security.id();
} catch (const MyMoneyException &e) {
KMessageBox::error(0, i18n("Error creating security record: %1", e.what()), i18n("Error"));
}
} else {
qDebug() << "Found " << security.name() << " with id " << security.id();
}
}
void MyMoneyStatementReader::processTransactionEntry(const MyMoneyStatement::Transaction& statementTransactionUnderImport)
{
MyMoneyFile* file = MyMoneyFile::instance();
MyMoneyTransaction transactionUnderImport;
QString dbgMsg;
dbgMsg = QString("Process on: '%1', id: '%3', amount: '%2', fees: '%4'")
.arg(statementTransactionUnderImport.m_datePosted.toString(Qt::ISODate))
.arg(statementTransactionUnderImport.m_amount.formatMoney("", 2))
.arg(statementTransactionUnderImport.m_strBankID)
.arg(statementTransactionUnderImport.m_fees.formatMoney("", 2));
qDebug("%s", qPrintable(dbgMsg));
// mark it imported for the view
transactionUnderImport.setImported();
// TODO (Ace) We can get the commodity from the statement!!
// Although then we would need UI to verify
transactionUnderImport.setCommodity(d->m_account.currencyId());
transactionUnderImport.setPostDate(statementTransactionUnderImport.m_datePosted);
transactionUnderImport.setMemo(statementTransactionUnderImport.m_strMemo);
MyMoneySplit s1;
MyMoneySplit s2;
MyMoneySplit sFees;
MyMoneySplit sBrokerage;
s1.setMemo(statementTransactionUnderImport.m_strMemo);
s1.setValue(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees);
s1.setShares(s1.value());
s1.setNumber(statementTransactionUnderImport.m_strNumber);
// set these values if a transfer split is needed at the very end.
MyMoneyMoney transfervalue;
// If the user has chosen to import into an investment account, determine the correct account to use
MyMoneyAccount thisaccount = d->m_account;
QString brokerageactid;
if (thisaccount.accountType() == Account::Type::Investment) {
// determine the brokerage account
brokerageactid = d->m_account.value("kmm-brokerage-account").toUtf8();
if (brokerageactid.isEmpty()) {
brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id();
}
if (brokerageactid.isEmpty()) {
brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount);
}
if (brokerageactid.isEmpty()) {
brokerageactid = file->nameToAccount(thisaccount.brokerageName());
}
if (brokerageactid.isEmpty()) {
brokerageactid = SelectBrokerageAccount();
}
// find the security transacted, UNLESS this transaction didn't
// involve any security.
if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::None)
// eaInterest transactions MAY have a security.
// && (t_in.m_eAction != MyMoneyStatement::Transaction::eaInterest)
&& (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Fees)) {
// the correct account is the stock account which matches two criteria:
// (1) it is a sub-account of the selected investment account, and
// (2a) the symbol of the underlying security matches the security of the
// transaction, or
// (2b) the name of the security matches the name of the security of the transaction.
// search through each subordinate account
auto found = false;
QString currencyid;
foreach (const auto sAccount, thisaccount.accountList()) {
currencyid = file->account(sAccount).currencyId();
auto security = file->security(currencyid);
if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, security.tradingSymbol()) ||
matchNotEmpty(statementTransactionUnderImport.m_strSecurity, security.name())) {
thisaccount = file->account(sAccount);
found = true;
break;
}
}
// If there was no stock account under the m_acccount investment account,
// add one using the security.
if (!found) {
// The security should always be available, because the statement file
// should separately list all the securities referred to in the file,
// and when we found a security, we added it to the file.
if (statementTransactionUnderImport.m_strSecurity.isEmpty()) {
KMessageBox::information(0, i18n("This imported statement contains investment transactions with no security. These transactions will be ignored."), i18n("Security not found"), QString("BlankSecurity"));
return;
} else {
MyMoneySecurity security;
QList list = MyMoneyFile::instance()->securityList();
QList::ConstIterator it = list.constBegin();
while (it != list.constEnd() && security.id().isEmpty()) {
if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, (*it).tradingSymbol()) ||
matchNotEmpty(statementTransactionUnderImport.m_strSecurity, (*it).name())) {
security = *it;
}
++it;
}
if (!security.id().isEmpty()) {
thisaccount = MyMoneyAccount();
thisaccount.setName(security.name());
thisaccount.setAccountType(Account::Type::Stock);
thisaccount.setCurrencyId(security.id());
currencyid = thisaccount.currencyId();
file->addAccount(thisaccount, d->m_account);
qDebug() << Q_FUNC_INFO << ": created account " << thisaccount.id() << " for security " << statementTransactionUnderImport.m_strSecurity << " under account " << d->m_account.id();
}
// this security does not exist in the file.
else {
// This should be rare. A statement should have a security entry for any
// of the securities referred to in the transactions. The only way to get
// here is if that's NOT the case.
int ret = KMessageBox::warningContinueCancel(0, i18n("This investment account does not contain the \"%1\" security."
"Transactions involving this security will be ignored.", statementTransactionUnderImport.m_strSecurity),
i18n("Security not found"), KStandardGuiItem::cont(), KStandardGuiItem::cancel());
if (ret == KMessageBox::Cancel) {
m_userAbort = true;
}
return;
}
}
}
// Don't update price if there is no price information contained in the transaction
if (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend
&& statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsin
&& statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsout) {
// update the price, while we're here. in the future, this should be
// an option
QString basecurrencyid = file->baseCurrency().id();
const MyMoneyPrice &price = file->price(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, true);
if (!price.isValid() && ((!statementTransactionUnderImport.m_amount.isZero() && !statementTransactionUnderImport.m_shares.isZero()) || !statementTransactionUnderImport.m_price.isZero())) {
MyMoneyPrice newprice;
if (!statementTransactionUnderImport.m_price.isZero()) {
newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted,
statementTransactionUnderImport.m_price.abs(), i18n("Statement Importer"));
} else {
newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted,
(statementTransactionUnderImport.m_amount / statementTransactionUnderImport.m_shares).abs(), i18n("Statement Importer"));
}
file->addPrice(newprice);
}
}
}
s1.setAccountId(thisaccount.id());
d->assignUniqueBankID(s1, statementTransactionUnderImport);
if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::ReinvestDividend) {
s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend));
s1.setShares(statementTransactionUnderImport.m_shares);
if (!statementTransactionUnderImport.m_price.isZero()) {
s1.setPrice(statementTransactionUnderImport.m_price);
} else {
if (statementTransactionUnderImport.m_shares.isZero()) {
KMessageBox::information(0, i18n("This imported statement contains investment transactions with no share amount. These transactions will be ignored."), i18n("No share amount provided"), QString("BlankAmount"));
return;
}
MyMoneyMoney total = -statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees;
s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).convertPrecision(file->security(thisaccount.currencyId()).pricePrecision())));
}
s2.setMemo(statementTransactionUnderImport.m_strMemo);
if (statementTransactionUnderImport.m_strInterestCategory.isEmpty())
s2.setAccountId(d->interestId(thisaccount));
else
s2.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory));
s2.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees);
s2.setValue(s2.shares());
} else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::CashDividend) {
// Cash dividends require setting 2 splits to get all of the information
// in. Split #1 will be the income split, and we'll set it to the first
// income account. This is a hack, but it's needed in order to get the
// amount into the transaction.
if (statementTransactionUnderImport.m_strInterestCategory.isEmpty())
s1.setAccountId(d->interestId(thisaccount));
else {// Ensure category sub-accounts are dealt with properly
s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory));
}
s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees);
s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees);
// Split 2 will be the zero-amount investment split that serves to
// mark this transaction as a cash dividend and note which stock account
// it belongs to.
s2.setMemo(statementTransactionUnderImport.m_strMemo);
s2.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend));
s2.setAccountId(thisaccount.id());
/* at this point any fees have been taken into account already
* so don't deduct them again.
* BUG 322381
*/
transfervalue = statementTransactionUnderImport.m_amount;
} else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Interest) {
if (statementTransactionUnderImport.m_strInterestCategory.isEmpty())
s1.setAccountId(d->interestId(thisaccount));
else {// Ensure category sub-accounts are dealt with properly
if (statementTransactionUnderImport.m_amount.isPositive())
s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory));
else
s1.setAccountId(d->expenseId(statementTransactionUnderImport.m_strInterestCategory));
}
s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees);
s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees);
/// *********** Add split as per Div **********
// Split 2 will be the zero-amount investment split that serves to
// mark this transaction as a cash dividend and note which stock account
// it belongs to.
s2.setMemo(statementTransactionUnderImport.m_strMemo);
s2.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome));
s2.setAccountId(thisaccount.id());
transfervalue = statementTransactionUnderImport.m_amount;
} else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Fees) {
if (statementTransactionUnderImport.m_strInterestCategory.isEmpty())
s1.setAccountId(d->feeId(thisaccount));
else// Ensure category sub-accounts are dealt with properly
s1.setAccountId(d->feeId(statementTransactionUnderImport.m_strInterestCategory));
s1.setShares(statementTransactionUnderImport.m_amount);
s1.setValue(statementTransactionUnderImport.m_amount);
transfervalue = statementTransactionUnderImport.m_amount;
} else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy) ||
(statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Sell)) {
s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares));
if (!statementTransactionUnderImport.m_price.isZero()) {
s1.setPrice(statementTransactionUnderImport.m_price.abs());
} else if (!statementTransactionUnderImport.m_shares.isZero()) {
MyMoneyMoney total = statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs();
s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).abs().convertPrecision(file->security(thisaccount.currencyId()).pricePrecision())));
}
if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy)
s1.setShares(statementTransactionUnderImport.m_shares.abs());
else
s1.setShares(-statementTransactionUnderImport.m_shares.abs());
s1.setValue(-(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs()));
transfervalue = statementTransactionUnderImport.m_amount;
} else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsin) ||
(statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsout)) {
s1.setValue(MyMoneyMoney());
s1.setShares(statementTransactionUnderImport.m_shares);
s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares));
} else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::None) {
// User is attempting to import a non-investment transaction into this
// investment account. This is not supportable the way KMyMoney is
// written. However, if a user has an associated brokerage account,
// we can stuff the transaction there.
QString brokerageactid = d->m_account.value("kmm-brokerage-account").toUtf8();
if (brokerageactid.isEmpty()) {
brokerageactid = file->accountByName(d->m_account.brokerageName()).id();
}
if (! brokerageactid.isEmpty()) {
s1.setAccountId(brokerageactid);
d->assignUniqueBankID(s1, statementTransactionUnderImport);
// Needed to satisfy the bankid check below.
thisaccount = file->account(brokerageactid);
} else {
// Warning!! Your transaction is being thrown away.
}
}
if (!statementTransactionUnderImport.m_fees.isZero()) {
sFees.setMemo(i18n("(Fees) %1", statementTransactionUnderImport.m_strMemo));
sFees.setValue(statementTransactionUnderImport.m_fees);
sFees.setShares(statementTransactionUnderImport.m_fees);
sFees.setAccountId(d->feeId(thisaccount));
}
} else {
// For non-investment accounts, just use the selected account
// Note that it is perfectly reasonable to import an investment statement into a non-investment account
// if you really want. The investment-specific information, such as number of shares and action will
// be discarded in that case.
s1.setAccountId(d->m_account.id());
d->assignUniqueBankID(s1, statementTransactionUnderImport);
}
QString payeename = statementTransactionUnderImport.m_strPayee;
if (!payeename.isEmpty()) {
qDebug() << QLatin1String("Start matching payee") << payeename;
QString payeeid;
try {
QList pList = file->payeeList();
QList::const_iterator it_p;
QMap matchMap;
for (it_p = pList.constBegin(); it_p != pList.constEnd(); ++it_p) {
bool ignoreCase;
QStringList keys;
QStringList::const_iterator it_s;
const MyMoneyPayee::payeeMatchType matchType = (*it_p).matchData(ignoreCase, keys);
switch (matchType) {
case MyMoneyPayee::matchDisabled:
break;
case MyMoneyPayee::matchName:
case MyMoneyPayee::matchNameExact:
keys << QString("%1").arg(QRegExp::escape((*it_p).name()));
if(matchType == MyMoneyPayee::matchNameExact) {
keys.clear();
keys << QString("^%1$").arg(QRegExp::escape((*it_p).name()));
}
// intentional fall through
case MyMoneyPayee::matchKey:
for (it_s = keys.constBegin(); it_s != keys.constEnd(); ++it_s) {
QRegExp exp(*it_s, ignoreCase ? Qt::CaseInsensitive : Qt::CaseSensitive);
if (exp.indexIn(payeename) != -1) {
qDebug("Found match with '%s' on '%s'", qPrintable(payeename), qPrintable((*it_p).name()));
matchMap[exp.matchedLength()] = (*it_p).id();
}
}
break;
}
}
// at this point we can have several scenarios:
// a) multiple matches
// b) a single match
// c) no match at all
//
// for c) we just do nothing, for b) we take the one we found
// in case of a) we take the one with the largest matchedLength()
// which happens to be the last one in the map
if (matchMap.count() > 1) {
qDebug("Multiple matches");
QMap::const_iterator it_m = matchMap.constEnd();
--it_m;
payeeid = *it_m;
} else if (matchMap.count() == 1) {
qDebug("Single matches");
payeeid = *(matchMap.constBegin());
}
// if we did not find a matching payee, we throw an exception and try to create it
if (payeeid.isEmpty())
throw MYMONEYEXCEPTION("payee not matched");
s1.setPayeeId(payeeid);
} catch (const MyMoneyException &) {
MyMoneyPayee payee;
int rc = KMessageBox::Yes;
if (m_autoCreatePayee == false) {
// Ask the user if that is what he intended to do?
QString msg = i18n("Do you want to add \"%1\" as payee/receiver?\n\n", payeename);
msg += i18n("Selecting \"Yes\" will create the payee, \"No\" will skip "
"creation of a payee record and remove the payee information "
"from this transaction. Selecting \"Cancel\" aborts the import "
"operation.\n\nIf you select \"No\" here and mark the \"Do not ask "
"again\" checkbox, the payee information for all following transactions "
"referencing \"%1\" will be removed.", payeename);
QString askKey = QString("Statement-Import-Payee-") + payeename;
if (!m_dontAskAgain.contains(askKey)) {
m_dontAskAgain += askKey;
}
rc = KMessageBox::questionYesNoCancel(0, msg, i18n("New payee/receiver"),
KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), askKey);
}
if (rc == KMessageBox::Yes) {
// for now, we just add the payee to the pool and turn
// on simple name matching, so that future transactions
// with the same name don't get here again.
//
// In the future, we could open a dialog and ask for
// all the other attributes of the payee, but since this
// is called in the context of an automatic procedure it
// might distract the user.
payee.setName(payeename);
payee.setMatchData(MyMoneyPayee::matchKey, true, QStringList() << QString("^%1$").arg(QRegExp::escape(payeename)));
if (m_askPayeeCategory) {
// We use a QPointer because the dialog may get deleted
// during exec() if the parent of the dialog gets deleted.
// In that case the guarded ptr will reset to 0.
QPointer dialog = new QDialog;
dialog->setWindowTitle(i18n("Default Category for Payee"));
dialog->setModal(true);
QWidget *mainWidget = new QWidget;
QVBoxLayout *topcontents = new QVBoxLayout(mainWidget);
//add in caption? and account combo here
QLabel *label1 = new QLabel(i18n("Please select a default category for payee '%1'", payeename));
topcontents->addWidget(label1);
auto filterProxyModel = new AccountNamesFilterProxyModel(this);
filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode());
filterProxyModel->addAccountGroup(QVector {Account::Type::Asset, Account::Type::Liability, Account::Type::Equity, Account::Type::Income, Account::Type::Expense});
auto const model = Models::instance()->accountsModel();
filterProxyModel->setSourceModel(model);
filterProxyModel->setSourceColumns(model->getColumns());
filterProxyModel->sort((int)eAccountsModel::Column::Account);
QPointer accountCombo = new KMyMoneyAccountCombo(filterProxyModel);
topcontents->addWidget(accountCombo);
mainWidget->setLayout(topcontents);
QVBoxLayout *mainLayout = new QVBoxLayout;
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel|QDialogButtonBox::No|QDialogButtonBox::Yes);
dialog->setLayout(mainLayout);
mainLayout->addWidget(mainWidget);
dialog->connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept()));
dialog->connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject()));
mainLayout->addWidget(buttonBox);
KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Save Category")));
KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KGuiItem(i18n("No Category")));
KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KGuiItem(i18n("Abort")));
int result = dialog->exec();
QString accountId;
if (accountCombo && !accountCombo->getSelected().isEmpty()) {
accountId = accountCombo->getSelected();
}
delete dialog;
//if they hit yes instead of no, then grab setting of account combo
if (result == QDialog::Accepted) {
payee.setDefaultAccountId(accountId);
} else if (result != QDialog::Rejected) {
//add cancel button? and throw exception like below
throw MYMONEYEXCEPTION("USERABORT");
}
}
try {
file->addPayee(payee);
qDebug("Payee '%s' created", qPrintable(payee.name()));
d->payees << payee;
payeeid = payee.id();
s1.setPayeeId(payeeid);
} catch (const MyMoneyException &e) {
KMessageBox::detailedSorry(0, i18n("Unable to add payee/receiver"),
i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line()));
}
} else if (rc == KMessageBox::No) {
s1.setPayeeId(QString());
} else {
throw MYMONEYEXCEPTION("USERABORT");
}
}
if (thisaccount.accountType() != Account::Type::Stock) {
//
// Fill in other side of the transaction (category/etc) based on payee
//
// Note, this logic is lifted from KLedgerView::slotPayeeChanged(),
// however this case is more complicated, because we have an amount and
// a memo. We just don't have the other side of the transaction.
//
// We'll search for the most recent transaction in this account with
// this payee. If this reference transaction is a simple 2-split
// transaction, it's simple. If it's a complex split, and the amounts
// are different, we have a problem. Somehow we have to balance the
// transaction. For now, we'll leave it unbalanced, and let the user
// handle it.
//
const MyMoneyPayee& payeeObj = MyMoneyFile::instance()->payee(payeeid);
if (statementTransactionUnderImport.m_listSplits.isEmpty() && payeeObj.defaultAccountEnabled()) {
MyMoneyAccount splitAccount = file->account(payeeObj.defaultAccountId());
MyMoneySplit s;
s.setReconcileFlag(eMyMoney::Split::State::Cleared);
s.clearId();
s.setBankID(QString());
s.setShares(-s1.shares());
s.setValue(-s1.value());
s.setAccountId(payeeObj.defaultAccountId());
s.setMemo(transactionUnderImport.memo());
s.setPayeeId(payeeid);
d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted);
transactionUnderImport.addSplit(s);
file->addVATSplit(transactionUnderImport, d->m_account, splitAccount, statementTransactionUnderImport.m_amount);
} else if (statementTransactionUnderImport.m_listSplits.isEmpty() && !d->m_skipCategoryMatching) {
MyMoneyTransactionFilter filter(thisaccount.id());
filter.addPayee(payeeid);
QList list = file->transactionList(filter);
if (!list.empty()) {
// Default to using the most recent transaction as the reference
MyMoneyTransaction t_old = list.last();
// if there is more than one matching transaction, try to be a little
// smart about which one we take. for now, we'll see if there's one
// with the same VALUE as our imported transaction, and if so take that one.
if (list.count() > 1) {
QList::ConstIterator it_trans = list.constEnd();
if (it_trans != list.constBegin())
--it_trans;
while (it_trans != list.constBegin()) {
MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id());
if (s.value() == s1.value()) {
// keep searching if this transaction references a closed account
if (!MyMoneyFile::instance()->referencesClosedAccount(*it_trans)) {
t_old = *it_trans;
break;
}
}
--it_trans;
}
// check constBegin, just in case
if (it_trans == list.constBegin()) {
MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id());
if (s.value() == s1.value()) {
t_old = *it_trans;
}
}
}
// Only copy the splits if the transaction found does not reference a closed account
if (!MyMoneyFile::instance()->referencesClosedAccount(t_old)) {
foreach (const auto split, t_old.splits()) {
// We don't need the split that covers this account,
// we just need the other ones.
if (split.accountId() != thisaccount.id()) {
MyMoneySplit s(split);
s.setReconcileFlag(eMyMoney::Split::State::NotReconciled);
s.clearId();
s.setBankID(QString());
s.removeMatch();
if (t_old.splits().count() == 2) {
s.setShares(-s1.shares());
s.setValue(-s1.value());
s.setMemo(s1.memo());
}
MyMoneyAccount splitAccount = file->account(s.accountId());
qDebug("Adding second split to %s(%s)",
qPrintable(splitAccount.name()),
qPrintable(s.accountId()));
d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted);
transactionUnderImport.addSplit(s);
}
}
}
}
}
}
}
s1.setReconcileFlag(statementTransactionUnderImport.m_reconcile);
// Add the 'account' split if it's needed
if (! transfervalue.isZero()) {
// in case the transaction has a reference to the brokerage account, we use it
// but if brokerageactid has already been set, keep that.
if (!statementTransactionUnderImport.m_strBrokerageAccount.isEmpty() && brokerageactid.isEmpty()) {
brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount);
}
if (brokerageactid.isEmpty()) {
brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id();
}
// There is no BrokerageAccount so have to nowhere to put this split.
if (!brokerageactid.isEmpty()) {
sBrokerage.setMemo(statementTransactionUnderImport.m_strMemo);
sBrokerage.setValue(transfervalue);
sBrokerage.setShares(transfervalue);
sBrokerage.setAccountId(brokerageactid);
sBrokerage.setReconcileFlag(statementTransactionUnderImport.m_reconcile);
MyMoneyAccount splitAccount = file->account(sBrokerage.accountId());
d->setupPrice(sBrokerage, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted);
}
}
if (!(sBrokerage == MyMoneySplit()))
transactionUnderImport.addSplit(sBrokerage);
if (!(sFees == MyMoneySplit()))
transactionUnderImport.addSplit(sFees);
if (!(s2 == MyMoneySplit()))
transactionUnderImport.addSplit(s2);
transactionUnderImport.addSplit(s1);
if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::ReinvestDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Interest)
) {
//******************************************
// process splits
//******************************************
QList::const_iterator it_s;
for (it_s = statementTransactionUnderImport.m_listSplits.begin(); it_s != statementTransactionUnderImport.m_listSplits.end(); ++it_s) {
MyMoneySplit s3;
s3.setAccountId((*it_s).m_accountId);
MyMoneyAccount acc = file->account(s3.accountId());
s3.setPayeeId(s1.payeeId());
s3.setMemo((*it_s).m_strMemo);
s3.setShares((*it_s).m_amount);
s3.setValue((*it_s).m_amount);
s3.setReconcileFlag((*it_s).m_reconcile);
d->setupPrice(s3, acc, d->m_account, statementTransactionUnderImport.m_datePosted);
transactionUnderImport.addSplit(s3);
}
}
// Add the transaction
try {
// check for matches already stored in the engine
TransactionMatchFinder::MatchResult result;
TransactionMatcher matcher(thisaccount);
d->transactionsCount++;
ExistingTransactionMatchFinder existingTrMatchFinder(KMyMoneySettings::matchInterval());
result = existingTrMatchFinder.findMatch(transactionUnderImport, s1);
if (result != TransactionMatchFinder::MatchNotFound) {
MyMoneyTransaction matchedTransaction = existingTrMatchFinder.getMatchedTransaction();
if (result == TransactionMatchFinder::MatchDuplicate
|| !matchedTransaction.isImported()
|| result == TransactionMatchFinder::MatchPrecise) { // don't match with just imported transaction
MyMoneySplit matchedSplit = existingTrMatchFinder.getMatchedSplit();
handleMatchingOfExistingTransaction(matcher, matchedTransaction, matchedSplit, transactionUnderImport, s1, result);
return;
}
}
addTransaction(transactionUnderImport);
ScheduledTransactionMatchFinder scheduledTrMatchFinder(thisaccount, KMyMoneySettings::matchInterval());
result = scheduledTrMatchFinder.findMatch(transactionUnderImport, s1);
if (result != TransactionMatchFinder::MatchNotFound) {
MyMoneySplit matchedSplit = scheduledTrMatchFinder.getMatchedSplit();
MyMoneySchedule matchedSchedule = scheduledTrMatchFinder.getMatchedSchedule();
handleMatchingOfScheduledTransaction(matcher, matchedSchedule, matchedSplit, transactionUnderImport, s1);
return;
}
} catch (const MyMoneyException &e) {
QString message(i18n("Problem adding or matching imported transaction with id '%1': %2", statementTransactionUnderImport.m_strBankID, e.what()));
qDebug("%s", qPrintable(message));
int result = KMessageBox::warningContinueCancel(0, message);
if (result == KMessageBox::Cancel)
throw MYMONEYEXCEPTION("USERABORT");
}
}
QString MyMoneyStatementReader::SelectBrokerageAccount()
{
if (d->m_brokerageAccount.id().isEmpty()) {
d->m_brokerageAccount.setAccountType(Account::Type::Checkings);
if (!m_userAbort)
m_userAbort = ! selectOrCreateAccount(Select, d->m_brokerageAccount);
}
return d->m_brokerageAccount.id();
}
bool MyMoneyStatementReader::selectOrCreateAccount(const SelectCreateMode /*mode*/, MyMoneyAccount& account)
{
bool result = false;
MyMoneyFile* file = MyMoneyFile::instance();
QString accountId;
// Try to find an existing account in the engine which matches this one.
// There are two ways to be a "matching account". The account number can
// match the statement account OR the "StatementKey" property can match.
// Either way, we'll update the "StatementKey" property for next time.
QString accountNumber = account.number();
if (! accountNumber.isEmpty()) {
// Get a list of all accounts
QList accounts;
file->accountList(accounts);
// Iterate through them
QList::const_iterator it_account = accounts.constBegin();
while (it_account != accounts.constEnd()) {
if (
((*it_account).value("StatementKey") == accountNumber) ||
((*it_account).number() == accountNumber)
) {
MyMoneyAccount newAccount((*it_account).id(), account);
account = newAccount;
accountId = (*it_account).id();
break;
}
++it_account;
}
}
QString msg = i18n("You have downloaded a statement for the following account:
");
msg += i18n(" - Account Name: %1", account.name()) + "
";
msg += i18n(" - Account Type: %1", MyMoneyAccount::accountTypeToString(account.accountType())) + "
";
msg += i18n(" - Account Number: %1", account.number()) + "
";
msg += "
";
if (!account.name().isEmpty()) {
if (!accountId.isEmpty())
msg += i18n("Do you want to import transactions to this account?");
else
msg += i18n("KMyMoney cannot determine which of your accounts to use. You can "
"create a new account by pressing the Create button "
"or select another one manually from the selection box below.");
} else {
msg += i18n("No account information has been found in the selected statement file. "
"Please select an account using the selection box in the dialog or "
"create a new account by pressing the Create button.");
}
eDialogs::Category type;
if (account.accountType() == Account::Type::Checkings) {
type = eDialogs::Category::checking;
} else if (account.accountType() == Account::Type::Savings) {
type = eDialogs::Category::savings;
} else if (account.accountType() == Account::Type::Investment) {
type = eDialogs::Category::investment;
} else if (account.accountType() == Account::Type::CreditCard) {
type = eDialogs::Category::creditCard;
} else {
type = static_cast(eDialogs::Category::asset | eDialogs::Category::liability);
}
QPointer accountSelect = new KAccountSelectDlg(type, "StatementImport", 0);
connect(accountSelect, &KAccountSelectDlg::createAccount, this, &MyMoneyStatementReader::slotNewAccount);
accountSelect->setHeader(i18n("Import transactions"));
accountSelect->setDescription(msg);
accountSelect->setAccount(account, accountId);
accountSelect->setMode(false);
accountSelect->showAbortButton(true);
accountSelect->hideQifEntry();
QString accname;
bool done = false;
while (!done) {
if (accountSelect->exec() == QDialog::Accepted && !accountSelect->selectedAccount().isEmpty()) {
result = true;
done = true;
accountId = accountSelect->selectedAccount();
account = file->account(accountId);
if (! accountNumber.isEmpty() && account.value("StatementKey") != accountNumber) {
account.setValue("StatementKey", accountNumber);
MyMoneyFileTransaction ft;
try {
MyMoneyFile::instance()->modifyAccount(account);
ft.commit();
accname = account.name();
} catch (const MyMoneyException &) {
qDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed");
}
}
} else {
if (accountSelect->aborted())
//throw MYMONEYEXCEPTION("USERABORT");
done = true;
else
KMessageBox::error(0, QLatin1String("") + i18n("You must select an account, create a new one, or press the Abort button.") + QLatin1String(""));
}
}
delete accountSelect;
return result;
}
const MyMoneyAccount& MyMoneyStatementReader::account() const {
return d->m_account;
}
void MyMoneyStatementReader::setProgressCallback(void(*callback)(int, int, const QString&))
{
m_progressCallback = callback;
}
void MyMoneyStatementReader::signalProgress(int current, int total, const QString& msg)
{
if (m_progressCallback != 0)
(*m_progressCallback)(current, total, msg);
}
void MyMoneyStatementReader::handleMatchingOfExistingTransaction(TransactionMatcher & matcher,
MyMoneyTransaction matchedTransaction,
MyMoneySplit matchedSplit,
MyMoneyTransaction & importedTransaction,
const MyMoneySplit & importedSplit,
const TransactionMatchFinder::MatchResult & matchResult)
{
switch (matchResult) {
case TransactionMatchFinder::MatchNotFound:
break;
case TransactionMatchFinder::MatchDuplicate:
d->transactionsDuplicate++;
qDebug("Detected transaction duplicate");
break;
case TransactionMatchFinder::MatchImprecise:
case TransactionMatchFinder::MatchPrecise:
addTransaction(importedTransaction);
qDebug("Detected as match to transaction '%s'", qPrintable(matchedTransaction.id()));
matcher.match(matchedTransaction, matchedSplit, importedTransaction, importedSplit, true);
d->transactionsMatched++;
break;
}
}
void MyMoneyStatementReader::handleMatchingOfScheduledTransaction(TransactionMatcher & matcher,
MyMoneySchedule matchedSchedule,
MyMoneySplit matchedSplit,
const MyMoneyTransaction & importedTransaction,
const MyMoneySplit & importedSplit)
{
QPointer editor;
if (askUserToEnterScheduleForMatching(matchedSchedule, importedSplit, importedTransaction)) {
KEnterScheduleDlg dlg(0, matchedSchedule);
editor = dlg.startEdit();
if (editor) {
MyMoneyTransaction torig;
try {
// in case the amounts of the scheduled transaction and the
// imported transaction differ, we need to update the amount
// using the transaction editor.
if (matchedSplit.shares() != importedSplit.shares() && !matchedSchedule.isFixed()) {
// for now this only works with regular transactions and not
// for investment transactions. As of this, we don't have
// scheduled investment transactions anyway.
auto se = dynamic_cast(editor.data());
if (se) {
// the following call will update the amount field in the
// editor and also adjust a possible VAT assignment. Make
// sure to use only the absolute value of the amount, because
// the editor keeps the sign in a different position (deposit,
// withdrawal tab)
KMyMoneyEdit* amount = dynamic_cast(se->haveWidget("amount"));
if (amount) {
amount->setValue(importedSplit.shares().abs());
se->slotUpdateAmount(importedSplit.shares().abs().toString());
// we also need to update the matchedSplit variable to
// have the modified share/value.
matchedSplit.setShares(importedSplit.shares());
matchedSplit.setValue(importedSplit.value());
}
}
}
editor->createTransaction(torig, dlg.transaction(), dlg.transaction().splits().isEmpty() ? MyMoneySplit() : dlg.transaction().splits().front(), true);
QString newId;
if (editor->enterTransactions(newId, false, true)) {
if (!newId.isEmpty()) {
torig = MyMoneyFile::instance()->transaction(newId);
matchedSchedule.setLastPayment(torig.postDate());
}
matchedSchedule.setNextDueDate(matchedSchedule.nextPayment(matchedSchedule.nextDueDate()));
MyMoneyFile::instance()->modifySchedule(matchedSchedule);
}
// now match the two transactions
matcher.match(torig, matchedSplit, importedTransaction, importedSplit);
d->transactionsMatched++;
} catch (const MyMoneyException &e) {
// make sure we get rid of the editor before
// the KEnterScheduleDlg is destroyed
delete editor;
throw e; // rethrow
}
}
// delete the editor
delete editor;
}
}
void MyMoneyStatementReader::addTransaction(MyMoneyTransaction& transaction)
{
MyMoneyFile* file = MyMoneyFile::instance();
file->addTransaction(transaction);
d->transactionsAdded++;
}
bool MyMoneyStatementReader::askUserToEnterScheduleForMatching(const MyMoneySchedule& matchedSchedule, const MyMoneySplit& importedSplit, const MyMoneyTransaction & importedTransaction) const
{
QString scheduleName = matchedSchedule.name();
int currencyDenom = d->m_account.fraction(MyMoneyFile::instance()->currency(d->m_account.currencyId()));
QString splitValue = importedSplit.value().formatMoney(currencyDenom);
QString payeeName = MyMoneyFile::instance()->payee(importedSplit.payeeId()).name();
QString questionMsg = i18n("KMyMoney has found a scheduled transaction which matches an imported transaction.
"
"Schedule name: %1
"
"Transaction: %2 %3
"
"Do you want KMyMoney to enter this schedule now so that the transaction can be matched?",
scheduleName, splitValue, payeeName);
// check that dates are within user's setting
const int gap = std::abs(matchedSchedule.transaction().postDate().toJulianDay() - importedTransaction.postDate().toJulianDay());
if (gap > KMyMoneySettings::matchInterval())
questionMsg = i18np("KMyMoney has found a scheduled transaction which matches an imported transaction.
"
"Schedule name: %2
"
"Transaction: %3 %4
"
"The transaction dates are one day apart.
"
"Do you want KMyMoney to enter this schedule now so that the transaction can be matched?",
"KMyMoney has found a scheduled transaction which matches an imported transaction.
"
"Schedule name: %2
"
"Transaction: %3 %4
"
"The transaction dates are %1 days apart.
"
"Do you want KMyMoney to enter this schedule now so that the transaction can be matched?",
gap ,scheduleName, splitValue, payeeName);
const int userAnswer = KMessageBox::questionYesNo(0, QLatin1String("") + questionMsg + QLatin1String(""), i18n("Schedule found"));
return (userAnswer == KMessageBox::Yes);
}
void MyMoneyStatementReader::slotNewAccount(const MyMoneyAccount& acc)
{
auto newAcc = acc;
NewAccountWizard::Wizard::newAccount(newAcc);
}
diff --git a/kmymoney/dialogs/konlinetransferform.cpp b/kmymoney/dialogs/konlinetransferform.cpp
index 06fb0a22c..20fa06d1e 100644
--- a/kmymoney/dialogs/konlinetransferform.cpp
+++ b/kmymoney/dialogs/konlinetransferform.cpp
@@ -1,332 +1,332 @@
/*
* This file is part of KMyMoney, A Personal Finance Manager by KDE
* Copyright (C) 2014 Christian Dávid
* (C) 2017 by Łukasz Wojniłowicz
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "konlinetransferform.h"
#include "ui_konlinetransferform.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include "kguiutils.h"
#include "onlinetasks/interfaces/ui/ionlinejobedit.h"
#include "mymoneyfile.h"
#include "mymoney/onlinejobadministration.h"
#include "onlinejob.h"
#include "tasks/onlinetask.h"
#include "accountsmodel.h"
#include "models/models.h"
#include "icons/icons.h"
using namespace Icons;
kOnlineTransferForm::kOnlineTransferForm(QWidget *parent)
: QDialog(parent),
ui(new Ui::kOnlineTransferForm),
m_onlineJobEditWidgets(QList()),
m_requiredFields(new KMandatoryFieldGroup(this))
{
ui->setupUi(this);
ui->unsupportedIcon->setPixmap(Icons::get(Icon::DialogInformation).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize)));
// The ui designer fills the QScrollArea with a QWidget. Remove it so we can simply check for .widget() == nullptr
// if it contains a valid widget
delete ui->creditTransferEdit->takeWidget();
OnlineBankingAccountNamesFilterProxyModel* accountsModel = new OnlineBankingAccountNamesFilterProxyModel(this);
auto const model = Models::instance()->accountsModel();
accountsModel->setSourceModel(model);
ui->originAccount->setModel(accountsModel);
ui->convertMessage->hide();
ui->convertMessage->setWordWrap(true);
auto edits = onlineJobAdministration::instance()->onlineJobEdits();
std::for_each(edits.constBegin(), edits.constEnd(), [this](onlineJobAdministration::onlineJobEditOffer in) {this->loadOnlineJobEditPlugin(in);});
// Message Widget for read only jobs
m_duplicateJob = KStandardAction::copy(this);
connect(m_duplicateJob, &QAction::triggered, this, &kOnlineTransferForm::duplicateCurrentJob);
ui->headMessage->hide();
ui->headMessage->setWordWrap(true);
ui->headMessage->setCloseButtonVisible(false);
ui->headMessage->addAction(m_duplicateJob);
connect(ui->transferTypeSelection, static_cast(&QComboBox::currentIndexChanged), this, &kOnlineTransferForm::convertCurrentJob);
connect(ui->buttonAbort, &QAbstractButton::clicked, this, &kOnlineTransferForm::reject);
connect(ui->buttonSend, &QAbstractButton::clicked, this, &kOnlineTransferForm::sendJob);
connect(ui->buttonEnque, &QAbstractButton::clicked, this, &kOnlineTransferForm::accept);
connect(m_requiredFields, static_cast(&KMandatoryFieldGroup::stateChanged), ui->buttonEnque, &QPushButton::setEnabled);
connect(ui->originAccount, &KMyMoneyAccountCombo::accountSelected, this, &kOnlineTransferForm::accountChanged);
accountChanged();
setJobReadOnly(false);
m_requiredFields->add(ui->originAccount);
m_requiredFields->setOkButton(ui->buttonSend);
}
void kOnlineTransferForm::loadOnlineJobEditPlugin(const onlineJobAdministration::onlineJobEditOffer& pluginDesc)
{
try {
std::unique_ptr loader{new QPluginLoader(pluginDesc.fileName, this)};
QObject* plugin = loader->instance();
if (!plugin) {
qWarning() << "Could not load plugin for online job editor from file \"" << pluginDesc.fileName << "\".";
return;
}
// Cast to KPluginFactory
KPluginFactory* pluginFactory = qobject_cast< KPluginFactory* >(plugin);
if (!pluginFactory) {
qWarning() << "Could not create plugin factory for online job editor in file \"" << pluginDesc.fileName << "\".";
return;
}
IonlineJobEdit* widget = pluginFactory->create(pluginDesc.pluginKeyword, this);
if (!widget) {
qWarning() << "Could not create online job editor in file \"" << pluginDesc.fileName << "\".";
return;
}
// directly load the first widget into QScrollArea
bool showWidget = true;
if (!m_onlineJobEditWidgets.isEmpty()) {
widget->setEnabled(false);
showWidget = false;
}
m_onlineJobEditWidgets.append(widget);
ui->transferTypeSelection->addItem(pluginDesc.name);
m_requiredFields->add(widget);
if (showWidget)
showEditWidget(widget);
- } catch (MyMoneyException& e) {
+ } catch (MyMoneyException&) {
qWarning("Error while loading a plugin (IonlineJobEdit).");
}
}
void kOnlineTransferForm::convertCurrentJob(const int& index)
{
Q_ASSERT(index < m_onlineJobEditWidgets.count());
IonlineJobEdit* widget = m_onlineJobEditWidgets.at(index);
// Vars set by onlineJobAdministration::convertBest
onlineTaskConverter::convertType convertType;
QString userMessage;
widget->setOnlineJob(onlineJobAdministration::instance()->convertBest(activeOnlineJob(), widget->supportedOnlineTasks(), convertType, userMessage));
if (convertType == onlineTaskConverter::convertImpossible && userMessage.isEmpty())
userMessage = i18n("During the change of the order your previous entries could not be converted.");
if (!userMessage.isEmpty()) {
switch (convertType) {
case onlineTaskConverter::convertionLossyMajor:
ui->convertMessage->setMessageType(KMessageWidget::Warning);
break;
case onlineTaskConverter::convertImpossible:
case onlineTaskConverter::convertionLossyMinor:
ui->convertMessage->setMessageType(KMessageWidget::Information);
break;
case onlineTaskConverter::convertionLoseless: break;
}
ui->convertMessage->setText(userMessage);
ui->convertMessage->animatedShow();
}
showEditWidget(widget);
}
void kOnlineTransferForm::duplicateCurrentJob()
{
IonlineJobEdit* widget = qobject_cast< IonlineJobEdit* >(ui->creditTransferEdit->widget());
if (widget == 0)
return;
onlineJob duplicate(QString(), activeOnlineJob());
widget->setOnlineJob(duplicate);
}
void kOnlineTransferForm::accept()
{
emit acceptedForSave(activeOnlineJob());
QDialog::accept();
}
void kOnlineTransferForm::sendJob()
{
emit acceptedForSend(activeOnlineJob());
QDialog::accept();
}
void kOnlineTransferForm::reject()
{
QDialog::reject();
}
bool kOnlineTransferForm::setOnlineJob(const onlineJob job)
{
QString name;
try {
name = job.task()->taskName();
} catch (const onlineJob::emptyTask&) {
return false;
}
setCurrentAccount(job.responsibleAccount());
if (showEditWidget(name)) {
IonlineJobEdit* widget = qobject_cast(ui->creditTransferEdit->widget());
if (widget != 0) { // This can happen if there are no widgets
const bool ret = widget->setOnlineJob(job);
setJobReadOnly(!job.isEditable());
return ret;
}
}
return false;
}
void kOnlineTransferForm::accountChanged()
{
const QString accountId = ui->originAccount->getSelected();
try {
ui->orderAccountBalance->setValue(MyMoneyFile::instance()->balance(accountId));
} catch (const MyMoneyException&) {
// @todo this can happen until the selection allows to select correct accounts only
ui->orderAccountBalance->setText("");
}
foreach (IonlineJobEdit* widget, m_onlineJobEditWidgets)
widget->setOriginAccount(accountId);
checkNotSupportedWidget();
}
bool kOnlineTransferForm::checkEditWidget()
{
return checkEditWidget(qobject_cast(ui->creditTransferEdit->widget()));
}
bool kOnlineTransferForm::checkEditWidget(IonlineJobEdit* widget)
{
if (widget != 0 && onlineJobAdministration::instance()->isJobSupported(ui->originAccount->getSelected(), widget->supportedOnlineTasks())) {
return true;
}
return false;
}
/** @todo auto set another widget if a loseless convert is possible */
void kOnlineTransferForm::checkNotSupportedWidget()
{
if (!checkEditWidget()) {
ui->displayStack->setCurrentIndex(0);
} else {
ui->displayStack->setCurrentIndex(1);
}
}
void kOnlineTransferForm::setCurrentAccount(const QString& accountId)
{
ui->originAccount->setSelected(accountId);
}
onlineJob kOnlineTransferForm::activeOnlineJob() const
{
IonlineJobEdit* widget = qobject_cast(ui->creditTransferEdit->widget());
if (widget == 0)
return onlineJob();
return widget->getOnlineJob();
}
void kOnlineTransferForm::setJobReadOnly(const bool& readOnly)
{
ui->originAccount->setDisabled(readOnly);
ui->transferTypeSelection->setDisabled(readOnly);
if (readOnly) {
ui->headMessage->setMessageType(KMessageWidget::Information);
if (activeOnlineJob().sendDate().isValid())
ui->headMessage->setText(i18n("This credit-transfer was sent to your bank at %1 therefore cannot be edited anymore. You may create a copy for editing.", activeOnlineJob().sendDate().toString(Qt::DefaultLocaleShortDate)));
else
ui->headMessage->setText(i18n("This credit-transfer is not editable. You may create a copy for editing."));
if (this->isHidden())
ui->headMessage->show();
else
ui->headMessage->animatedShow();
} else {
ui->headMessage->animatedHide();
}
}
bool kOnlineTransferForm::showEditWidget(const QString& onlineTaskName)
{
int index = 0;
foreach (IonlineJobEdit* widget, m_onlineJobEditWidgets) {
if (widget->supportedOnlineTasks().contains(onlineTaskName)) {
ui->transferTypeSelection->setCurrentIndex(index);
showEditWidget(widget);
return true;
}
++index;
}
return false;
}
void kOnlineTransferForm::showEditWidget(IonlineJobEdit* widget)
{
Q_CHECK_PTR(widget);
QWidget* oldWidget = ui->creditTransferEdit->takeWidget();
if (oldWidget != 0) { // This is true at the first call of showEditWidget() and if there are no widgets.
oldWidget->setEnabled(false);
disconnect(qobject_cast(oldWidget), &IonlineJobEdit::readOnlyChanged, this, &kOnlineTransferForm::setJobReadOnly);
}
widget->setEnabled(true);
ui->creditTransferEdit->setWidget(widget);
setJobReadOnly(widget->isReadOnly());
widget->show();
connect(widget, &IonlineJobEdit::readOnlyChanged, this, &kOnlineTransferForm::setJobReadOnly);
checkNotSupportedWidget();
m_requiredFields->changed();
}
kOnlineTransferForm::~kOnlineTransferForm()
{
ui->creditTransferEdit->takeWidget();
qDeleteAll(m_onlineJobEditWidgets);
delete ui;
delete m_duplicateJob;
}
diff --git a/kmymoney/dialogs/settings/ksettingsplugins.cpp b/kmymoney/dialogs/settings/ksettingsplugins.cpp
index ce4c121db..f24192bfd 100644
--- a/kmymoney/dialogs/settings/ksettingsplugins.cpp
+++ b/kmymoney/dialogs/settings/ksettingsplugins.cpp
@@ -1,176 +1,176 @@
/***************************************************************************
ksettingsplugins.cpp
--------------------
- (C) 2017 by Łukasz Wojniłowicz
+ Copyright (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 "ksettingsplugins.h"
// ----------------------------------------------------------------------------
// QT Includes
#include
#include
// ----------------------------------------------------------------------------
// KDE Includes
#include
#include
#include
#include
// ----------------------------------------------------------------------------
// Project Includes
#include "pluginloader.h"
struct pluginGroupInfo {
QList plugins;
KPluginSelector::PluginLoadMethod loadMethod;
QString categoryName;
};
class KSettingsPluginsPrivate
{
Q_DISABLE_COPY(KSettingsPluginsPrivate)
public:
KSettingsPluginsPrivate(KSettingsPlugins* qq) :
m_pluginSelector(new KPluginSelector(qq))
{
}
~KSettingsPluginsPrivate()
{
delete m_pluginSelector;
}
/**
* @brief This should be called after save to kmymoneyrc in order to update cached on/off states
*/
void updateSavedPluginStates()
{
for (auto i = 0 ; i < pluginInfos.size(); ++i)
savedPluginStates[i] = pluginInfos[i].isPluginEnabled();
}
/**
* @brief This compares plugin on/off states from KPluginSelector with cached one
* @return true if user changes to plugin on/off state aren't different than initial
*/
bool isEqualToSavedStates()
{
for (auto i = 0 ; i < pluginInfos.size(); ++i)
if (savedPluginStates[i] != pluginInfos[i].isPluginEnabled())
return false;
return true;
}
KPluginSelector* const m_pluginSelector;
QList pluginInfos;
/**
* @brief savedPluginStates This caches on/off states as in kmymoneyrc
*/
QBitArray savedPluginStates;
};
KSettingsPlugins::KSettingsPlugins(QWidget* parent) :
QWidget(parent),
d_ptr(new KSettingsPluginsPrivate(this))
{
Q_D(KSettingsPlugins);
auto layout = new QVBoxLayout;
setLayout(layout); // otherwise KPluginSelector occupies very little area
layout->addWidget(d->m_pluginSelector);
auto allPluginDatas = KMyMoneyPlugin::listPlugins(false); // fetch all available KMyMoney plugins
QVector standardPlugins;
QVector payeePlugins;
QVector onlinePlugins;
// divide plugins in some arbitrary categories
for (const KPluginMetaData& pluginData : allPluginDatas)
switch (KMyMoneyPlugin::pluginCategory(pluginData)) {
case KMyMoneyPlugin::Category::StandardPlugin:
standardPlugins.append(pluginData);
break;
case KMyMoneyPlugin::Category::PayeeIdentifier:
payeePlugins.append(pluginData);
break;
case KMyMoneyPlugin::Category::OnlineBankOperations:
onlinePlugins.append(pluginData);
break;
default:
break;
}
const QVector pluginGroups {
{KPluginInfo::fromMetaData(standardPlugins),
KPluginSelector::PluginLoadMethod::ReadConfigFile,
i18n("KMyMoney Plugins")},
{KPluginInfo::fromMetaData(payeePlugins),
KPluginSelector::PluginLoadMethod::IgnoreConfigFile,
i18n("Payee Identifier")},
{KPluginInfo::fromMetaData(onlinePlugins),
KPluginSelector::PluginLoadMethod::IgnoreConfigFile,
i18n("Online Banking Operations")}
};
// add all plugins to selector
for(const auto& pluginGroup : pluginGroups) {
if (!pluginGroup.plugins.isEmpty()) {
d->m_pluginSelector->addPlugins(pluginGroup.plugins,
pluginGroup.loadMethod,
pluginGroup.categoryName); // at that step plugin on/off state should be fetched automatically by KPluginSelector
d->pluginInfos.append(pluginGroup.plugins); // store initial on/off state to be able to enable/disable Apply button
}
}
d->savedPluginStates.resize(d->pluginInfos.size());
d->updateSavedPluginStates();
connect(d->m_pluginSelector, &KPluginSelector::changed, this, &KSettingsPlugins::slotPluginsSelectionChanged);
}
KSettingsPlugins::~KSettingsPlugins()
{
Q_D(KSettingsPlugins);
delete d;
}
void KSettingsPlugins::slotPluginsSelectionChanged(bool b)
{
Q_D(KSettingsPlugins);
if (b) {
d->m_pluginSelector->updatePluginsState();
emit changed(!d->isEqualToSavedStates());
}
}
void KSettingsPlugins::slotResetToDefaults()
{
Q_D(KSettingsPlugins);
d->m_pluginSelector->defaults();
}
void KSettingsPlugins::slotSavePluginConfiguration()
{
Q_D(KSettingsPlugins);
if (!d->isEqualToSavedStates()) {
d->m_pluginSelector->save();
d->updateSavedPluginStates();
emit settingsChanged(QStringLiteral("Plugins"));
}
}
diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp
index 862d5210a..60ba1aadd 100644
--- a/kmymoney/kmymoney.cpp
+++ b/kmymoney/kmymoney.cpp
@@ -1,4324 +1,4324 @@
/***************************************************************************
kmymoney.cpp
-------------------
copyright : (C) 2000 by Michael Edwardes
(C) 2007 by Thomas Baumgart
(C) 2017 by Łukasz Wojniłowicz
****************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include
#include "kmymoney.h"
// ----------------------------------------------------------------------------
// Std C++ / STL Includes
#include
#include
#include
// ----------------------------------------------------------------------------
// QT Includes
#include
#include // only for performance tests
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// ----------------------------------------------------------------------------
// KDE Includes
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef KF5Holidays_FOUND
#include
#include
#endif
// ----------------------------------------------------------------------------
// Project Includes
#include "kmymoneysettings.h"
#include "kmymoneyadaptor.h"
#include "dialogs/settings/ksettingskmymoney.h"
#include "dialogs/kbackupdlg.h"
#include "dialogs/kenterscheduledlg.h"
#include "dialogs/kconfirmmanualenterdlg.h"
#include "dialogs/kmymoneypricedlg.h"
#include "dialogs/kcurrencyeditdlg.h"
#include "dialogs/kequitypriceupdatedlg.h"
#include "dialogs/kmymoneyfileinfodlg.h"
#include "dialogs/kfindtransactiondlg.h"
#include "dialogs/knewbankdlg.h"
#include "wizards/newinvestmentwizard/knewinvestmentwizard.h"
#include "dialogs/knewaccountdlg.h"
#include "dialogs/editpersonaldatadlg.h"
#include "dialogs/kcurrencycalculator.h"
#include "dialogs/keditscheduledlg.h"
#include "wizards/newloanwizard/keditloanwizard.h"
#include "dialogs/kpayeereassigndlg.h"
#include "dialogs/kcategoryreassigndlg.h"
#include "wizards/endingbalancedlg/kendingbalancedlg.h"
#include "dialogs/kbalancechartdlg.h"
#include "dialogs/kloadtemplatedlg.h"
#include "dialogs/kgpgkeyselectiondlg.h"
#include "dialogs/ktemplateexportdlg.h"
#include "dialogs/transactionmatcher.h"
#include "wizards/newuserwizard/knewuserwizard.h"
#include "wizards/newaccountwizard/knewaccountwizard.h"
#include "dialogs/kbalancewarning.h"
#include "widgets/kmymoneyaccountselector.h"
#include "widgets/kmymoneypayeecombo.h"
#include "widgets/onlinejobmessagesview.h"
#include "widgets/amountedit.h"
#include "widgets/kmymoneyedit.h"
#include "widgets/kmymoneymvccombo.h"
#include "views/kmymoneyview.h"
#include "views/konlinejoboutbox.h"
#include "models/onlinejobmessagesmodel.h"
#include "models/models.h"
#include "models/accountsmodel.h"
#include "models/equitiesmodel.h"
#include "models/securitiesmodel.h"
#include "mymoney/mymoneyobject.h"
#include "mymoney/mymoneyfile.h"
#include "mymoney/mymoneyinstitution.h"
#include "mymoney/mymoneyaccount.h"
#include "mymoney/mymoneyaccountloan.h"
#include "mymoney/mymoneysecurity.h"
#include "mymoney/mymoneypayee.h"
#include "mymoney/mymoneyprice.h"
#include "mymoney/mymoneytag.h"
#include "mymoney/mymoneybudget.h"
#include "mymoney/mymoneyreport.h"
#include "mymoney/mymoneysplit.h"
#include "mymoney/mymoneyutils.h"
#include "mymoney/mymoneystatement.h"
#include "mymoney/mymoneyforecast.h"
#include "mymoney/mymoneytransactionfilter.h"
#include "mymoney/onlinejobmessage.h"
#include "converter/mymoneystatementreader.h"
#include "converter/mymoneytemplate.h"
#include "plugins/interfaces/kmmappinterface.h"
#include "plugins/interfaces/kmmviewinterface.h"
#include "plugins/interfaces/kmmstatementinterface.h"
#include "plugins/interfaces/kmmimportinterface.h"
#include "plugins/interfaceloader.h"
#include "plugins/onlinepluginextended.h"
#include "pluginloader.h"
#include "tasks/credittransfer.h"
#include "icons/icons.h"
#include "misc/webconnect.h"
#include "storage/mymoneystoragemgr.h"
#include "storage/mymoneystoragexml.h"
#include "storage/mymoneystoragebin.h"
#include "storage/mymoneystorageanon.h"
#include
#include "transactioneditor.h"
#include "konlinetransferform.h"
#include
#include
#include "kmymoneyutils.h"
#include "kcreditswindow.h"
#include "ledgerdelegate.h"
#include "storageenums.h"
#include "mymoneyenums.h"
#include "dialogenums.h"
#include "menuenums.h"
#include "misc/platformtools.h"
#ifdef KMM_DEBUG
#include "mymoney/storage/mymoneystoragedump.h"
#include "mymoneytracer.h"
#endif
using namespace Icons;
using namespace eMenu;
static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip;
static constexpr char recoveryKeyId[] = "0xD2B08440";
static constexpr char recoveryKeyId2[] = "59B0F826D2B08440";
// define the default period to warn about an expiring recoverkey to 30 days
// but allows to override this setting during build time
#ifndef RECOVER_KEY_EXPIRATION_WARNING
#define RECOVER_KEY_EXPIRATION_WARNING 30
#endif
QHash pActions;
QHash pMenus;
enum backupStateE {
BACKUP_IDLE = 0,
BACKUP_MOUNTING,
BACKUP_COPYING,
BACKUP_UNMOUNTING
};
class KMyMoneyApp::Private
{
public:
Private(KMyMoneyApp *app) :
q(app),
m_ft(0),
m_moveToAccountSelector(0),
m_statementXMLindex(0),
m_balanceWarning(0),
m_backupResult(0),
m_backupMount(0),
m_ignoreBackupExitCode(false),
m_fileOpen(false),
m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser),
m_myMoneyView(0),
m_progressBar(0),
m_searchDlg(0),
m_autoSaveTimer(0),
m_progressTimer(0),
m_inAutoSaving(false),
m_transactionEditor(0),
m_endingBalanceDlg(0),
m_saveEncrypted(0),
m_additionalKeyLabel(0),
m_additionalKeyButton(0),
m_recentFiles(0),
#ifdef KF5Holidays_FOUND
m_holidayRegion(0),
#endif
m_applicationIsReady(true),
m_webConnect(new WebConnect(app)) {
// since the days of the week are from 1 to 7,
// and a day of the week is used to index this bit array,
// resize the array to 8 elements (element 0 is left unused)
m_processingDays.resize(8);
}
void closeFile();
void unlinkStatementXML();
void moveInvestmentTransaction(const QString& fromId,
const QString& toId,
const MyMoneyTransaction& t);
QList > automaticReconciliation(const MyMoneyAccount &account,
const QList > &transactions,
const MyMoneyMoney &amount);
/**
* The public interface.
*/
KMyMoneyApp * const q;
MyMoneyFileTransaction* m_ft;
KMyMoneyAccountSelector* m_moveToAccountSelector;
int m_statementXMLindex;
KBalanceWarning* m_balanceWarning;
/** the configuration object of the application */
KSharedConfigPtr m_config;
/**
* @brief Structure of plugins objects by their interfaces
*/
KMyMoneyPlugin::Container m_plugins;
/**
* The following variable represents the state while crafting a backup.
* It can have the following values
*
* - IDLE: the default value if not performing a backup
* - MOUNTING: when a mount command has been issued
* - COPYING: when a copy command has been issued
* - UNMOUNTING: when an unmount command has been issued
*/
backupStateE m_backupState;
/**
* This variable keeps the result of the backup operation.
*/
int m_backupResult;
/**
* This variable is set, when the user selected to mount/unmount
* the backup volume.
*/
bool m_backupMount;
/**
* Flag for internal run control
*/
bool m_ignoreBackupExitCode;
bool m_fileOpen;
QFileDevice::Permissions m_fmode;
KMyMoneyApp::fileTypeE m_fileType;
KProcess m_proc;
/// A pointer to the view holding the tabs.
KMyMoneyView *m_myMoneyView;
/// The URL of the file currently being edited when open.
QUrl m_fileName;
bool m_startDialog;
QString m_mountpoint;
QProgressBar* m_progressBar;
QTime m_lastUpdate;
QLabel* m_statusLabel;
// allows multiple imports to be launched trough web connect and to be executed sequentially
QQueue m_importUrlsQueue;
KFindTransactionDlg* m_searchDlg;
// This is Auto Saving related
bool m_autoSaveEnabled;
QTimer* m_autoSaveTimer;
QTimer* m_progressTimer;
int m_autoSavePeriod;
bool m_inAutoSaving;
// pointer to the current transaction editor
TransactionEditor* m_transactionEditor;
// Reconciliation dialog
KEndingBalanceDlg* m_endingBalanceDlg;
// Pointer to the combo box used for key selection during
// File/Save as
KComboBox* m_saveEncrypted;
// id's that need to be remembered
QString m_accountGoto, m_payeeGoto;
QStringList m_additionalGpgKeys;
QLabel* m_additionalKeyLabel;
QPushButton* m_additionalKeyButton;
KRecentFilesAction* m_recentFiles;
#ifdef KF5Holidays_FOUND
// used by the calendar interface for schedules
KHolidays::HolidayRegion* m_holidayRegion;
#endif
QBitArray m_processingDays;
QMap m_holidayMap;
QStringList m_consistencyCheckResult;
bool m_applicationIsReady;
WebConnect* m_webConnect;
// methods
void consistencyCheck(bool alwaysDisplayResults);
static void setThemedCSS();
void copyConsistencyCheckResults();
void saveConsistencyCheckResults();
void checkAccountName(const MyMoneyAccount& _acc, const QString& name) const
{
auto file = MyMoneyFile::instance();
if (_acc.name() != name) {
MyMoneyAccount acc(_acc);
acc.setName(name);
file->modifyAccount(acc);
}
}
/**
* This method updates names of currencies from file to localized names
*/
void updateCurrencyNames()
{
auto file = MyMoneyFile::instance();
MyMoneyFileTransaction ft;
QList storedCurrencies = MyMoneyFile::instance()->currencyList();
QList availableCurrencies = MyMoneyFile::instance()->availableCurrencyList();
QStringList currencyIDs;
foreach (auto currency, availableCurrencies)
currencyIDs.append(currency.id());
try {
foreach (auto currency, storedCurrencies) {
int i = currencyIDs.indexOf(currency.id());
if (i != -1 && availableCurrencies.at(i).name() != currency.name()) {
currency.setName(availableCurrencies.at(i).name());
file->modifyCurrency(currency);
}
}
ft.commit();
} catch (const MyMoneyException &e) {
qDebug("Error %s updating currency names", qPrintable(e.what()));
}
}
void updateAccountNames()
{
// make sure we setup the name of the base accounts in translated form
try {
MyMoneyFileTransaction ft;
const auto file = MyMoneyFile::instance();
checkAccountName(file->asset(), i18n("Asset"));
checkAccountName(file->liability(), i18n("Liability"));
checkAccountName(file->income(), i18n("Income"));
checkAccountName(file->expense(), i18n("Expense"));
checkAccountName(file->equity(), i18n("Equity"));
ft.commit();
} catch (const MyMoneyException &) {
}
}
void ungetString(QIODevice *qfile, char *buf, int len)
{
buf = &buf[len-1];
while (len--) {
qfile->ungetChar(*buf--);
}
}
bool applyFileFixes()
{
const auto blocked = MyMoneyFile::instance()->blockSignals(true);
KSharedConfigPtr config = KSharedConfig::openConfig();
KConfigGroup grp = config->group("General Options");
// For debugging purposes, we can turn off the automatic fix manually
// by setting the entry in kmymoneyrc to true
grp = config->group("General Options");
if (grp.readEntry("SkipFix", false) != true) {
MyMoneyFileTransaction ft;
try {
// Check if we have to modify the file before we allow to work with it
auto s = MyMoneyFile::instance()->storage();
while (s->fileFixVersion() < s->currentFixVersion()) {
qDebug("%s", qPrintable((QString("testing fileFixVersion %1 < %2").arg(s->fileFixVersion()).arg(s->currentFixVersion()))));
switch (s->fileFixVersion()) {
case 0:
fixFile_0();
s->setFileFixVersion(1);
break;
case 1:
fixFile_1();
s->setFileFixVersion(2);
break;
case 2:
fixFile_2();
s->setFileFixVersion(3);
break;
case 3:
fixFile_3();
s->setFileFixVersion(4);
break;
// add new levels above. Don't forget to increase currentFixVersion() for all
// the storage backends this fix applies to
default:
throw MYMONEYEXCEPTION(i18n("Unknown fix level in input file"));
}
}
ft.commit();
} catch (const MyMoneyException &) {
MyMoneyFile::instance()->blockSignals(blocked);
return false;
}
} else {
qDebug("Skipping automatic transaction fix!");
}
MyMoneyFile::instance()->blockSignals(blocked);
return true;
}
void connectStorageToModels()
{
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->accountsModel(), &AccountsModel::slotObjectModified);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged,
Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged,
Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged,
Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged,
Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged,
Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged,
Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified);
q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved);
}
void disconnectStorageFromModels()
{
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->accountsModel(), &AccountsModel::slotObjectModified);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged,
Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged,
Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged,
Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged,
Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged,
Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged,
Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded,
Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified,
Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified);
q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved,
Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved);
}
/**
* This method is used after a file or database has been
* read into storage, and performs various initialization tasks
*
* @retval true all went okay
* @retval false an exception occurred during this process
*/
bool initializeStorage()
{
const auto blocked = MyMoneyFile::instance()->blockSignals(true);
updateAccountNames();
updateCurrencyNames();
selectBaseCurrency();
// setup the standard precision
AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()));
KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()));
if (!applyFileFixes())
return false;
MyMoneyFile::instance()->blockSignals(blocked);
emit q->kmmFilePlugin(KMyMoneyApp::postOpen);
Models::instance()->fileOpened();
connectStorageToModels();
// inform everyone about new data
MyMoneyFile::instance()->forceDataChanged();
q->slotCheckSchedules();
m_myMoneyView->slotFileOpened();
return true;
}
/**
* This method attaches an empty storage object to the MyMoneyFile
* object. It calls removeStorage() to remove a possibly attached
* storage object.
*/
void newStorage()
{
removeStorage();
auto file = MyMoneyFile::instance();
file->attachStorage(new MyMoneyStorageMgr);
}
/**
* This method removes an attached storage from the MyMoneyFile
* object.
*/
void removeStorage()
{
auto file = MyMoneyFile::instance();
auto p = file->storage();
if (p) {
file->detachStorage(p);
delete p;
}
}
/**
* if no base currency is defined, start the dialog and force it to be set
*/
void selectBaseCurrency()
{
auto file = MyMoneyFile::instance();
// check if we have a base currency. If not, we need to select one
QString baseId;
try {
baseId = MyMoneyFile::instance()->baseCurrency().id();
} catch (const MyMoneyException &e) {
qDebug("%s", qPrintable(e.what()));
}
if (baseId.isEmpty()) {
QPointer dlg = new KCurrencyEditDlg(q);
// connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotSetBaseCurrency(MyMoneySecurity)));
dlg->exec();
delete dlg;
}
try {
baseId = MyMoneyFile::instance()->baseCurrency().id();
} catch (const MyMoneyException &e) {
qDebug("%s", qPrintable(e.what()));
}
if (!baseId.isEmpty()) {
// check that all accounts have a currency
QList list;
file->accountList(list);
QList::Iterator it;
// don't forget those standard accounts
list << file->asset();
list << file->liability();
list << file->income();
list << file->expense();
list << file->equity();
for (it = list.begin(); it != list.end(); ++it) {
QString cid;
try {
if (!(*it).currencyId().isEmpty() || (*it).currencyId().length() != 0)
cid = MyMoneyFile::instance()->currency((*it).currencyId()).id();
} catch (const MyMoneyException& e) {
qDebug() << QLatin1String("Account") << (*it).id() << (*it).name() << e.what();
}
if (cid.isEmpty()) {
(*it).setCurrencyId(baseId);
MyMoneyFileTransaction ft;
try {
file->modifyAccount(*it);
ft.commit();
} catch (const MyMoneyException &e) {
qDebug("Unable to setup base currency in account %s (%s): %s", qPrintable((*it).name()), qPrintable((*it).id()), qPrintable(e.what()));
}
}
}
}
}
/**
* Calls MyMoneyFile::readAllData which reads a MyMoneyFile into appropriate
* data structures in memory. The return result is examined to make sure no
* errors occurred whilst parsing.
*
* @param url The URL to read from.
* If no protocol is specified, file:// is assumed.
*
* @return Whether the read was successful.
*/
bool openNondatabase(const QUrl &url)
{
if (!url.isValid())
throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url())));
QString fileName;
auto downloadedFile = false;
if (url.isLocalFile()) {
fileName = url.toLocalFile();
} else {
fileName = KMyMoneyUtils::downloadFile(url);
downloadedFile = true;
}
if (!KMyMoneyUtils::fileExists(QUrl::fromLocalFile(fileName)))
throw MYMONEYEXCEPTION(QString::fromLatin1("Error opening the file.\n"
"Requested file: '%1'.\n"
"Downloaded file: '%2'").arg(qPrintable(url.url()), fileName));
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly))
throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName));
QByteArray qbaFileHeader(2, '\0');
const auto sFileToShort = QString::fromLatin1("File %1 is too short.").arg(fileName);
if (file.read(qbaFileHeader.data(), 2) != 2)
throw MYMONEYEXCEPTION(sFileToShort);
file.close();
// There's a problem with the KFilterDev and KGPGFile classes:
// One supports the at(n) member but not ungetch() together with
// read() and the other does not provide an at(n) method but
// supports read() that considers the ungetch() buffer. QFile
// supports everything so this is not a problem. We solve the problem
// for now by keeping track of which method can be used.
auto haveAt = true;
auto isEncrypted = false;
emit q->kmmFilePlugin(preOpen);
QIODevice* qfile = nullptr;
QString sFileHeader(qbaFileHeader);
if (sFileHeader == QString("\037\213")) { // gzipped?
qfile = new KCompressionDevice(fileName, COMPRESSION_TYPE);
} else if (sFileHeader == QString("--") || // PGP ASCII armored?
sFileHeader == QString("\205\001") || // PGP binary?
sFileHeader == QString("\205\002")) { // PGP binary?
if (KGPGFile::GPGAvailable()) {
qfile = new KGPGFile(fileName);
haveAt = false;
isEncrypted = true;
} else {
throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("GPG is not available for decryption of file %1", fileName)));
}
} else {
// we can't use file directly, as we delete qfile later on
qfile = new QFile(file.fileName());
}
if (!qfile->open(QIODevice::ReadOnly)) {
delete qfile;
throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName));
}
qbaFileHeader.resize(8);
if (qfile->read(qbaFileHeader.data(), 8) != 8)
throw MYMONEYEXCEPTION(sFileToShort);
if (haveAt)
qfile->seek(0);
else
ungetString(qfile, qbaFileHeader.data(), 8);
// Ok, we got the first block of 8 bytes. Read in the two
// unsigned long int's by preserving endianess. This is
// achieved by reading them through a QDataStream object
qint32 magic0, magic1;
QDataStream s(&qbaFileHeader, QIODevice::ReadOnly);
s >> magic0;
s >> magic1;
// If both magic numbers match (we actually read in the
// text 'KMyMoney' then we assume a binary file and
// construct a reader for it. Otherwise, we construct
// an XML reader object.
//
// The expression magic0 < 30 is only used to create
// a binary reader if we assume an old binary file. This
// should be removed at some point. An alternative is to
// check the beginning of the file against an pattern
// of the XML file (e.g. '?%1").arg(i18n("File %1 contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.", fileName)));
}
// Scan the first 70 bytes to see if we find something
// we know. For now, we support our own XML format and
// GNUCash XML format. If the file is smaller, then it
// contains no valid data and we reject it anyway.
qbaFileHeader.resize(70);
if (qfile->read(qbaFileHeader.data(), 70) != 70)
throw MYMONEYEXCEPTION(sFileToShort);
if (haveAt)
qfile->seek(0);
else
ungetString(qfile, qbaFileHeader.data(), 70);
IMyMoneyOperationsFormat* pReader = nullptr;
QRegExp kmyexp("");
QRegExp gncexp("formatName().compare(QLatin1String("GNC")) == 0) {
pReader = plugin->reader();
break;
}
}
if (!pReader) {
KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage."));
return false;
}
m_fileType = KMyMoneyApp::GncXML;
} else {
throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("File %1 contains an unknown file format.", fileName)));
}
// disconnect the current storga manager from the engine
MyMoneyFile::instance()->detachStorage();
auto storage = new MyMoneyStorageMgr;
pReader->setProgressCallback(&KMyMoneyApp::progressCallback);
pReader->readFile(qfile, storage);
pReader->setProgressCallback(0);
delete pReader;
qfile->close();
delete qfile;
// if a temporary file was downloaded, then it will be removed
// with the next call. Otherwise, it stays untouched on the local
// filesystem.
if (downloadedFile)
QFile::remove(fileName);
// things are finished, now we connect the storage to the engine
// which forces a reload of the cache in the engine with those
// objects that are cached
MyMoneyFile::instance()->attachStorage(storage);
// encapsulate transactions to the engine to be able to commit/rollback
MyMoneyFileTransaction ft;
// make sure we setup the encryption key correctly
if (isEncrypted && MyMoneyFile::instance()->value("kmm-encryption-key").isEmpty())
MyMoneyFile::instance()->setValue("kmm-encryption-key", KMyMoneySettings::gpgRecipientList().join(","));
ft.commit();
return true;
}
/**
* This method is called from readFile to open a database file which
* is to be processed in 'proper' database mode, i.e. in-place updates
*
* @param dbaseURL pseudo-QUrl representation of database
*
* @retval true Database opened successfully
* @retval false Could not open or read database
*/
bool openDatabase(const QUrl &url)
{
// open the database
auto pStorage = MyMoneyFile::instance()->storage();
if (!pStorage)
pStorage = new MyMoneyStorageMgr;
auto rc = false;
auto pluginFound = false;
for (const auto& plugin : m_plugins.storage) {
if (plugin->formatName().compare(QLatin1String("SQL")) == 0) {
rc = plugin->open(pStorage, url);
pluginFound = true;
break;
}
}
if(!pluginFound)
KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage."));
if(!rc) {
removeStorage();
delete pStorage;
return false;
}
if (pStorage) {
MyMoneyFile::instance()->detachStorage();
MyMoneyFile::instance()->attachStorage(pStorage);
}
return true;
}
/**
* Close the currently opened file and create an empty new file.
*
* @see MyMoneyFile
*/
void newFile()
{
closeFile();
m_fileType = KMyMoneyApp::KmmXML; // assume native type until saved
m_fileOpen = true;
}
/**
* Saves the data into permanent storage using the XML format.
*
* @param url The URL to save into.
* If no protocol is specified, file:// is assumed.
* @param keyList QString containing a comma separated list of keys
* to be used for encryption. If @p keyList is empty,
* the file will be saved unencrypted (the default)
*
* @retval false save operation failed
* @retval true save operation was successful
*/
bool saveFile(const QUrl &url, const QString& keyList = QString())
{
QString filename = url.path();
if (!m_fileOpen) {
KMessageBox::error(q, i18n("Tried to access a file when it has not been opened"));
return false;
}
emit q->kmmFilePlugin(KMyMoneyApp::preSave);
std::unique_ptr storageWriter;
// If this file ends in ".ANON.XML" then this should be written using the
// anonymous writer.
bool plaintext = filename.right(4).toLower() == ".xml";
if (filename.right(9).toLower() == ".anon.xml")
storageWriter = std::make_unique();
else
storageWriter = std::make_unique();
// actually, url should be the parameter to this function
// but for now, this would involve too many changes
bool rc = true;
try {
if (! url.isValid()) {
throw MYMONEYEXCEPTION(i18n("Malformed URL '%1'", url.url()));
}
if (url.isLocalFile()) {
filename = url.toLocalFile();
try {
const unsigned int nbak = KMyMoneySettings::autoBackupCopies();
if (nbak) {
KBackup::numberedBackupFile(filename, QString(), QStringLiteral("~"), nbak);
}
saveToLocalFile(filename, storageWriter.get(), plaintext, keyList);
} catch (const MyMoneyException &) {
throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename));
}
} else {
QTemporaryFile tmpfile;
tmpfile.open(); // to obtain the name
tmpfile.close();
saveToLocalFile(tmpfile.fileName(), storageWriter.get(), plaintext, keyList);
Q_CONSTEXPR int permission = -1;
QFile file(tmpfile.fileName());
file.open(QIODevice::ReadOnly);
KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite);
if (!putjob->exec()) {
throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'.
%2", url.toDisplayString(), putjob->errorString()));
}
file.close();
}
m_fileType = KMyMoneyApp::KmmXML;
} catch (const MyMoneyException &e) {
KMessageBox::error(q, e.what());
MyMoneyFile::instance()->setDirty();
rc = false;
}
emit q->kmmFilePlugin(postSave);
return rc;
}
/**
* This method is used by saveFile() to store the data
* either directly in the destination file if it is on
* the local file system or in a temporary file when
* the final destination is reached over a network
* protocol (e.g. FTP)
*
* @param localFile the name of the local file
* @param writer pointer to the formatter
* @param plaintext whether to override any compression & encryption settings
* @param keyList QString containing a comma separated list of keys to be used for encryption
* If @p keyList is empty, the file will be saved unencrypted
*
* @note This method will close the file when it is written.
*/
void saveToLocalFile(const QString& localFile, IMyMoneyOperationsFormat* pWriter, bool plaintext, const QString& keyList)
{
// Check GPG encryption
bool encryptFile = true;
bool encryptRecover = false;
if (!keyList.isEmpty()) {
if (!KGPGFile::GPGAvailable()) {
KMessageBox::sorry(q, i18n("GPG does not seem to be installed on your system. Please make sure that GPG can be found using the standard search path. This time, encryption is disabled."), i18n("GPG not found"));
encryptFile = false;
} else {
if (KMyMoneySettings::encryptRecover()) {
encryptRecover = true;
if (!KGPGFile::keyAvailable(QString(recoveryKeyId))) {
KMessageBox::sorry(q, i18n("You have selected to encrypt your data also with the KMyMoney recover key, but the key with id
%1
has not been found in your keyring at this time. Please make sure to import this key into your keyring. You can find it on the KMyMoney web-site. This time your data will not be encrypted with the KMyMoney recover key.
", QString(recoveryKeyId)), i18n("GPG Key not found"));
encryptRecover = false;
}
}
for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) {
if (!KGPGFile::keyAvailable(key)) {
KMessageBox::sorry(q, i18n("You have specified to encrypt your data for the user-id
%1.Unfortunately, a valid key for this user-id was not found in your keyring. Please make sure to import a valid key for this user-id. This time, encryption is disabled.
", key), i18n("GPG Key not found"));
encryptFile = false;
break;
}
}
if (encryptFile == true) {
QString msg = i18n("You have configured to save your data in encrypted form using GPG. Make sure you understand that you might lose all your data if you encrypt it, but cannot decrypt it later on. If unsure, answer No.
");
if (KMessageBox::questionYesNo(q, msg, i18n("Store GPG encrypted"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "StoreEncrypted") == KMessageBox::No) {
encryptFile = false;
}
}
}
}
// Create a temporary file if needed
QString writeFile = localFile;
QTemporaryFile tmpFile;
if (QFile::exists(localFile)) {
tmpFile.open();
writeFile = tmpFile.fileName();
tmpFile.close();
}
/**
* @brief Automatically restore settings when scope is left
*/
struct restorePreviousSettingsHelper {
restorePreviousSettingsHelper()
: m_signalsWereBlocked{MyMoneyFile::instance()->signalsBlocked()}
{
MyMoneyFile::instance()->blockSignals(true);
}
~restorePreviousSettingsHelper()
{
MyMoneyFile::instance()->blockSignals(m_signalsWereBlocked);
}
const bool m_signalsWereBlocked;
} restoreHelper;
MyMoneyFileTransaction ft;
MyMoneyFile::instance()->deletePair("kmm-encryption-key");
std::unique_ptr device;
if (!keyList.isEmpty() && encryptFile && !plaintext) {
std::unique_ptr kgpg = std::unique_ptr(new KGPGFile{writeFile});
if (kgpg) {
for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) {
kgpg->addRecipient(key.toLatin1());
}
if (encryptRecover) {
kgpg->addRecipient(recoveryKeyId);
}
MyMoneyFile::instance()->setValue("kmm-encryption-key", keyList);
device = std::unique_ptr(kgpg.release());
}
} else {
QFile *file = new QFile(writeFile);
// The second parameter of KCompressionDevice means that KCompressionDevice will delete the QFile object
device = std::unique_ptr(new KCompressionDevice{file, true, (plaintext) ? KCompressionDevice::None : COMPRESSION_TYPE});
}
ft.commit();
if (!device || !device->open(QIODevice::WriteOnly)) {
throw MYMONEYEXCEPTION(i18n("Unable to open file '%1' for writing.", localFile));
}
pWriter->setProgressCallback(&KMyMoneyApp::progressCallback);
pWriter->writeFile(device.get(), MyMoneyFile::instance()->storage());
device->close();
// Check for errors if possible, only possible for KGPGFile
QFileDevice *fileDevice = qobject_cast(device.get());
if (fileDevice && fileDevice->error() != QFileDevice::NoError) {
throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile));
}
if (writeFile != localFile) {
// This simple comparison is possible because the strings are equal if no temporary file was created.
// If a temporary file was created, it is made in a way that the name is definitely different. So no
// symlinks etc. have to be evaluated.
if (!QFile::remove(localFile) || !QFile::rename(writeFile, localFile))
throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile));
}
QFile::setPermissions(localFile, m_fmode);
pWriter->setProgressCallback(0);
}
/**
* Call this to see if the MyMoneyFile contains any unsaved data.
*
* @retval true if any data has been modified but not saved
* @retval false otherwise
*/
bool dirty()
{
if (!m_fileOpen)
return false;
return MyMoneyFile::instance()->dirty();
}
/* DO NOT ADD code to this function or any of it's called ones.
Instead, create a new function, fixFile_n, and modify the initializeStorage()
logic above to call it */
void fixFile_3()
{
// make sure each storage object contains a (unique) id
MyMoneyFile::instance()->storageId();
}
void fixFile_2()
{
auto file = MyMoneyFile::instance();
MyMoneyTransactionFilter filter;
filter.setReportAllSplits(false);
QList transactionList;
file->transactionList(transactionList, filter);
// scan the transactions and modify transactions with two splits
// which reference an account and a category to have the memo text
// of the account.
auto count = 0;
foreach (const auto transaction, transactionList) {
if (transaction.splitCount() == 2) {
QString accountId;
QString categoryId;
QString accountMemo;
QString categoryMemo;
foreach (const auto split, transaction.splits()) {
auto acc = file->account(split.accountId());
if (acc.isIncomeExpense()) {
categoryId = split.id();
categoryMemo = split.memo();
} else {
accountId = split.id();
accountMemo = split.memo();
}
}
if (!accountId.isEmpty() && !categoryId.isEmpty()
&& accountMemo != categoryMemo) {
MyMoneyTransaction t(transaction);
MyMoneySplit s(t.splitById(categoryId));
s.setMemo(accountMemo);
t.modifySplit(s);
file->modifyTransaction(t);
++count;
}
}
}
qDebug("%d transactions fixed in fixFile_2", count);
}
void fixFile_1()
{
// we need to fix reports. If the account filter list contains
// investment accounts, we need to add the stock accounts to the list
// as well if we don't have the expert mode enabled
if (!KMyMoneySettings::expertMode()) {
try {
QList reports = MyMoneyFile::instance()->reportList();
QList::iterator it_r;
for (it_r = reports.begin(); it_r != reports.end(); ++it_r) {
QStringList list;
(*it_r).accounts(list);
QStringList missing;
QStringList::const_iterator it_a, it_b;
for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) {
auto acc = MyMoneyFile::instance()->account(*it_a);
if (acc.accountType() == eMyMoney::Account::Type::Investment) {
foreach (const auto accountID, acc.accountList()) {
if (!list.contains(accountID)) {
missing.append(accountID);
}
}
}
}
if (!missing.isEmpty()) {
(*it_r).addAccount(missing);
MyMoneyFile::instance()->modifyReport(*it_r);
}
}
} catch (const MyMoneyException &) {
}
}
}
#if 0
if (!m_accountsView->allItemsSelected())
{
// retrieve a list of selected accounts
QStringList list;
m_accountsView->selectedItems(list);
// if we're not in expert mode, we need to make sure
// that all stock accounts for the selected investment
// account are also selected
if (!KMyMoneySettings::expertMode()) {
QStringList missing;
QStringList::const_iterator it_a, it_b;
for (it_a = list.begin(); it_a != list.end(); ++it_a) {
auto acc = MyMoneyFile::instance()->account(*it_a);
if (acc.accountType() == Account::Type::Investment) {
foreach (const auto accountID, acc.accountList()) {
if (!list.contains(accountID)) {
missing.append(accountID);
}
}
}
}
list += missing;
}
m_filter.addAccount(list);
}
#endif
void fixFile_0()
{
/* (Ace) I am on a crusade against file fixups. Whenever we have to fix the
* file, it is really a warning. So I'm going to print a debug warning, and
* then go track them down when I see them to figure out how they got saved
* out needing fixing anyway.
*/
auto file = MyMoneyFile::instance();
QList accountList;
file->accountList(accountList);
QList::Iterator it_a;
QList scheduleList = file->scheduleList();
QList::Iterator it_s;
MyMoneyAccount equity = file->equity();
MyMoneyAccount asset = file->asset();
bool equityListEmpty = equity.accountList().count() == 0;
for (it_a = accountList.begin(); it_a != accountList.end(); ++it_a) {
if ((*it_a).accountType() == eMyMoney::Account::Type::Loan
|| (*it_a).accountType() == eMyMoney::Account::Type::AssetLoan) {
fixLoanAccount_0(*it_a);
}
// until early before 0.8 release, the equity account was not saved to
// the file. If we have an equity account with no sub-accounts but
// find and equity account that has equity() as it's parent, we reparent
// this account. Need to move it to asset() first, because otherwise
// MyMoneyFile::reparent would act as NOP.
if (equityListEmpty && (*it_a).accountType() == eMyMoney::Account::Type::Equity) {
if ((*it_a).parentAccountId() == equity.id()) {
auto acc = *it_a;
// tricky, force parent account to be empty so that we really
// can re-parent it
acc.setParentAccountId(QString());
file->reparentAccount(acc, equity);
qDebug() << Q_FUNC_INFO << " fixed account " << acc.id() << " reparented to " << equity.id();
}
}
}
for (it_s = scheduleList.begin(); it_s != scheduleList.end(); ++it_s) {
fixSchedule_0(*it_s);
}
fixTransactions_0();
}
void fixSchedule_0(MyMoneySchedule sched)
{
MyMoneyTransaction t = sched.transaction();
QList splitList = t.splits();
QList::ConstIterator it_s;
bool updated = false;
try {
// Check if the splits contain valid data and set it to
// be valid.
for (it_s = splitList.constBegin(); it_s != splitList.constEnd(); ++it_s) {
// the first split is always the account on which this transaction operates
// and if the transaction commodity is not set, we take this
if (it_s == splitList.constBegin() && t.commodity().isEmpty()) {
qDebug() << Q_FUNC_INFO << " " << t.id() << " has no commodity";
try {
auto acc = MyMoneyFile::instance()->account((*it_s).accountId());
t.setCommodity(acc.currencyId());
updated = true;
} catch (const MyMoneyException &) {
}
}
// make sure the account exists. If not, remove the split
try {
MyMoneyFile::instance()->account((*it_s).accountId());
} catch (const MyMoneyException &) {
qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " removed, because account '" << (*it_s).accountId() << "' does not exist.";
t.removeSplit(*it_s);
updated = true;
}
if ((*it_s).reconcileFlag() != eMyMoney::Split::State::NotReconciled) {
qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " should be 'not reconciled'";
MyMoneySplit split = *it_s;
split.setReconcileDate(QDate());
split.setReconcileFlag(eMyMoney::Split::State::NotReconciled);
t.modifySplit(split);
updated = true;
}
// the schedule logic used to operate only on the value field.
// This is now obsolete.
if ((*it_s).shares().isZero() && !(*it_s).value().isZero()) {
MyMoneySplit split = *it_s;
split.setShares(split.value());
t.modifySplit(split);
updated = true;
}
}
// If there have been changes, update the schedule and
// the engine data.
if (updated) {
sched.setTransaction(t);
MyMoneyFile::instance()->modifySchedule(sched);
}
} catch (const MyMoneyException &e) {
qWarning("Unable to update broken schedule: %s", qPrintable(e.what()));
}
}
void fixLoanAccount_0(MyMoneyAccount acc)
{
if (acc.value("final-payment").isEmpty()
|| acc.value("term").isEmpty()
|| acc.value("periodic-payment").isEmpty()
|| acc.value("loan-amount").isEmpty()
|| acc.value("interest-calculation").isEmpty()
|| acc.value("schedule").isEmpty()
|| acc.value("fixed-interest").isEmpty()) {
KMessageBox::information(q,
i18n("The account \"%1\" was previously created as loan account but some information is missing.
The new loan wizard will be started to collect all relevant information.
Please use KMyMoney version 0.8.7 or later and earlier than version 0.9 to correct the problem.
"
, acc.name()),
i18n("Account problem"));
throw MYMONEYEXCEPTION("Fix LoanAccount0 not supported anymore");
}
}
void fixTransactions_0()
{
auto file = MyMoneyFile::instance();
QList scheduleList = file->scheduleList();
MyMoneyTransactionFilter filter;
filter.setReportAllSplits(false);
QList transactionList;
file->transactionList(transactionList, filter);
QList::Iterator it_x;
QStringList interestAccounts;
KMSTATUS(i18n("Fix transactions"));
q->slotStatusProgressBar(0, scheduleList.count() + transactionList.count());
int cnt = 0;
// scan the schedules to find interest accounts
for (it_x = scheduleList.begin(); it_x != scheduleList.end(); ++it_x) {
MyMoneyTransaction t = (*it_x).transaction();
QList::ConstIterator it_s;
QStringList accounts;
bool hasDuplicateAccounts = false;
foreach (const auto split, t.splits()) {
if (accounts.contains(split.accountId())) {
hasDuplicateAccounts = true;
qDebug() << Q_FUNC_INFO << " " << t.id() << " has multiple splits with account " << split.accountId();
} else {
accounts << split.accountId();
}
if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) {
if (interestAccounts.contains(split.accountId()) == 0) {
interestAccounts << split.accountId();
}
}
}
if (hasDuplicateAccounts) {
fixDuplicateAccounts_0(t);
}
++cnt;
if (!(cnt % 10))
q->slotStatusProgressBar(cnt);
}
// scan the transactions and modify loan transactions
for (auto& transaction : transactionList) {
QString defaultAction;
QList splits = transaction.splits();
QStringList accounts;
// check if base commodity is set. if not, set baseCurrency
if (transaction.commodity().isEmpty()) {
qDebug() << Q_FUNC_INFO << " " << transaction.id() << " has no base currency";
transaction.setCommodity(file->baseCurrency().id());
file->modifyTransaction(transaction);
}
bool isLoan = false;
// Determine default action
if (transaction.splitCount() == 2) {
// check for transfer
int accountCount = 0;
MyMoneyMoney val;
foreach (const auto split, splits) {
auto acc = file->account(split.accountId());
if (acc.accountGroup() == eMyMoney::Account::Type::Asset
|| acc.accountGroup() == eMyMoney::Account::Type::Liability) {
val = split.value();
accountCount++;
if (acc.accountType() == eMyMoney::Account::Type::Loan
|| acc.accountType() == eMyMoney::Account::Type::AssetLoan)
isLoan = true;
} else
break;
}
if (accountCount == 2) {
if (isLoan)
defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization);
else
defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer);
} else {
if (val.isNegative())
defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal);
else
defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit);
}
}
isLoan = false;
foreach (const auto split, splits) {
auto acc = file->account(split.accountId());
MyMoneyMoney val = split.value();
if (acc.accountGroup() == eMyMoney::Account::Type::Asset
|| acc.accountGroup() == eMyMoney::Account::Type::Liability) {
if (!val.isPositive()) {
defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal);
break;
} else {
defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit);
break;
}
}
}
#if 0
// Check for correct actions in transactions referencing credit cards
bool needModify = false;
// The action fields are actually not used anymore in the ledger view logic
// so we might as well skip this whole thing here!
for (it_s = splits.begin(); needModify == false && it_s != splits.end(); ++it_s) {
auto acc = file->account((*it_s).accountId());
MyMoneyMoney val = (*it_s).value();
if (acc.accountType() == Account::Type::CreditCard) {
if (val < 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer))
needModify = true;
if (val >= 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer))
needModify = true;
}
}
// (Ace) Extended the #endif down to cover this conditional, because as-written
// it will ALWAYS be skipped.
if (needModify == true) {
for (it_s = splits.begin(); it_s != splits.end(); ++it_s) {
(*it_s).setAction(defaultAction);
transaction.modifySplit(*it_s);
file->modifyTransaction(transaction);
}
splits = transaction.splits(); // update local copy
qDebug("Fixed credit card assignment in %s", transaction.id().data());
}
#endif
// Check for correct assignment of ActionInterest in all splits
// and check if there are any duplicates in this transactions
for (auto& split : splits) {
MyMoneyAccount splitAccount = file->account(split.accountId());
if (!accounts.contains(split.accountId())) {
accounts << split.accountId();
}
// if this split references an interest account, the action
// must be of type ActionInterest
if (interestAccounts.contains(split.accountId())) {
if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) {
qDebug() << Q_FUNC_INFO << " " << transaction.id() << " contains an interest account (" << split.accountId() << ") but does not have ActionInterest";
split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest));
transaction.modifySplit(split);
file->modifyTransaction(transaction);
qDebug("Fixed interest action in %s", qPrintable(transaction.id()));
}
// if it does not reference an interest account, it must not be
// of type ActionInterest
} else {
if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) {
qDebug() << Q_FUNC_INFO << " " << transaction.id() << " does not contain an interest account so it should not have ActionInterest";
split.setAction(defaultAction);
transaction.modifySplit(split);
file->modifyTransaction(transaction);
qDebug("Fixed interest action in %s", qPrintable(transaction.id()));
}
}
// check that for splits referencing an account that has
// the same currency as the transactions commodity the value
// and shares field are the same.
if (transaction.commodity() == splitAccount.currencyId()
&& split.value() != split.shares()) {
qDebug() << Q_FUNC_INFO << " " << transaction.id() << " " << split.id() << " uses the transaction currency, but shares != value";
split.setShares(split.value());
transaction.modifySplit(split);
file->modifyTransaction(transaction);
}
// fix the shares and values to have the correct fraction
if (!splitAccount.isInvest()) {
try {
int fract = splitAccount.fraction();
if (split.shares() != split.shares().convert(fract)) {
qDebug("adjusting fraction in %s,%s", qPrintable(transaction.id()), qPrintable(split.id()));
split.setShares(split.shares().convert(fract));
split.setValue(split.value().convert(fract));
transaction.modifySplit(split);
file->modifyTransaction(transaction);
}
} catch (const MyMoneyException &) {
qDebug("Missing security '%s', split not altered", qPrintable(splitAccount.currencyId()));
}
}
}
++cnt;
if (!(cnt % 10))
q->slotStatusProgressBar(cnt);
}
q->slotStatusProgressBar(-1, -1);
}
void fixDuplicateAccounts_0(MyMoneyTransaction& t)
{
qDebug("Duplicate account in transaction %s", qPrintable(t.id()));
}
};
KMyMoneyApp::KMyMoneyApp(QWidget* parent) :
KXmlGuiWindow(parent),
d(new Private(this))
{
#ifdef KMM_DBUS
new KmymoneyAdaptor(this);
QDBusConnection::sessionBus().registerObject("/KMymoney", this);
QDBusConnection::sessionBus().interface()->registerService(
"org.kde.kmymoney-" + QString::number(platformTools::processId()), QDBusConnectionInterface::DontQueueService);
#endif
// Register the main engine types used as meta-objects
qRegisterMetaType("MyMoneyMoney");
qRegisterMetaType("MyMoneySecurity");
// preset the pointer because we need it during the course of this constructor
kmymoney = this;
d->m_config = KSharedConfig::openConfig();
d->setThemedCSS();
MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay());
updateCaption(true);
QFrame* frame = new QFrame;
frame->setFrameStyle(QFrame::NoFrame);
// values for margin (11) and spacing(6) taken from KDialog implementation
QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame);
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(6);
{
#ifdef Q_OS_WIN
QString themeName = QLatin1Literal("system"); // using QIcon::setThemeName on Craft build system causes icons to disappear
#else
QString themeName = KMyMoneySettings::iconsTheme(); // get theme user wants
#endif
if (!themeName.isEmpty() && themeName != QLatin1Literal("system")) // if it isn't default theme then set it
QIcon::setThemeName(themeName);
Icons::setIconThemeNames(QIcon::themeName()); // get whatever theme user ends up with and hope our icon names will fit that theme
}
initStatusBar();
pActions = initActions();
pMenus = initMenus();
d->newStorage();
d->m_myMoneyView = new KMyMoneyView(this/*the global variable kmymoney is not yet assigned. So we pass it here*/);
layout->addWidget(d->m_myMoneyView, 10);
connect(d->m_myMoneyView, &KMyMoneyView::aboutToChangeView, this, &KMyMoneyApp::slotResetSelections);
connect(d->m_myMoneyView, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)),
this, SLOT(slotUpdateActions()));
connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg);
connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar);
///////////////////////////////////////////////////////////////////
// call inits to invoke all other construction parts
readOptions();
// now initialize the plugin structure
createInterfaces();
KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, d->m_plugins, this, guiFactory());
onlineJobAdministration::instance()->setOnlinePlugins(d->m_plugins.extended);
d->m_myMoneyView->setOnlinePlugins(d->m_plugins.online);
d->m_myMoneyView->setStoragePlugins(d->m_plugins.storage);
setCentralWidget(frame);
connect(&d->m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotBackupHandleEvents()));
// force to show the home page if the file is closed
connect(pActions[Action::ViewTransactionDetail], &QAction::toggled, d->m_myMoneyView, &KMyMoneyView::slotShowTransactionDetail);
d->m_backupState = BACKUP_IDLE;
QLocale locale;
int weekStart = locale.firstDayOfWeek();
int weekEnd = weekStart-1;
if (weekEnd < Qt::Monday) {
weekEnd = Qt::Sunday;
}
bool startFirst = (weekStart < weekEnd);
for (int i = 0; i < 8; ++i) {
if (startFirst)
d->m_processingDays.setBit(i, (i >= weekStart && i <= weekEnd));
else
d->m_processingDays.setBit(i, (i >= weekStart || i <= weekEnd));
}
d->m_autoSaveTimer = new QTimer(this);
d->m_progressTimer = new QTimer(this);
connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave()));
connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone()));
// make sure, we get a note when the engine changes state
connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotDataChanged()));
// connect the WebConnect server
connect(d->m_webConnect, SIGNAL(gotUrl(QUrl)), this, SLOT(webConnect(QUrl)));
// make sure we have a balance warning object
d->m_balanceWarning = new KBalanceWarning(this);
// setup the initial configuration
slotUpdateConfiguration(QString());
// kickstart date change timer
slotDateChanged();
connect(this, SIGNAL(fileLoaded(QUrl)), onlineJobAdministration::instance(), SLOT(updateOnlineTaskProperties()));
}
KMyMoneyApp::~KMyMoneyApp()
{
d->removeStorage();
// delete cached objects since the are in the way
// when unloading the plugins
onlineJobAdministration::instance()->clearCaches();
// we need to unload all plugins before we destroy anything else
KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Unload, d->m_plugins, this, guiFactory());
delete d->m_searchDlg;
delete d->m_transactionEditor;
delete d->m_endingBalanceDlg;
delete d->m_moveToAccountSelector;
#ifdef KF5Holidays_FOUND
delete d->m_holidayRegion;
#endif
delete d;
}
QUrl KMyMoneyApp::lastOpenedURL()
{
QUrl url = d->m_startDialog ? QUrl() : d->m_fileName;
if (!url.isValid()) {
url = QUrl::fromUserInput(readLastUsedFile());
}
ready();
return url;
}
void KMyMoneyApp::slotObjectDestroyed(QObject* o)
{
if (o == d->m_moveToAccountSelector) {
d->m_moveToAccountSelector = 0;
}
}
void KMyMoneyApp::slotInstallConsistencyCheckContextMenu()
{
// this code relies on the implementation of KMessageBox::informationList to add a context menu to that list,
// please adjust it if it's necessary or rewrite the way the consistency check results are displayed
if (QWidget* dialog = QApplication::activeModalWidget()) {
if (QListWidget* widget = dialog->findChild()) {
// give the user a hint that the data can be saved
widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it."));
widget->setWhatsThis(widget->toolTip());
widget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint)));
}
}
}
void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos)
{
// allow the user to save the consistency check results
if (QWidget* widget = qobject_cast< QWidget* >(sender())) {
QMenu contextMenu(widget);
QAction* copy = new QAction(i18n("Copy to clipboard"), widget);
QAction* save = new QAction(i18n("Save to file"), widget);
contextMenu.addAction(copy);
contextMenu.addAction(save);
QAction *result = contextMenu.exec(widget->mapToGlobal(pos));
if (result == copy) {
// copy the consistency check results to the clipboard
d->copyConsistencyCheckResults();
} else if (result == save) {
// save the consistency check results to a file
d->saveConsistencyCheckResults();
}
}
}
QHash KMyMoneyApp::initMenus()
{
QHash