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