File indexing completed on 2025-02-16 04:43:33
0001 /* 0002 SPDX-FileCopyrightText: 2014 Ralf Habacker <ralf.habacker@freenet.de> 0003 SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "../kmymoney/mymoney/mymoneyaccount.h" 0007 0008 #include <QDebug> 0009 #include <QDir> 0010 #include <QFile> 0011 #include <QMap> 0012 #include <QRegularExpression> 0013 #include <QStringList> 0014 #include <QTextStream> 0015 #include <QXmlStreamReader> 0016 0017 #include "mymoneyenums.h" 0018 0019 using namespace eMyMoney; 0020 0021 QDebug operator <<(QDebug out, const QXmlStreamNamespaceDeclaration &a) 0022 { 0023 out << "QXmlStreamNamespaceDeclaration(" 0024 << "prefix:" << a.prefix().toString() 0025 << "namespaceuri:" << a.namespaceUri().toString() 0026 << ")"; 0027 return out; 0028 } 0029 0030 QDebug operator <<(QDebug out, const QXmlStreamAttribute &a) 0031 { 0032 out << "QXmlStreamAttribute(" 0033 << "prefix:" << a.prefix().toString() 0034 << "namespaceuri:" << a.namespaceUri().toString() 0035 << "name:" << a.name().toString() 0036 << " value:" << a.value().toString() 0037 << ")"; 0038 return out; 0039 } 0040 0041 static bool debug = false; 0042 static bool verbose = false; 0043 static bool withID = false; 0044 static bool noLevel1Names = false; 0045 static bool withTax = false; 0046 static bool prefixNameWithCode = false; 0047 0048 typedef QMap<QString,QString> DirNameMapType; 0049 0050 /** 0051 * map to hold differences from gnucash to kmymoney template directory 0052 * @return directory name map 0053 */ 0054 DirNameMapType &getDirNameMap() 0055 { 0056 static DirNameMapType dirNameMap; 0057 dirNameMap["cs"] = "cs_CZ"; 0058 dirNameMap["da"] = "dk"; 0059 dirNameMap["ja"] = "ja_JP"; 0060 dirNameMap["ko"] = "ko_KR"; 0061 dirNameMap["nb"] = "nb_NO"; 0062 dirNameMap["nl"] = "nl_NL"; 0063 dirNameMap["ru"] = "ru_RU"; 0064 return dirNameMap; 0065 } 0066 0067 int toKMyMoneyAccountType(const QString& type, int index) 0068 { 0069 // in case index is 1 we process the level that represents the 0070 // KMyMoney top-level accounts. They can only have distinct values 0071 // and we have to make sure to return one of them by converting 0072 // the Account::Type into the respective accountGroup value. 0073 if (index == 1) { 0074 const auto accountType = toKMyMoneyAccountType(type, 0); 0075 if (accountType != (int)Account::Type::Unknown) { 0076 return (int)MyMoneyAccount::accountGroup(static_cast<eMyMoney::Account::Type>(accountType)); 0077 } 0078 return 99; // unknown 0079 } 0080 0081 if(type == "ROOT") return (int)Account::Type::Unknown; 0082 else if (type == "BANK") return (int)Account::Type::Checkings; 0083 else if (type == "CASH") return (int)Account::Type::Cash; 0084 else if (type == "CREDIT") return (int)Account::Type::CreditCard; 0085 else if (type == "INVEST") return (int)Account::Type::Investment; 0086 else if (type == "RECEIVABLE") return (int)Account::Type::Asset; 0087 else if (type == "ASSET") return (int)Account::Type::Asset; 0088 else if (type == "PAYABLE") return (int)Account::Type::Liability; 0089 else if (type == "LIABILITY") return (int)Account::Type::Liability; 0090 else if (type == "CURRENCY") return (int)Account::Type::Currency; 0091 else if (type == "INCOME") return (int)Account::Type::Income; 0092 else if (type == "EXPENSE") return (int)Account::Type::Expense; 0093 else if (type == "STOCK") return (int)Account::Type::Stock; 0094 else if (type == "MUTUAL") return (int)Account::Type::Stock; 0095 else if (type == "EQUITY") return (int)Account::Type::Equity; 0096 else return 99; // unknown 0097 } 0098 0099 class TemplateAccount { 0100 public: 0101 typedef QList<TemplateAccount> List; 0102 typedef QList<TemplateAccount*> PointerList; 0103 typedef QMap<QString,QString> SlotList; 0104 0105 QString id; 0106 QString m_type; 0107 QString m_name; 0108 QString code; 0109 QString parent; 0110 SlotList slotList; 0111 0112 TemplateAccount() 0113 { 0114 } 0115 0116 void clear() 0117 { 0118 id.clear(); 0119 m_type.clear(); 0120 m_name.clear(); 0121 code.clear(); 0122 parent.clear(); 0123 slotList.clear(); 0124 } 0125 0126 bool readSlots(QXmlStreamReader &xml) 0127 { 0128 while (!xml.atEnd()) { 0129 QXmlStreamReader::TokenType type = xml.readNext(); 0130 if (type == QXmlStreamReader::StartElement) { 0131 QStringView _name = xml.name(); 0132 if (_name == QLatin1String("slot")) { 0133 type = xml.readNext(); 0134 if (type == QXmlStreamReader::Characters) 0135 type = xml.readNext(); 0136 if (type == QXmlStreamReader::StartElement) { 0137 QStringView name = xml.name(); 0138 QString key, value; 0139 if (name == QLatin1String("key")) 0140 key = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); 0141 type = xml.readNext(); 0142 if (type == QXmlStreamReader::Characters) 0143 type = xml.readNext(); 0144 if (type == QXmlStreamReader::StartElement) { 0145 name = xml.name(); 0146 if (name == QLatin1String("value")) 0147 value = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); 0148 } 0149 if (!key.isEmpty() && !value.isEmpty()) 0150 slotList[key] = value; 0151 } 0152 } 0153 } else if (type == QXmlStreamReader::EndElement) { 0154 QStringView _name = xml.name(); 0155 if (_name == QLatin1String("slots")) 0156 return true; 0157 } 0158 } 0159 return true; 0160 } 0161 0162 bool read(QXmlStreamReader &xml) 0163 { 0164 while (!xml.atEnd()) { 0165 xml.readNext(); 0166 QStringView _name = xml.name(); 0167 if (xml.isEndElement() && _name == QLatin1String("account")) { 0168 if (prefixNameWithCode && !code.isEmpty() && !m_name.startsWith(code)) 0169 m_name = code + ' ' + m_name; 0170 return true; 0171 } 0172 if (xml.isStartElement()) 0173 { 0174 if (_name == QLatin1String("name")) 0175 m_name = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); 0176 else if (_name == QLatin1String("id")) 0177 id = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); 0178 else if (_name == QLatin1String("type")) 0179 m_type = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); 0180 else if (_name == QLatin1String("code")) 0181 code = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); 0182 else if (_name == QLatin1String("parent")) 0183 parent = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); 0184 else if (_name == QLatin1String("slots")) 0185 readSlots(xml); 0186 else 0187 { 0188 xml.readElementText(QXmlStreamReader::SkipChildElements); 0189 if (debug) 0190 qDebug() << "skipping" << _name.toString(); 0191 } 0192 } 0193 } 0194 return false; 0195 } 0196 }; 0197 0198 QDebug operator <<(QDebug out, const TemplateAccount &a) 0199 { 0200 out << "TemplateAccount(" 0201 << "name:" << a.m_name 0202 << "id:" << a.id 0203 << "type:" << a.m_type 0204 << "code:" << a.code 0205 << "parent:" << a.parent 0206 << "slotList:" << a.slotList 0207 << ")\n"; 0208 return out; 0209 } 0210 0211 QDebug operator <<(QDebug out, const TemplateAccount::PointerList &a) 0212 { 0213 out << "TemplateAccount::List("; 0214 for (const auto* account : a) 0215 out << *account; 0216 out << ")"; 0217 return out; 0218 } 0219 0220 class TemplateFile { 0221 public: 0222 QString title; 0223 QString longDescription; 0224 QString shortDescription; 0225 QString fileName; 0226 TemplateAccount::List accounts; 0227 TemplateAccount *openingBalanceAccount{nullptr}; 0228 0229 bool read(QXmlStreamReader &xml) 0230 { 0231 Q_ASSERT(xml.isStartElement() && xml.name().toString() == "gnc-account-example"); 0232 0233 while (xml.readNextStartElement()) { 0234 QStringView name = xml.name(); 0235 if (name == QLatin1String("title")) 0236 title = xml.readElementText().trimmed(); 0237 else if (name == QLatin1String("short-description")) 0238 shortDescription = xml.readElementText().trimmed().replace(" ", " "); 0239 else if (name == QLatin1String("long-description")) 0240 longDescription = xml.readElementText().trimmed().replace(" ", " "); 0241 else if (name == QLatin1String("account")) { 0242 TemplateAccount account; 0243 if (account.read(xml)) 0244 accounts.append(account); 0245 } else { 0246 if (debug) 0247 qDebug() << "skipping" << name.toString(); 0248 xml.skipCurrentElement(); 0249 } 0250 } 0251 return true; 0252 } 0253 0254 bool writeAsXml(QXmlStreamWriter &xml) 0255 { 0256 xml.writeStartElement("","title"); 0257 xml.writeCharacters(title); 0258 xml.writeEndElement(); 0259 xml.writeStartElement("","shortdesc"); 0260 xml.writeCharacters(shortDescription); 0261 xml.writeEndElement(); 0262 xml.writeStartElement("","longdesc"); 0263 xml.writeCharacters(longDescription); 0264 xml.writeEndElement(); 0265 xml.writeStartElement("","accounts"); 0266 bool result = writeAccountsAsXml(xml); 0267 xml.writeEndElement(); 0268 return result; 0269 } 0270 0271 bool writeAccountsAsXml(QXmlStreamWriter &xml, const QString &id="", int index=0) 0272 { 0273 TemplateAccount::PointerList list; 0274 0275 if (index == 0) 0276 list = accountsByType("ROOT"); 0277 else 0278 list = accountsByParentID(id); 0279 0280 for (const auto account : qAsConst(list)) { 0281 if (account->m_type != "ROOT") 0282 { 0283 xml.writeStartElement("","account"); 0284 xml.writeAttribute("type", QString::number(toKMyMoneyAccountType(account->m_type, index))); 0285 xml.writeAttribute("name", noLevel1Names && index < 2 ? "" : account->m_name); 0286 if (withID) 0287 xml.writeAttribute("id", account->id); 0288 if (withTax) { 0289 if (account->slotList.contains("tax-related")) { 0290 xml.writeStartElement("flag"); 0291 xml.writeAttribute("name","Tax"); 0292 xml.writeAttribute("value",account->slotList["tax-related"] == "1" ? "Yes" : "No"); 0293 xml.writeEndElement(); 0294 } 0295 } 0296 if (account->slotList.contains("equity-type") && account->slotList["equity-type"] == "opening-balance") { 0297 if (openingBalanceAccount) { 0298 qWarning() << "template" << fileName << "already has specified" 0299 << openingBalanceAccount->m_name 0300 << "as opening balance account," 0301 << "ignoring account" << account->m_name; 0302 continue; 0303 } 0304 xml.writeStartElement("flag"); 0305 xml.writeAttribute("name","OpeningBalanceAccount"); 0306 xml.writeAttribute("value","Yes"); 0307 xml.writeEndElement(); 0308 openingBalanceAccount = account; 0309 } 0310 } 0311 index++; 0312 writeAccountsAsXml(xml, account->id, index); 0313 index--; 0314 xml.writeEndElement(); 0315 } 0316 return true; 0317 } 0318 0319 TemplateAccount *account(const QString &id) 0320 { 0321 for(int i=0; i < accounts.size(); i++) 0322 { 0323 TemplateAccount &account = accounts[i]; 0324 if (account.id == id) 0325 return &account; 0326 } 0327 return 0; 0328 } 0329 0330 TemplateAccount::PointerList accountsByType(const QString &type) 0331 { 0332 TemplateAccount::PointerList list; 0333 for(int i=0; i < accounts.size(); i++) 0334 { 0335 TemplateAccount &account = accounts[i]; 0336 if (account.m_type == type) 0337 list.append(&account); 0338 } 0339 return list; 0340 } 0341 0342 0343 static bool nameLessThan(TemplateAccount *a1, TemplateAccount *a2) 0344 { 0345 return a1->m_name < a2->m_name; 0346 } 0347 0348 TemplateAccount::PointerList accountsByParentID(const QString &parentID) 0349 { 0350 TemplateAccount::PointerList list; 0351 0352 for(int i=0; i < accounts.size(); i++) 0353 { 0354 TemplateAccount &account = accounts[i]; 0355 if (account.parent == parentID) 0356 list.append(&account); 0357 } 0358 std::sort(list.begin(), list.end(), nameLessThan); 0359 return list; 0360 } 0361 0362 bool dumpTemplates(const QString &id="", int index=0) 0363 { 0364 TemplateAccount::PointerList list; 0365 0366 if (index == 0) 0367 list = accountsByType("ROOT"); 0368 else 0369 list = accountsByParentID(id); 0370 0371 for (const auto account : qAsConst(list)) { 0372 QString a; 0373 a.fill(' ', index); 0374 qDebug() << a << account->m_name << toKMyMoneyAccountType(account->m_type, index); 0375 index++; 0376 dumpTemplates(account->id, index); 0377 index--; 0378 } 0379 return true; 0380 } 0381 }; 0382 0383 QDebug operator <<(QDebug out, const TemplateFile &a) 0384 { 0385 out << "TemplateFile(" 0386 << "title:" << a.title 0387 << "short description:" << a.shortDescription 0388 << "long description:" << a.longDescription 0389 << "accounts:"; 0390 for (const auto& account : a.accounts) 0391 out << account; 0392 out << ")"; 0393 return out; 0394 } 0395 0396 class GnuCashAccountTemplateReader { 0397 public: 0398 GnuCashAccountTemplateReader() 0399 { 0400 } 0401 0402 bool read(const QString &filename) 0403 { 0404 QFile file(filename); 0405 QTextStream in(&file); 0406 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0407 in.setCodec("utf-8"); 0408 #endif 0409 0410 if(!file.open(QIODevice::ReadOnly)) 0411 return false; 0412 inFileName = filename; 0413 return read(in.device()); 0414 } 0415 0416 TemplateFile &result() 0417 { 0418 return _template; 0419 } 0420 0421 bool dumpTemplates() 0422 { 0423 return _template.dumpTemplates(); 0424 } 0425 0426 bool writeAsXml(const QString &filename=QString()) 0427 { 0428 if (filename.isEmpty()) 0429 { 0430 QTextStream stream(stdout); 0431 return writeAsXml(stream.device()); 0432 } 0433 else 0434 { 0435 QFile file(filename); 0436 if(!file.open(QIODevice::WriteOnly)) 0437 return false; 0438 return writeAsXml(&file); 0439 } 0440 } 0441 0442 protected: 0443 0444 bool checkAndUpdateAvailableNamespaces(QXmlStreamReader &xml) 0445 { 0446 if (xml.namespaceDeclarations().size() < 5) 0447 { 0448 qWarning() << "gnucash template file is missing required name space declarations; adding by self"; 0449 } 0450 xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("act", "http://www.gnucash.org/XML/act")); 0451 xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc", "http://www.gnucash.org/XML/gnc")); 0452 xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc-act", "http://www.gnucash.org/XML/gnc-act")); 0453 xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("cmdty","http://www.gnucash.org/XML/cmdty")); 0454 xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("slot","http://www.gnucash.org/XML/slot")); 0455 return true; 0456 } 0457 0458 bool read(QIODevice *device) 0459 { 0460 m_xml.setDevice(device); 0461 while(!m_xml.atEnd()) 0462 { 0463 m_xml.readNext(); 0464 if (m_xml.isStartElement()) 0465 { 0466 if (m_xml.name() == QLatin1String("gnc-account-example")) { 0467 checkAndUpdateAvailableNamespaces(m_xml); 0468 _template.read(m_xml); 0469 } else 0470 m_xml.raiseError(QObject::tr("The file is not an gnucash account template file.")); 0471 } 0472 } 0473 if (m_xml.error() != QXmlStreamReader::NoError) 0474 qWarning() << m_xml.errorString(); 0475 return !m_xml.error(); 0476 } 0477 0478 bool writeAsXml(QIODevice *device) 0479 { 0480 static const QRegularExpression regExp(".*/accounts"); 0481 QXmlStreamWriter xml(device); 0482 xml.setAutoFormatting(true); 0483 xml.setAutoFormattingIndent(1); 0484 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0485 xml.setCodec("utf-8"); 0486 #endif 0487 xml.writeStartDocument(); 0488 0489 QString fileName = inFileName; 0490 fileName.replace(regExp, "accounts"); 0491 xml.writeComment(QString("\n" 0492 " Converted using xea2kmt from GnuCash sources\n" 0493 "\n" 0494 " %1\n" 0495 "\n" 0496 " Please check the source file for possible copyright\n" 0497 " and license information.\n" 0498 ).arg(fileName)); 0499 xml.writeDTD("<!DOCTYPE KMYMONEY-TEMPLATE>"); 0500 xml.writeStartElement("","kmymoney-account-template"); 0501 _template.fileName = fileName; 0502 bool result = _template.writeAsXml(xml); 0503 xml.writeEndElement(); 0504 xml.writeEndDocument(); 0505 return result; 0506 } 0507 0508 QXmlStreamReader m_xml; 0509 TemplateFile _template; 0510 QString inFileName; 0511 }; 0512 0513 void scanDir(QDir dir, QStringList &files) 0514 { 0515 dir.setNameFilters(QStringList("*.gnucash-xea")); 0516 dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); 0517 0518 if (debug) 0519 qDebug() << "Scanning: " << dir.path(); 0520 0521 QStringList fileList = dir.entryList(); 0522 for (int i=0; i<fileList.count(); i++) 0523 { 0524 if (debug) 0525 qDebug() << "Found file: " << fileList[i]; 0526 files.append(QString("%1/%2").arg(dir.absolutePath(), fileList[i])); 0527 } 0528 0529 dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); 0530 QStringList dirList = dir.entryList(); 0531 for (int i=0; i<dirList.size(); ++i) 0532 { 0533 QString newPath = QString("%1/%2").arg(dir.absolutePath(), dirList.at(i)); 0534 scanDir(QDir(newPath), files); 0535 } 0536 } 0537 0538 bool convertFile(const QString &inFile, const QString &outFile) 0539 { 0540 GnuCashAccountTemplateReader reader; 0541 if (!reader.read(inFile)) 0542 return false; 0543 return reader.writeAsXml(outFile); 0544 } 0545 0546 int convertFileStructure(const QString &indir, const QString &outdir) 0547 { 0548 DirNameMapType &dirNameMap = getDirNameMap(); 0549 // get gnucash account files 0550 QDir d(indir); 0551 QStringList files; 0552 scanDir(d, files); 0553 0554 const QString inPath = d.absolutePath(); 0555 const QDir outDir(outdir); 0556 const QString outPath = outDir.absolutePath(); 0557 const QStringList mapKeys = dirNameMap.keys(); 0558 int result = 0; 0559 0560 // process templates 0561 for (const auto& file : qAsConst(files)) { 0562 if (debug || verbose) 0563 qDebug() << "processing" << file; 0564 0565 // create output file dir 0566 QFileInfo fi(file); 0567 auto outFileName = fi.canonicalFilePath(); 0568 outFileName.replace(inPath, outPath); 0569 outFileName.remove("acctchrt_"); 0570 outFileName.replace(".gnucash-xea", ".kmt"); 0571 for (const auto& key : mapKeys) { 0572 if (outFileName.contains('/' + key + '/')) 0573 outFileName = outFileName.replace('/' + key + '/', '/' + dirNameMap[key] + '/'); 0574 } 0575 fi.setFile(outFileName); 0576 0577 d.setPath(fi.absolutePath()); 0578 if (!d.exists()) 0579 { 0580 if (debug) 0581 qDebug() << "creating path " << fi.absolutePath(); 0582 d.mkpath(fi.absolutePath()); 0583 } 0584 if (debug) 0585 qDebug() << "writing to " << outFileName; 0586 if (!convertFile(file, outFileName)) 0587 { 0588 qWarning() << "could not create" << outFileName; 0589 result = 1; 0590 } 0591 } 0592 return result; 0593 } 0594 0595 int main(int argc, char *argv[]) 0596 { 0597 if (argc < 2 || (argc == 2 && QLatin1String(argv[1]) == "--help")) 0598 { 0599 qWarning() << "xea2kmt: convert gnucash template file to kmymoney template file"; 0600 qWarning() << argv[0] << "<options> <gnucash-template-file> [<kmymoney-template-output-file>]"; 0601 qWarning() << argv[0] << "<options> --in-dir <gnucash-template-files-root> --out-dir <kmymoney-template-files-root>"; 0602 qWarning() << "options:"; 0603 qWarning() << " --debug - output debug information"; 0604 qWarning() << " --help - this page"; 0605 qWarning() << " --no-level1-names - do not export account names for top level accounts"; 0606 qWarning() << " --prefix-name-with-code - prefix account name with account code if present"; 0607 qWarning() << " --verbose - output processing information"; 0608 qWarning() << " --with-id - write account id attribute"; 0609 qWarning() << " --with-tax-related - parse and export gnucash 'tax-related' flag"; 0610 qWarning() << " --in-dir <dir> - search for gnucash templates files in <dir>"; 0611 qWarning() << " --out-dir <dir> - generate kmymoney templates below <dir"; 0612 return -1; 0613 } 0614 0615 QString inFileName; 0616 QString outFileName; 0617 QString inDir; 0618 QString outDir; 0619 for(int i = 1; i < argc; i++) 0620 { 0621 QString arg = QLatin1String(argv[i]); 0622 if (arg == "--debug") 0623 debug = true; 0624 else if (arg == "--verbose") 0625 verbose = true; 0626 else if (arg == "--with-id") 0627 withID = true; 0628 else if (arg == "--no-level1-names") 0629 noLevel1Names = true; 0630 else if (arg == "--with-tax-related") 0631 withTax = true; 0632 else if (arg == "--prefix-name-with-code") 0633 prefixNameWithCode = true; 0634 else if (arg == "--in-dir") 0635 inDir = argv[++i]; 0636 else if (arg == "--out-dir") 0637 outDir = argv[++i]; 0638 else if (!arg.startsWith(QLatin1String("--"))) 0639 { 0640 if (inFileName.isEmpty()) 0641 inFileName = arg; 0642 else 0643 outFileName = arg; 0644 } 0645 else 0646 { 0647 qWarning() << "invalid command line parameter'" << arg << "'"; 0648 return -1; 0649 } 0650 } 0651 0652 if (!inDir.isEmpty() && !outDir.isEmpty()) 0653 { 0654 return convertFileStructure(inDir, outDir); 0655 } 0656 0657 GnuCashAccountTemplateReader reader; 0658 bool result = reader.read(inFileName); 0659 if (debug) 0660 { 0661 qDebug() << reader.result(); 0662 reader.dumpTemplates(); 0663 } 0664 reader.writeAsXml(outFileName); 0665 return result ? 0 : -2; 0666 }