File indexing completed on 2024-05-19 05:08:12

0001 /*
0002     SPDX-FileCopyrightText: 2020 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "config-kmymoney.h"
0007 
0008 #include "templateloader.h"
0009 
0010 // ----------------------------------------------------------------------------
0011 // QT Includes
0012 
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QDomElement>
0016 #include <QList>
0017 #include <QStandardPaths>
0018 #include <QTemporaryFile>
0019 #include <QTimer>
0020 #include <QTreeView>
0021 #include <QTreeWidget>
0022 #include <QUrl>
0023 
0024 // ----------------------------------------------------------------------------
0025 // KDE Includes
0026 
0027 #include <KLocalizedString>
0028 #include <KIO/StoredTransferJob>
0029 #include <KJobWidgets>
0030 #include <KXmlGuiWindow>
0031 #include <KMessageBox>
0032 #include <KTextEdit>
0033 
0034 // ----------------------------------------------------------------------------
0035 // Project Includes
0036 
0037 #include "templatesmodel.h"
0038 #include "mymoneytemplate.h"
0039 #include "kmymoneyutils.h"
0040 #include "mymoneyfile.h"
0041 #include "mymoneyaccount.h"
0042 #include "mymoneyexception.h"
0043 
0044 class TemplateLoaderPrivate
0045 {
0046     Q_DISABLE_COPY(TemplateLoaderPrivate)
0047 
0048 public:
0049     TemplateLoaderPrivate(TemplateLoader* qq)
0050         : q_ptr(qq)
0051         , model(nullptr)
0052         , countryRow(0)
0053     {
0054     }
0055 
0056     ~TemplateLoaderPrivate()
0057     {
0058     }
0059 
0060     bool loadTemplate(const QUrl &url, MyMoneyTemplate& tmpl)
0061     {
0062         QString filename;
0063         bool downloadedFile = false;
0064         if (!url.isValid()) {
0065             qDebug() << "Invalid template URL" << url.url();
0066             return false;
0067         }
0068 
0069         if (url.isLocalFile()) {
0070             filename = url.toLocalFile();
0071 
0072         } else {
0073             downloadedFile = true;
0074             KIO::StoredTransferJob *transferjob = KIO::storedGet (url);
0075             KJobWidgets::setWindow(transferjob, KMyMoneyUtils::mainWindow());
0076             if (! transferjob->exec()) {
0077                 KMessageBox::detailedError(KMyMoneyUtils::mainWindow(),
0078                                            i18n("Error while loading file '%1'.", url.url()),
0079                                            transferjob->errorString(),
0080                                            i18n("File access error"));
0081                 return false;
0082             }
0083             QTemporaryFile file;
0084             file.setAutoRemove(false);
0085             file.open();
0086             file.write(transferjob->data());
0087             filename = file.fileName();
0088             file.close();
0089         }
0090 
0091         bool rc = true;
0092         QFile file(filename);
0093         QFileInfo info(file);
0094         if (!info.isFile()) {
0095             QString msg = i18n("<p><b>%1</b> is not a template file.</p>", filename);
0096             KMessageBox::error(KMyMoneyUtils::mainWindow(), msg, i18n("Filetype Error"));
0097             return false;
0098         }
0099 
0100         if (file.open(QIODevice::ReadOnly)) {
0101             tmpl.setSource(url);
0102             const auto result = tmpl.setAccountTree(&file);
0103             if (!result.isOK()) {
0104                 QString msg = i18n("<p>Error while reading template file <b>%1</b> in line %2, column %3</p>", filename, result.errorLine, result.errorColumn);
0105                 KMessageBox::detailedError(KMyMoneyUtils::mainWindow(), msg, result.errorMsg, i18nc("@title:window", "Template Loading Error"));
0106                 rc = false;
0107             }
0108             file.close();
0109         } else {
0110             KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("File '%1' not found.", filename));
0111             rc = false;
0112         }
0113 
0114         // if a temporary file was downloaded, then it will be removed
0115         // with the next call. Otherwise, it stays untouched on the local
0116         // filesystem.
0117         if (downloadedFile) {
0118             QFile::remove(filename);
0119         }
0120         return rc;
0121     }
0122 
0123     bool createAccounts(const MyMoneyTemplate& tmpl, MyMoneyAccount& parent, QDomNode account)
0124     {
0125         bool rc = true;
0126         while (rc == true && !account.isNull()) {
0127             MyMoneyAccount acc;
0128             if (account.isElement()) {
0129                 QDomElement accountElement = account.toElement();
0130                 if (accountElement.tagName() == "account") {
0131                     QList<MyMoneyAccount> subAccountList;
0132                     QList<MyMoneyAccount>::ConstIterator it;
0133                     it = subAccountList.constEnd();
0134                     if (!parent.accountList().isEmpty()) {
0135                         MyMoneyFile::instance()->accountList(subAccountList, parent.accountList());
0136                         for (it = subAccountList.constBegin(); it != subAccountList.constEnd(); ++it) {
0137                             if ((*it).name() == accountElement.attribute("name")) {
0138                                 acc = *it;
0139                                 QString id = accountElement.attribute("id");
0140                                 if (!id.isEmpty())
0141                                     m_vatAccountMap[id] = acc.id();
0142                                 break;
0143                             }
0144                         }
0145                     }
0146                     if (it == subAccountList.constEnd()) {
0147                         // not found, we need to create it
0148                         acc.setName(accountElement.attribute("name"));
0149                         acc.setAccountType(static_cast<eMyMoney::Account::Type>(accountElement.attribute("type").toUInt()));
0150                         setFlags(tmpl, acc, account.firstChild());
0151                         try {
0152                             MyMoneyFile::instance()->addAccount(acc, parent);
0153                         } catch (const MyMoneyException &) {
0154                         }
0155                         QString id = accountElement.attribute("id");
0156                         if (!id.isEmpty())
0157                             m_vatAccountMap[id] = acc.id();
0158                     }
0159                     createAccounts(tmpl, acc, account.firstChild());
0160                 }
0161             }
0162             account = account.nextSibling();
0163         }
0164         return rc;
0165     }
0166 
0167     bool setFlags(const MyMoneyTemplate& tmpl, MyMoneyAccount& acc, QDomNode flags)
0168     {
0169         bool rc = true;
0170         while (rc == true && !flags.isNull()) {
0171             if (flags.isElement()) {
0172                 QDomElement flagElement = flags.toElement();
0173                 if (flagElement.tagName() == "flag") {
0174                     // make sure, we only store flags we know!
0175                     QString value = flagElement.attribute("name");
0176                     if (value == "Tax") {
0177                         acc.setValue(value, "Yes");
0178                     } else if (value == "VatRate") {
0179                         acc.setValue(value, flagElement.attribute("value"));
0180                     } else if (value == "VatAccount") {
0181                         // will be resolved later in importTemplate()
0182                         acc.setValue("UnresolvedVatAccount", flagElement.attribute("value"));
0183                     } else if (value == "OpeningBalanceAccount") {
0184                         acc.setValue("OpeningBalanceAccount", "Yes");
0185                     } else {
0186                         KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("<p>Invalid flag type <b>%1</b> for account <b>%3</b> in template file <b>%2</b></p>", flagElement.attribute("name"), tmpl.source().toDisplayString(), acc.name()));
0187                         rc = false;
0188                     }
0189                     QString currency = flagElement.attribute("currency");
0190                     if (!currency.isEmpty())
0191                         acc.setCurrencyId(currency);
0192                 }
0193             }
0194             flags = flags.nextSibling();
0195         }
0196         return rc;
0197     }
0198 
0199 public:
0200     TemplateLoader*                         q_ptr;
0201     TemplatesModel*                         model;
0202     // a map of country name or country name (language name) -> localeId (lang_country) so be careful how you use it
0203     QMap<QString, QString>                  countries;
0204     QString                                 currentLocaleId;
0205 
0206     QStringList                             dirlist;          ///< list of directories to scan for templates
0207     QMap<QString, QString>::const_iterator  it_m;
0208     int                                     countryRow;
0209     QMap<QString,QString>                   m_vatAccountMap;
0210 };
0211 
0212 TemplateLoader::TemplateLoader(QWidget* parent) :
0213     QObject(parent),
0214     d_ptr(new TemplateLoaderPrivate(this))
0215 {
0216     Q_INIT_RESOURCE(templates);
0217 }
0218 
0219 TemplateLoader::~TemplateLoader()
0220 {
0221     Q_D(TemplateLoader);
0222     delete d;
0223 }
0224 
0225 void TemplateLoader::load(TemplatesModel* model)
0226 {
0227     Q_D(TemplateLoader);
0228     d->model = model;
0229     model->unload();
0230     d->currentLocaleId.clear();
0231 
0232     QStringList dirs;
0233 
0234     if (d->model == nullptr) {
0235         return;
0236     }
0237     d->dirlist = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, "templates", QStandardPaths::LocateDirectory);
0238     d->dirlist.append(":/templates");
0239 
0240     QStringList::iterator it;
0241     for (it = d->dirlist.begin(); it != d->dirlist.end(); ++it) {
0242         QDir dir(*it);
0243         dirs = dir.entryList(QStringList("*"), QDir::Dirs | QDir::NoDotAndDotDot);
0244         QStringList::iterator it_d;
0245 
0246         // note: the logic for multiple languages seems to work only for two
0247         // a third one will be entered without the language in parenthesis
0248         for (it_d = dirs.begin(); it_d != dirs.end(); ++it_d) {
0249             QLocale templateLocale(*it_d);
0250             if (templateLocale.language() != QLocale::C) {
0251                 QString country = QLocale().countryToString(templateLocale.country());
0252                 QString lang = QLocale().languageToString(templateLocale.language());
0253                 if (d->countries.contains(country)) {
0254                     if (d->countries[country] != *it_d) {
0255                         QString otherName = d->countries[country];
0256                         QLocale otherTemplateLocale(otherName);
0257                         QString otherCountry = QLocale().countryToString(otherTemplateLocale.country());
0258                         QString otherLang = QLocale().languageToString(otherTemplateLocale.language());
0259                         d->countries.remove(country);
0260                         d->countries[QString("%1 (%2)").arg(otherCountry, otherLang)] = otherName;
0261                         d->countries[QString("%1 (%2)").arg(country, lang)] = *it_d;
0262                         // retain the item corresponding to the current locale
0263                         if (QLocale().country() == templateLocale.country()) {
0264                             d->currentLocaleId = *it_d;
0265                         }
0266                     }
0267                 } else {
0268                     d->countries[country] = *it_d;
0269                     // retain the item corresponding to the current locale
0270                     if (QLocale().country() == templateLocale.country()) {
0271                         d->currentLocaleId = *it_d;
0272                     }
0273                 }
0274             } else {
0275                 qDebug("'%s/%s' not scanned", qPrintable(*it), qPrintable(*it_d));
0276             }
0277         }
0278     }
0279 
0280     // now that we know, what we can get at max, we scan everything
0281     // and parse the templates into the model
0282 
0283     d->model->insertRows(0, d->countries.count());
0284     d->countryRow = 0;
0285     d->it_m = d->countries.constBegin();
0286 
0287     // in case we have found countries, we load them
0288     if (d->countryRow < d->countries.count()) {
0289         QMetaObject::invokeMethod(this, "slotLoadCountry", Qt::QueuedConnection);
0290     } else {
0291         Q_EMIT loadingFinished();
0292     }
0293 }
0294 
0295 void TemplateLoader::slotLoadCountry()
0296 {
0297     Q_D(TemplateLoader);
0298 
0299     const auto parentIdx = d->model->index(d->countryRow, 0);
0300     d->model->setData(parentIdx, d->it_m.key(), eMyMoney::Model::TemplatesCountryRole);
0301     d->model->setData(parentIdx, d->it_m.value(), eMyMoney::Model::TemplatesLocaleRole);
0302 
0303     // now scan all directories for that country
0304     for (QStringList::iterator it = d->dirlist.begin(); it != d->dirlist.end(); ++it) {
0305         QDir dir(QString("%1/%2").arg(*it, d->it_m.value()));
0306         if (dir.exists()) {
0307             const QStringList files = dir.entryList(QStringList("*.kmt"), QDir::Files);
0308             for (const auto& file : qAsConst(files)) {
0309                 const auto url = QUrl::fromUserInput(QString("%1/%2").arg(dir.canonicalPath(), file));
0310                 MyMoneyTemplate tmpl;
0311                 if (d->loadTemplate(url, tmpl)) {
0312                     d->model->addItem(tmpl, parentIdx);
0313                 }
0314             }
0315         }
0316     }
0317 
0318     // next item in list
0319     ++d->it_m;
0320     ++d->countryRow;
0321     if (d->countryRow < d->countries.count()) {
0322         QMetaObject::invokeMethod(this, "slotLoadCountry", Qt::QueuedConnection);
0323     } else {
0324         Q_EMIT loadingFinished();
0325     }
0326 }
0327 
0328 bool TemplateLoader::importTemplate(const MyMoneyTemplate& tmpl)
0329 {
0330     Q_D(TemplateLoader);
0331     d->m_vatAccountMap.clear();
0332     auto accounts = tmpl.accountTree();
0333     bool rc = !accounts.isNull();
0334 
0335     MyMoneyFile* file = MyMoneyFile::instance();
0336     while (rc == true && !accounts.isNull() && accounts.isElement()) {
0337         QDomElement childElement = accounts.toElement();
0338         if (childElement.tagName() == "account") {
0339             MyMoneyAccount parent;
0340             switch (childElement.attribute("type").toUInt()) {
0341             case (uint)eMyMoney::Account::Type::Asset:
0342                 parent = file->asset();
0343                 break;
0344             case (uint)eMyMoney::Account::Type::Liability:
0345                 parent = file->liability();
0346                 break;
0347             case (uint)eMyMoney::Account::Type::Income:
0348                 parent = file->income();
0349                 break;
0350             case (uint)eMyMoney::Account::Type::Expense:
0351                 parent = file->expense();
0352                 break;
0353             case (uint)eMyMoney::Account::Type::Equity:
0354                 parent = file->equity();
0355                 break;
0356 
0357             default:
0358                 KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("<p>Invalid top-level account type <b>%1</b> in template file <b>%2</b></p>", childElement.attribute("type"), tmpl.source().toDisplayString()));
0359                 rc = false;
0360             }
0361 
0362             if (rc == true) {
0363                 if (childElement.attribute("name").isEmpty())
0364                     rc = d->createAccounts(tmpl, parent, childElement.firstChild());
0365                 else
0366                     rc = d->createAccounts(tmpl, parent, childElement);
0367             }
0368         } else {
0369             rc = false;
0370         }
0371         accounts = accounts.nextSibling();
0372     }
0373 
0374     /*
0375      * Resolve imported vat account assignments
0376      *
0377      * The template account id of the assigned vat account
0378      * is stored temporarily in the account key/value pair
0379      * 'UnresolvedVatAccount' and resolved below.
0380      */
0381     QList<MyMoneyAccount> accountList;
0382     file->accountList(accountList);
0383     for (auto acc : qAsConst(accountList)) {
0384         if (!acc.pairs().contains("UnresolvedVatAccount"))
0385             continue;
0386         QString id = acc.value("UnresolvedVatAccount");
0387         acc.setValue("VatAccount", d->m_vatAccountMap[id]);
0388         acc.deletePair("UnresolvedVatAccount");
0389         MyMoneyFile::instance()->modifyAccount(acc);
0390     }
0391 
0392     return rc;
0393 }