File indexing completed on 2024-05-05 05:08:10

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 }