File indexing completed on 2024-06-23 05:03:14
0001 /*************************************************************************** 0002 * SPDX-FileCopyrightText: 2022 S. MANKOWSKI stephane@mankowski.fr 0003 * SPDX-FileCopyrightText: 2022 G. DE BURE support@mankowski.fr 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 ***************************************************************************/ 0006 /** @file 0007 * This file is Skrooge plugin for ledger import / export. 0008 * 0009 * @author Stephane MANKOWSKI / Guillaume DE BURE 0010 */ 0011 #include "skgimportpluginledger.h" 0012 0013 #include <klocalizedstring.h> 0014 #include <kpluginfactory.h> 0015 0016 #include <qsavefile.h> 0017 #include <qfile.h> 0018 #include <qprocess.h> 0019 #include <qdir.h> 0020 #include <quuid.h> 0021 0022 #include "skgbankincludes.h" 0023 #include "skgdocumentbank.h" 0024 #include "skgservices.h" 0025 #include "skgtraces.h" 0026 0027 /** 0028 * This plugin factory. 0029 */ 0030 K_PLUGIN_CLASS_WITH_JSON(SKGImportPluginLedger, "metadata.json") 0031 0032 SKGImportPluginLedger::SKGImportPluginLedger(QObject* iImporter, const QVariantList& iArg) 0033 : SKGImportPlugin(iImporter) 0034 { 0035 SKGTRACEINFUNC(10) 0036 Q_UNUSED(iArg) 0037 0038 m_importParameters[QStringLiteral("ledger_account_identification")] = QStringLiteral("COMPTE,COMPTES,CAPITAUX,ASSETS,LIABILITIES,SAVING"); 0039 } 0040 0041 SKGImportPluginLedger::~SKGImportPluginLedger() 0042 = default; 0043 0044 bool SKGImportPluginLedger::isExportPossible() 0045 { 0046 SKGTRACEINFUNC(10) 0047 return (m_importer->getDocument() == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("LEDGER")); 0048 } 0049 0050 SKGError SKGImportPluginLedger::exportFile() 0051 { 0052 SKGError err; 0053 QSaveFile file(m_importer->getLocalFileName(false)); 0054 if (!file.open(QIODevice::WriteOnly)) { 0055 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Save file '%1' failed", m_importer->getFileName().toDisplayString())); 0056 } else { 0057 auto listUUIDs = SKGServices::splitCSVLine(m_exportParameters.value(QStringLiteral("uuid_of_selected_accounts_or_operations"))); 0058 0059 QString wc; 0060 for (const auto& uuid : qAsConst(listUUIDs)) { 0061 auto items = SKGServices::splitCSVLine(uuid, '-'); 0062 if (items.at(1) == QStringLiteral("operation")) { 0063 if (!wc.isEmpty()) { 0064 wc += QLatin1String(" AND "); 0065 } 0066 wc += " i_OPID=" + items.at(0); 0067 } else if (items.at(1) == QStringLiteral("account")) { 0068 if (!wc.isEmpty()) { 0069 wc += QLatin1String(" AND "); 0070 } 0071 wc += " rd_account_id=" + items.at(0); 0072 } 0073 } 0074 if (wc.isEmpty()) { 0075 wc = QStringLiteral("1=1"); 0076 } else { 0077 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Only selected accounts and transactions have been exported"))) 0078 } 0079 0080 QLocale en(QLocale::C); 0081 QTextStream stream(&file); 0082 if (!m_importer->getCodec().isEmpty()) { 0083 stream.setCodec(m_importer->getCodec().toLatin1().constData()); 0084 } 0085 stream << "; -*- ledger file generated by Skrooge -*-" << SKGENDL; 0086 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export %1 file", "ledger"), 2); 0087 IFOK(err) { 0088 auto punit = m_importer->getDocument()->getPrimaryUnit(); 0089 SKGObjectBase::SKGListSKGObjectBase units; 0090 err = m_importer->getDocument()->getObjects(QStringLiteral("v_unit"), QStringLiteral("t_type NOT IN ('C', '1', '2')"), units); 0091 int nb = units.count(); 0092 IFOK(err) { 0093 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export units"), nb); 0094 for (int i = 0; !err && i < nb; ++i) { 0095 SKGUnitObject unit(units.at(i)); 0096 QString qs = en.toCurrencyString(SKGServices::stringToDouble(unit.getAttribute(QStringLiteral("f_CURRENTAMOUNT"))), punit.Symbol, punit.NbDecimal); 0097 stream << "P " << SKGServices::dateToSqlString(QDate::currentDate()).replace('-', '/') 0098 << " \"" << unit.getSymbol() << '"' 0099 << " " << qs 0100 << SKGENDL; 0101 stream << SKGENDL; 0102 0103 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 0104 } 0105 0106 SKGENDTRANSACTION(m_importer->getDocument(), err) 0107 } 0108 } 0109 0110 IFOKDO(err, m_importer->getDocument()->stepForward(1)) 0111 0112 IFOK(err) { 0113 SKGObjectBase::SKGListSKGObjectBase transactions; 0114 err = m_importer->getDocument()->getObjects(QStringLiteral("v_operation"), wc % QStringLiteral(" AND t_template='N' ORDER BY d_date"), transactions); 0115 int nb = transactions.count(); 0116 IFOK(err) { 0117 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export transactions"), nb); 0118 for (int i = 0; !err && i < nb; ++i) { 0119 SKGOperationObject op(transactions.at(i)); 0120 auto status = op.getStatus(); 0121 auto number = op.getNumber(); 0122 0123 SKGPayeeObject payee; 0124 op.getPayee(payee); 0125 0126 SKGUnitObject unit; 0127 op.getUnit(unit); 0128 bool isCurrency = unit.getType() == SKGUnitObject::CURRENCY || unit.getType() == SKGUnitObject::PRIMARY || unit.getType() == SKGUnitObject::SECONDARY; 0129 0130 auto payeeString = payee.getName(); 0131 if (payeeString.isEmpty()) { 0132 payeeString = op.getComment(); 0133 } 0134 0135 auto nbDec = SKGServices::stringToInt(op.getAttribute(QStringLiteral("i_NBDEC"))); 0136 if (nbDec == 0) { 0137 nbDec = 2; 0138 } 0139 QString symbol = unit.getSymbol(); 0140 if (symbol.contains(QStringLiteral(" "))) { 0141 symbol = '"' + symbol + '"'; 0142 } 0143 QString qs = en.toCurrencyString(SKGServices::stringToDouble(op.getAttribute(QStringLiteral("f_QUANTITY"))), QStringLiteral(" "), nbDec); 0144 if (isCurrency) { 0145 qs = symbol + qs; 0146 } else { 0147 qs = qs + ' ' + symbol; 0148 } 0149 0150 stream << SKGServices::dateToSqlString(op.getDate()).replace('-', '/') 0151 << (status == SKGOperationObject::CHECKED ? " *" : status == SKGOperationObject::MARKED ? " !" : "") 0152 << (!number.isEmpty() ? QStringLiteral(" (") % number % ")" : QString()) 0153 << QStringLiteral(" ") << payeeString 0154 << SKGENDL; 0155 stream << " ; Skrooge ID: " << op.getID() << SKGENDL; 0156 stream << " ; Import ID: " << op.getImportID() << SKGENDL; 0157 auto properties = op.getProperties(); 0158 for (const auto& p : qAsConst(properties)) { 0159 stream << " ; " << p << ": " << op.getProperty(p) << SKGENDL; 0160 } 0161 stream << " " << i18nc("The default category for the accounts for ledger export", "Account") << ':' << op.getAttribute(QStringLiteral("t_ACCOUNT")) 0162 << " " << qs 0163 << SKGENDL; 0164 0165 SKGObjectBase::SKGListSKGObjectBase subtransactions; 0166 IFOKDO(err, op.getSubOperations(subtransactions)) 0167 int nbsuboperations = subtransactions.count(); 0168 for (int j = 0; !err && j < nbsuboperations; ++j) { 0169 SKGSubOperationObject sop(subtransactions.at(j)); 0170 SKGCategoryObject cat; 0171 sop.getCategory(cat); 0172 auto catString = cat.getFullName().replace(OBJECTSEPARATOR, QLatin1String(":")); 0173 if (catString.isEmpty()) { 0174 catString = i18nc("Category not defined", "Not defined"); 0175 } 0176 QString qs = en.toCurrencyString(-sop.getQuantity(), QStringLiteral(" "), nbDec); 0177 if (isCurrency) { 0178 qs = unit.getSymbol() + qs; 0179 } else { 0180 qs = qs + ' ' + unit.getSymbol(); 0181 } 0182 0183 stream << " " << i18nc("The default category for the categories for ledger export", "Category") << ':' << catString 0184 << " " << qs; 0185 if (sop.getDate() != op.getDate()) { 0186 stream << " ; [=" << SKGServices::dateToSqlString(sop.getDate()).replace('-', '/') << "]"; 0187 } 0188 0189 auto comment = sop.getComment(); 0190 if (!comment.isEmpty()) { 0191 stream << " ;comment=" << comment; 0192 } 0193 stream << " ; Skrooge ID: " << sop.getID(); 0194 stream << SKGENDL; 0195 } 0196 stream << SKGENDL; 0197 0198 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 0199 } 0200 0201 SKGENDTRANSACTION(m_importer->getDocument(), err) 0202 } 0203 } 0204 0205 IFOKDO(err, m_importer->getDocument()->stepForward(2)) 0206 0207 SKGENDTRANSACTION(m_importer->getDocument(), err) 0208 0209 // Close file 0210 file.commit(); 0211 } 0212 return err; 0213 } 0214 0215 bool SKGImportPluginLedger::isImportPossible() 0216 { 0217 SKGTRACEINFUNC(10) 0218 return (m_importer->getDocument() == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("LEDGER")); 0219 } 0220 0221 bool SKGImportPluginLedger::isAccount(const QString& type) 0222 { 0223 return m_importParameters.value(QStringLiteral("ledger_account_identification")).split(',').indexOf(type) != -1; 0224 } 0225 0226 SKGError SKGImportPluginLedger::importFile() 0227 { 0228 if (m_importer->getDocument() == nullptr) { 0229 return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters")); 0230 } 0231 SKGError err; 0232 SKGTRACEINFUNCRC(2, err) 0233 0234 // Initialisation 0235 // Generate xml 0236 QString uniqueId = QUuid::createUuid().toString(); 0237 QString temporaryPath = QDir::tempPath() % "/" % uniqueId % ".xml"; 0238 QString cmd = "ledger -f \"" % m_importer->getLocalFileName() % "\" xml --output " % temporaryPath; 0239 SKGTRACEL(10) << "Execution of :" << cmd << SKGENDL; 0240 QProcess p; 0241 p.start(QStringLiteral("/bin/bash"), QStringList() << QStringLiteral("-c") << cmd); 0242 if (p.waitForFinished(1000 * 60 * 5) && p.exitCode() == 0) { 0243 // Open file 0244 QFile file(temporaryPath); 0245 if (!file.open(QIODevice::ReadOnly)) { 0246 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString())); 0247 } else { 0248 QDomDocument doc; 0249 0250 // Set the file 0251 QString errorMsg; 0252 int errorLine = 0; 0253 int errorCol = 0; 0254 bool contentOK = doc.setContent(file.readAll(), &errorMsg, &errorLine, &errorCol); 0255 file.close(); 0256 0257 // Get root 0258 QDomElement docElem = doc.documentElement(); 0259 if (!contentOK) { 0260 err.setReturnCode(ERR_ABORT).setMessage(i18nc("Error message", "%1-%2: '%3'", errorLine, errorCol, errorMsg)); 0261 err.addError(ERR_INVALIDARG, i18nc("Error message", "Invalid XML content in file '%1'", m_importer->getFileName().toDisplayString())); 0262 } else { 0263 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import %1 file", "LEDGER"), 2); 0264 0265 QMap<QString, SKGUnitObject> mapIdUnit; 0266 QMap<QString, SKGAccountObject> mapIdAccount; 0267 QMap<QString, SKGCategoryObject> mapIdCategory; 0268 QMap<QString, SKGPayeeObject> mapIdPayee; 0269 0270 // Step 1-Create units 0271 IFOK(err) { 0272 auto commodityL = docElem.elementsByTagName(QStringLiteral("commodity")); 0273 int nb = commodityL.count(); 0274 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import units"), nb); 0275 for (int i = 0; !err && i < nb; ++i) { 0276 // Get unit object 0277 auto commodity = commodityL.at(i).toElement(); 0278 auto symbol = commodity.firstChildElement(QStringLiteral("symbol")).toElement().text(); 0279 0280 // Creation of the units 0281 SKGUnitObject unitObj(m_importer->getDocument()); 0282 IFOKDO(err, unitObj.setName(symbol)) 0283 IFOKDO(err, unitObj.setSymbol(symbol)) 0284 IFOKDO(err, unitObj.setNumberDecimal(2)) 0285 IFOKDO(err, unitObj.save()) 0286 0287 // Creation of the unit values 0288 auto annotation = commodity.firstChildElement(QStringLiteral("annotation")).toElement(); 0289 auto price = annotation.firstChildElement(QStringLiteral("price")).toElement(); 0290 auto commodity2 = price.firstChildElement(QStringLiteral("commodity")).toElement(); 0291 auto quantity = price.firstChildElement(QStringLiteral("quantity")).toElement().text().trimmed(); 0292 auto symbol2 = commodity2.firstChildElement(QStringLiteral("symbol")).toElement(); 0293 auto date = annotation.firstChildElement(QStringLiteral("date")).toElement().text().trimmed(); 0294 if (!date.isNull() && !symbol2.isNull() && !quantity.isNull()) { 0295 SKGUnitValueObject unitValueObj; 0296 IFOKDO(err, unitObj.addUnitValue(unitValueObj)) 0297 IFOKDO(err, unitValueObj.setDate(QDate::fromString(date, QStringLiteral("yyyy/MM/dd")))) 0298 IFOKDO(err, unitValueObj.setQuantity(1.0 / SKGServices::stringToDouble(quantity))) 0299 IFOKDO(err, unitValueObj.save()) 0300 } 0301 0302 mapIdUnit[symbol] = unitObj; 0303 0304 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 0305 } 0306 0307 SKGENDTRANSACTION(m_importer->getDocument(), err) 0308 } 0309 IFOKDO(err, m_importer->getDocument()->stepForward(1)) 0310 0311 // Step 2-Create transaction 0312 IFOK(err) { 0313 auto transactionL = docElem.elementsByTagName(QStringLiteral("transaction")); 0314 int nb = transactionL.count(); 0315 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import transactions"), nb); 0316 for (int i = 0; !err && i < nb; ++i) { 0317 // Get account object 0318 auto transaction = transactionL.at(i).toElement(); 0319 0320 auto date = transaction.firstChildElement(QStringLiteral("date")).toElement().text().trimmed(); 0321 auto payee = transaction.firstChildElement(QStringLiteral("payee")).toElement().text().trimmed(); 0322 auto note = transaction.firstChildElement(QStringLiteral("note")).toElement().text().trimmed(); 0323 auto status = getAttribute(transaction, QStringLiteral("state")); 0324 0325 // Create transaction and suboperation 0326 SKGOperationObject opObj; 0327 SKGSubOperationObject subObj; 0328 auto postingL = transaction.elementsByTagName(QStringLiteral("posting")); 0329 0330 auto symbol = postingL.at(0).toElement().elementsByTagName(QStringLiteral("symbol")).at(0).toElement().text().trimmed(); 0331 int nb2 = postingL.count(); 0332 0333 //Put account in the first place 0334 QList<QDomElement> list_porting; 0335 for (int i2 = 0; !err && i2 < nb2; ++i2) { 0336 auto posting = postingL.at(i2).toElement(); 0337 auto account = posting.firstChildElement(QStringLiteral("account")).toElement(); 0338 auto type = account.firstChildElement(QStringLiteral("name")).toElement().text().split(QStringLiteral(":"))[0].toUpper().trimmed(); 0339 0340 if (isAccount(type)) { 0341 list_porting.insert(0, posting); 0342 } else { 0343 list_porting.append(posting); 0344 } 0345 } 0346 SKGOperationObject opPreviousObj; 0347 for (int i2 = 0; !err && i2 < nb2; ++i2) { 0348 auto posting = list_porting.at(i2); 0349 auto account = posting.firstChildElement(QStringLiteral("account")).toElement(); 0350 auto name = account.firstChildElement(QStringLiteral("name")).toElement().text().trimmed(); 0351 auto snote = posting.firstChildElement(QStringLiteral("note")).toElement().text().trimmed(); 0352 0353 auto postamount = posting.firstChildElement(QStringLiteral("post-amount")).toElement(); 0354 0355 auto amount = postamount.firstChildElement(QStringLiteral("amount")).toElement(); 0356 auto quantity = amount.firstChildElement(QStringLiteral("quantity")).toElement().text().trimmed(); 0357 0358 auto names = name.split(QStringLiteral(":")); 0359 QString type; 0360 if (names.length() > 1) { 0361 type = names[0].toUpper(); 0362 name = name.right(name.length() - type.length() - 1); 0363 } 0364 SKGTRACEL(2) << "Sub transaction : " << name << ": " << date << ": " << payee << " | " << quantity << SKGENDL; 0365 0366 auto isaccount = isAccount(type); 0367 if (i2 > 0) { 0368 if (isaccount) { 0369 // Save the subtransaction 0370 IFOKDO(err, subObj.save()) 0371 } else { 0372 // Forget the subtransaction 0373 IFOKDO(err, opObj.load()) 0374 } 0375 } 0376 if (isaccount || !opObj.exist()) { 0377 auto account_id = getAttribute(account, QStringLiteral("ref")); 0378 0379 SKGAccountObject accountObj; 0380 if (!mapIdAccount.contains(account_id)) { 0381 auto err2 = m_importer->getDocument()->getObject(QStringLiteral("v_account"), "t_name='" % SKGServices::stringToSqlString(name) % '\'', accountObj); 0382 if (!!err2) { 0383 SKGBankObject bankDefault(m_importer->getDocument()); 0384 IFOKDO(err, bankDefault.setName(QStringLiteral("LEDGER"))) 0385 IFOKDO(err, bankDefault.save()) 0386 IFOK(err) { 0387 IFOKDO(err, bankDefault.addAccount(accountObj)) 0388 IFOKDO(err, accountObj.setName(name)) 0389 IFOKDO(err, accountObj.save()) 0390 mapIdAccount[account_id] = accountObj; 0391 } 0392 } 0393 } else { 0394 accountObj = mapIdAccount[account_id]; 0395 } 0396 0397 // Creation of the transaction 0398 IFOKDO(err, accountObj.addOperation(opObj, true)) 0399 IFOKDO(err, opObj.setDate(QDate::fromString(date, QStringLiteral("yyyy/MM/dd")))) 0400 0401 IFOKDO(err, opObj.setUnit(mapIdUnit[symbol])) 0402 if (!payee.isEmpty()) { 0403 SKGPayeeObject payeeObject; 0404 if (!mapIdPayee.contains(payee)) { 0405 IFOKDO(err, SKGPayeeObject::createPayee(m_importer->getDocument(), payee, payeeObject)) 0406 mapIdPayee[payee] = payeeObject; 0407 } else { 0408 payeeObject = mapIdPayee[payee]; 0409 } 0410 0411 IFOKDO(err, opObj.setPayee(payeeObject)) 0412 } 0413 IFOKDO(err, opObj.setComment(note)) 0414 IFOKDO(err, opObj.setImported(true)) 0415 IFOKDO(err, opObj.setImportID(QStringLiteral("LEDGER-"))) 0416 IFOKDO(err, opObj.setStatus(status == QStringLiteral("cleared") ? SKGOperationObject::CHECKED : 0417 status == QStringLiteral("pending") ? SKGOperationObject::MARKED : SKGOperationObject::NONE)) 0418 IFOKDO(err, opObj.save()) 0419 0420 if (opPreviousObj.getID()) { 0421 IFOKDO(err, opPreviousObj.setGroupOperation(opObj)); 0422 IFOKDO(err, opPreviousObj.save()) 0423 } else { 0424 opPreviousObj = opObj; 0425 } 0426 } 0427 0428 // Creation of the subtransaction 0429 IFOKDO(err, opObj.addSubOperation(subObj)) 0430 if (!isaccount) { 0431 SKGCategoryObject catObj; 0432 if (!mapIdCategory.contains(name)) { 0433 IFOKDO(err, SKGCategoryObject::createPathCategory(m_importer->getDocument(), name.replace(QStringLiteral(":"), QStringLiteral(" > ")), catObj)) 0434 mapIdCategory[name] = catObj; 0435 } else { 0436 catObj = mapIdCategory[name]; 0437 } 0438 IFOKDO(err, subObj.setCategory(catObj)) 0439 } 0440 if (snote.startsWith(QStringLiteral("[=")) && snote.endsWith(QStringLiteral("]"))) { 0441 IFOKDO(err, subObj.setDate(QDate::fromString(snote.mid(2, snote.length() - 3), QStringLiteral("yyyy/MM/dd")))) 0442 } else { 0443 IFOKDO(err, subObj.setComment(snote)) 0444 } 0445 IFOKDO(err, subObj.setQuantity((isaccount ? 1 : -1)*SKGServices::stringToDouble(quantity))) 0446 if (!isaccount || i2 == nb2 - 1) { 0447 IFOKDO(err, subObj.save()) 0448 } 0449 } 0450 0451 if (!err && i % 500 == 0) { 0452 err = m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE")); 0453 } 0454 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 0455 } 0456 0457 SKGENDTRANSACTION(m_importer->getDocument(), err) 0458 } 0459 IFOKDO(err, m_importer->getDocument()->stepForward(2)) 0460 SKGENDTRANSACTION(m_importer->getDocument(), err) 0461 0462 IFOKDO(err, m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE"))) 0463 } 0464 } 0465 } else { 0466 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "The execution of '%1' failed", cmd)).addError(ERR_FAIL, i18nc("Error message", "The ledger conversion in xml of '%1' failed", m_importer->getFileName().toDisplayString())); 0467 } 0468 return err; 0469 } 0470 0471 QString SKGImportPluginLedger::getMimeTypeFilter() const 0472 { 0473 return "*.ledger|" % i18nc("A file format", "Ledger file"); 0474 } 0475 0476 QString SKGImportPluginLedger::getAttribute(const QDomElement& iElement, const QString& iAttribute) 0477 { 0478 QString val = iElement.attribute(iAttribute); 0479 if (val == QStringLiteral("(null)")) { 0480 val = QString(); 0481 } 0482 return val; 0483 } 0484 #include <skgimportpluginledger.moc>