::ConstIterator it_s;
try {
bool updated = false;
// 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", 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_CSTRING("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()));
}
/**
* This method is used to update the caption of the application window.
* It sets the caption to "filename [modified] - KMyMoney".
*
* @param skipActions if true, the actions will not be updated. This
* is usually onyl required by some early calls when
* these widgets are not yet created (the default is false).
*/
void updateCaption();
void updateActions();
bool canFileSaveAs() const;
bool canUpdateAllAccounts() const;
void fileAction(eKMyMoney::FileAction action);
};
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");
#ifdef ENABLE_SQLCIPHER
/* Issues:
* 1) libsqlite3 loads implicitly before libsqlcipher
* thus making the second one loaded but non-functional,
* 2) libsqlite3 gets linked into kmymoney target implicitly
* and it's not possible to unload or unlink it explicitly
*
* Solution:
* Use e.g. dummy sqlite3_key call, so that libsqlcipher gets loaded implicitly before libsqlite3
* thus making the first one functional.
*
* Additional info:
* 1) loading libsqlcipher explicitly doesn't solve the issue,
* 2) using sqlite3_key only in sqlstorage plugin doesn't solve the issue,
* 3) in a separate, minimal test case, loading libsqlite3 explicitly
* with QLibrary::ExportExternalSymbolsHint makes libsqlcipher non-functional
*/
sqlite3_key(nullptr, nullptr, 0);
#endif
// 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());
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);
{
const auto customIconRelativePath = QString(QStringLiteral("icons/hicolor/16x16/actions/account-add.png"));
#ifndef IS_APPIMAGE
// find where our custom icons were installed based on an custom icon that we know should exist after installation
auto customIconAbsolutePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, customIconRelativePath);
if (customIconAbsolutePath.isEmpty()) {
qWarning("Custom icons were not found in any of the following QStandardPaths::AppDataLocation:");
for (const auto &standardPath : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation))
qWarning() << standardPath;
}
#else
// according to https://docs.appimage.org/packaging-guide/ingredients.html#open-source-applications
// QStandardPaths::AppDataLocation is unreliable on AppImages, so apply workaround here in case we fail to find icons
QString customIconAbsolutePath;
const auto appImageAppDataLocation = QString("%1%2%3").arg(QCoreApplication::applicationDirPath(), QString("/../share/kmymoney/"), customIconRelativePath);
if (QFile::exists(appImageAppDataLocation )) {
customIconAbsolutePath = appImageAppDataLocation ;
} else {
qWarning("Custom icons were not found in the following location:");
qWarning() << appImageAppDataLocation ;
}
#endif
// add our custom icons path to icons search path
if (!customIconAbsolutePath.isEmpty()) {
customIconAbsolutePath.chop(customIconRelativePath.length());
customIconAbsolutePath.append(QLatin1String("icons"));
auto paths = QIcon::themeSearchPaths();
paths.append(customIconAbsolutePath);
QIcon::setThemeSearchPaths(paths);
}
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
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->m_myMoneyView = new KMyMoneyView;
layout->addWidget(d->m_myMoneyView, 10);
connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg);
connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar);
// Initialize kactivities resource instance
#ifdef ENABLE_ACTIVITIES
d->m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this);
#endif
const auto viewActions = d->m_myMoneyView->actionsToBeConnected();
actionCollection()->addActions(viewActions.values());
for (auto it = viewActions.cbegin(); it != viewActions.cend(); ++it)
pActions.insert(it.key(), it.value());
///////////////////////////////////////////////////////////////////
// call inits to invoke all other construction parts
readOptions();
// now initialize the plugin structure
createInterfaces();
KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, pPlugins, this, guiFactory());
onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended);
d->m_myMoneyView->setOnlinePlugins(pPlugins.online);
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;
for (auto const& weekDay: locale.weekdays())
{
d->m_processingDays.setBit(static_cast(weekDay));
}
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()));
// connect the WebConnect server
connect(d->m_webConnect, &WebConnect::gotUrl, this, &KMyMoneyApp::webConnectUrl);
// setup the initial configuration
slotUpdateConfiguration(QString());
// kickstart date change timer
slotDateChanged();
d->fileAction(eKMyMoney::FileAction::Closed);
}
KMyMoneyApp::~KMyMoneyApp()
{
// delete cached objects since they 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, pPlugins, this, guiFactory());
d->removeStorage();
#ifdef ENABLE_HOLIDAYS
delete d->m_holidayRegion;
#endif
#ifdef ENABLE_ACTIVITIES
delete d->m_activityResourceInstance;
#endif
+ // destroy printer object
+ KMyMoneyPrinter::cleanup();
+
// make sure all settings are written to disk
KMyMoneySettings::self()->save();
delete d;
}
QUrl KMyMoneyApp::lastOpenedURL()
{
QUrl url = d->m_startDialog ? QUrl() : d->m_storageInfo.url;
if (!url.isValid()) {
url = QUrl::fromUserInput(readLastUsedFile());
}
ready();
return url;
}
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