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 }