1 /***************************************************************************
2 * SPDX-FileCopyrightText: 2021 S. MANKOWSKI stephane@mankowski.fr
3 * SPDX-FileCopyrightText: 2021 G. DE BURE support@mankowski.fr
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 ***************************************************************************/
6 /** @file
7 * This file is Skrooge plugin for QIF import / export.
8 *
9 * @author Stephane MANKOWSKI / Guillaume DE BURE
10 */
11 #include "skgimportpluginqif.h"
12
13 #include <klocalizedstring.h>
14
15 #include <kpluginfactory.h>
16
17 #include <qcryptographichash.h>
18 #include <qfile.h>
19 #include <qsavefile.h>
20
21 #include "skgbankincludes.h"
22 #include "skgimportexportmanager.h"
23 #include "skgservices.h"
24 #include "skgtraces.h"
25
26 /**
27 * Opening balance string
28 */
29 #define OPENINGBALANCE QStringLiteral("Opening Balance")
30
31 /**
32 * This plugin factory.
33 */
K_PLUGIN_FACTORY(SKGImportPluginQifFactory,registerPlugin<SKGImportPluginQif> ();)34 K_PLUGIN_FACTORY(SKGImportPluginQifFactory, registerPlugin<SKGImportPluginQif>();)
35
36 SKGImportPluginQif::SKGImportPluginQif(QObject* iImporter, const QVariantList& iArg)
37 : SKGImportPlugin(iImporter)
38 {
39 SKGTRACEINFUNC(10)
40 Q_UNUSED(iArg)
41
42 m_importParameters[QStringLiteral("date_format")] = QString();
43 m_exportParameters[QStringLiteral("uuid_of_selected_accounts_or_operations")] = QString();
44 }
45
46 SKGImportPluginQif::~SKGImportPluginQif()
47 = default;
48
isImportPossible()49 bool SKGImportPluginQif::isImportPossible()
50 {
51 SKGTRACEINFUNC(10)
52 return isExportPossible();
53 }
54
importFile()55 SKGError SKGImportPluginQif::importFile()
56 {
57 if (m_importer == nullptr) {
58 return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters"));
59 }
60
61 SKGError err;
62 SKGTRACEINFUNCRC(2, err)
63
64 // Begin transaction
65 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import %1 file", "QIF"), 3);
66 IFOK(err) {
67 // Create account if needed
68 QDateTime now = QDateTime::currentDateTime();
69 QString postFix = SKGServices::dateToSqlString(now);
70
71 // Step 1 done
72 IFOKDO(err, m_importer->getDocument()->stepForward(1))
73
74 // Open file
75 QFile file(m_importer->getLocalFileName());
76 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
77 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString()));
78 } else {
79 QTextStream stream(&file);
80 if (!m_importer->getCodec().isEmpty()) {
81 stream.setCodec(m_importer->getCodec().toLatin1().constData());
82 }
83
84 // load file in memory
85 QStringList lines;
86 QStringList dates;
87 bool inWrongSection = false;
88 bool inPriceSection = false;
89 while (!stream.atEnd()) {
90 // Read line
91 // Check line if line is empty or is a commented
92 QString line = stream.readLine().trimmed().toUtf8();
93 if (!line.isEmpty() && line[0] != '#') {
94 lines.push_back(line);
95 // Manage !Account section
96 if (line.startsWith(QLatin1String("!"))) {
97 inWrongSection = false;
98 inPriceSection = false;
99 }
100
101 if (QString::compare(line, QStringLiteral("!account"), Qt::CaseInsensitive) == 0 ||
102 QString::compare(line, QStringLiteral("!type:cat"), Qt::CaseInsensitive) == 0 ||
103 QString::compare(line, QStringLiteral("!type:tag"), Qt::CaseInsensitive) == 0 ||
104 QString::compare(line, QStringLiteral("!type:class"), Qt::CaseInsensitive) == 0) {
105 inWrongSection = true;
106 } else if (QString::compare(line, QStringLiteral("!type:prices"), Qt::CaseInsensitive) == 0) {
107 inPriceSection = true;
108 }
109
110 // We try to find automatically the date format
111 if (!inWrongSection && line[0] == 'D') {
112 dates.push_back(line.right(line.length() - 1));
113 } else if (inPriceSection) {
114 QStringList vals = SKGServices::splitCSVLine(line, ',');
115 if (vals.count() == 3) {
116 dates.push_back(vals.at(2));
117 }
118 }
119 }
120 }
121
122 // close file
123 file.close();
124
125 // Select dateformat
126 QString dateFormat = m_importParameters.value(QStringLiteral("date_format"));
127 if (dateFormat.isEmpty()) {
128 dateFormat = SKGServices::getDateFormat(dates); // Automatic detection
129 }
130 if (dateFormat.isEmpty()) {
131 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "Date format not supported"));
132 }
133 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Import of '%1' with code '%2' and date format '%3'", m_importer->getFileName().toDisplayString(), m_importer->getCodec(), dateFormat)))
134
135 // Step 2 done
136 IFOKDO(err, m_importer->getDocument()->stepForward(2))
137
138 // Treat all lines
139 IFOK(err) {
140 SKGAccountObject* account = nullptr;
141 SKGOperationObject currentOperation;
142 SKGOperationObject payement;
143 SKGPayeeObject currentPayee;
144 SKGTrackerObject currentTracker;
145 SKGUnitObject currentUnit;
146 SKGSubOperationObject currentSubOperation;
147 QDate currentOperationDate;
148 QString lastTransferAccount;
149 QList<QString> transferAccount;
150 QList<double> transferQuantity;
151 bool addNextAmountToTransferQuantity = false;
152 QString stringForHash;
153 QString currentUnitForInvestment;
154 QChar inSection = 'B';
155 bool currentOperationInitialized = false;
156 bool latestSubCatMustBeRemoved = false;
157 bool investmentAccount = false;
158 bool div = false;
159 bool automaticAccount = true;
160 int quantityFactor = 1;
161 double currentUnitPrice = 1;
162 double checkOperationAmount = 0;
163 double checkSuboperationsAmount = 0;
164 bool openingbalancecreated = false;
165
166 int nb = lines.size();
167 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import operations"), nb);
168 for (int i = 0; !err && i < nb; ++i) {
169 QString line = lines.at(i);
170 QString val;
171 QChar op = line[0];
172 if (line.length() > 1) {
173 val = line.right(line.length() - 1).trimmed();
174 }
175
176 // Manage !Account section
177 if (QString::compare(line, QStringLiteral("!type:bank"), Qt::CaseInsensitive) == 0 ||
178 QString::compare(line, QStringLiteral("!type:cash"), Qt::CaseInsensitive) == 0 ||
179 QString::compare(line, QStringLiteral("!type:ccard"), Qt::CaseInsensitive) == 0 ||
180 QString::compare(line, QStringLiteral("!type:oth a"), Qt::CaseInsensitive) == 0 ||
181 QString::compare(line, QStringLiteral("!type:oth l"), Qt::CaseInsensitive) == 0 ||
182 QString::compare(line, QStringLiteral("!type:invst"), Qt::CaseInsensitive) == 0) {
183 inSection = 'B';
184 openingbalancecreated = false;
185 investmentAccount = (QString::compare(val, QStringLiteral("type:invst"), Qt::CaseInsensitive) == 0);
186
187 // Set type of account
188 if (account == nullptr) {
189 SKGAccountObject defAccount;
190 err = m_importer->getDefaultAccount(defAccount);
191 IFOKDO(err, defAccount.addOperation(currentOperation, true))
192 IFOK(err) account = new SKGAccountObject(defAccount);
193 }
194
195 if (!err && (account != nullptr)) {
196 err = account->setType(QString::compare(line, QStringLiteral("!type:bank"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::CURRENT :
197 (QString::compare(line, QStringLiteral("!type:ccard"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::CREDITCARD :
198 (QString::compare(line, QStringLiteral("!type:invst"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::INVESTMENT :
199 (QString::compare(line, QStringLiteral("!type:oth a"), Qt::CaseInsensitive) == 0 ? SKGAccountObject::ASSETS : SKGAccountObject::OTHER))));
200 IFOKDO(err, account->save())
201 }
202 } else if (QString::compare(line, QStringLiteral("!account"), Qt::CaseInsensitive) == 0) {
203 inSection = 'A';
204 openingbalancecreated = false;
205 automaticAccount = false;
206 } else if (QString::compare(line, QStringLiteral("!type:cat"), Qt::CaseInsensitive) == 0) {
207 inSection = 'C';
208 openingbalancecreated = false;
209 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Categories found and imported")))
210 } else if (QString::compare(line, QStringLiteral("!type:prices"), Qt::CaseInsensitive) == 0) {
211 inSection = 'U';
212 openingbalancecreated = false;
213 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Units prices found and imported")))
214 } else if (QString::compare(line, QStringLiteral("!type:security"), Qt::CaseInsensitive) == 0) {
215 inSection = 'S';
216 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Units found and imported")))
217 } else if (QString::compare(line, QStringLiteral("!type:tag"), Qt::CaseInsensitive) == 0) {
218 inSection = 'T';
219 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Trackers found and imported")))
220 } else if (line.at(0) == '!') {
221 inSection = '?';
222 openingbalancecreated = false;
223 } else if (inSection == 'U') {
224 // Unit value creation
225 openingbalancecreated = false;
226 QStringList vals = SKGServices::splitCSVLine(line, ',');
227 if (vals.count() == 3 && !vals.at(0).isEmpty()) {
228 err = m_importer->getDocument()->addOrModifyUnitValue(vals.at(0), SKGServices::stringToTime(SKGServices::dateToSqlString(vals.at(2), dateFormat)).date(), SKGServices::stringToDouble(vals.at(1)));
229 }
230
231 } else if (inSection == 'T') {
232 // Tracker creation
233 if (op == 'N') {
234 IFOKDO(err, SKGTrackerObject::createTracker(m_importer->getDocument(), val, currentTracker))
235 } else if (op == 'D') {
236 IFOKDO(err, currentTracker.setComment(val))
237 IFOKDO(err, currentTracker.save())
238 }
239 } else if (inSection == 'S') {
240 // Unit creation
241 if (op == 'N') {
242 currentUnit = SKGUnitObject(m_importer->getDocument());
243 IFOKDO(err, currentUnit.setName(val))
244 IFOKDO(err, currentUnit.setSymbol(val))
245 IFOKDO(err, currentUnit.setType(SKGUnitObject::CURRENCY))
246 IFOKDO(err, currentUnit.setNumberDecimal(2))
247 IFOKDO(err, currentUnit.save())
248 } else if (op == 'S') {
249 IFOKDO(err, currentUnit.setSymbol(val))
250 IFOKDO(err, currentUnit.save())
251 } else if (op == 'T') {
252 if (QString::compare(val, QStringLiteral("stock"), Qt::CaseInsensitive) == 0) {
253 IFOKDO(err, currentUnit.setType(SKGUnitObject::SHARE))
254 IFOKDO(err, currentUnit.setNumberDecimal(4))
255 IFOKDO(err, currentUnit.save())
256 }
257 }
258 } else if (inSection == 'C') {
259 // Category creation
260 openingbalancecreated = false;
261 if (op == 'N') {
262 SKGCategoryObject Category;
263 val.replace('/', OBJECTSEPARATOR);
264 val.replace(':', OBJECTSEPARATOR);
265 err = SKGCategoryObject::createPathCategory(m_importer->getDocument(), val, Category);
266 }
267 } else if (inSection == 'A') {
268 // Account creation
269 openingbalancecreated = false;
270 if (op == 'N') {
271 // Check if the account already exist
272 SKGAccountObject account2;
273 err = SKGNamedObject::getObjectByName(m_importer->getDocument(), QStringLiteral("account"), val, account2);
274 IFKO(err) {
275 // Create account
276 SKGBankObject bank(m_importer->getDocument());
277 err = bank.setName(i18nc("Noun", "Bank for import %1", postFix));
278 if (!err && bank.load().isFailed()) {
279 err = bank.save(false);
280 }
281 IFOKDO(err, bank.addAccount(account2))
282 IFOKDO(err, account2.setName(val))
283 if (!err && account2.load().isFailed()) {
284 err = account2.save(false); // Save only
285 }
286 }
287
288 IFOK(err) {
289 delete account;
290 account = new SKGAccountObject(account2);
291 }
292 } else if (op == 'D') {
293 if (account != nullptr) {
294 err = account->setNumber(val);
295 }
296 } else if (op == 'T') {
297 if (account != nullptr) {
298 err = account->setType(val == QStringLiteral("Bank") ? SKGAccountObject::CURRENT : (val == QStringLiteral("CCard") ? SKGAccountObject::CREDITCARD : (val == QStringLiteral("Invst") ? SKGAccountObject::INVESTMENT : (val == QStringLiteral("Oth A") ? SKGAccountObject::ASSETS : SKGAccountObject::OTHER))));
299 }
300 } else if (op == '^') {
301 // ^ End of entry
302 // save
303 if (account != nullptr) {
304 err = account->save();
305 }
306 }
307 } else if (inSection == 'B') {
308 // Operation creation
309 /*
310 >>>> Items for Non-Investment Accounts <<<<
311 DONE D Date
312 DONE T Amount
313 U Transaction amount (higher possible value than T)
314 DONE C Cleared status
315 DONE N Number (check or reference number)
316 DONE P Payee/description
317 DONE M Memo
318 DONE A Address (up to 5 lines; 6th line is an optional message)
319 DONE L Category (category/class or transfer/class)
320 DONE S Category in split (category/class or transfer/class)
321 DONE E Memo in split
322 DONE $ Dollar amount of split
323 % Percentage of split if percentages are used
324 F Reimbursable business expense flag
325 X Small Business extensions
326 DONE ^ End of entry
327
328 >>>> Items for Investment Accounts <<<<
329 DONE D Date
330 N Action
331 DONE Y Security
332 DONE I Price
333 DONE Q Quantity (number of shares or split ratio)
334 DONE T Transaction amount
335 DONE C Cleared status
336 P Text in the first line for transfers and reminders
337 DONE M Memo
338 O Commission
339 L Account for the transfer
340 $ Amount transferred
341 ^ End of entry
342 */
343 stringForHash += line;
344 if (op == 'D') {
345 // D Date
346 /*
347 Dates in US QIF files are usually in the format MM/DD/YY, although
348 four-digit years are not uncommon. Dates sometimes occur without the
349 slash separator, or using other separators in place of the slash,
350 commonly '-' and '.'. US Quicken seems to be using the ' to indicate
351 post-2000 two-digit years (such as 01/01'00 for Jan 1 2000). Some
352 banks appear to be using a completely undifferentiated numeric QString
353 formateed YYYYMMDD in downloaded QIF files.
354 */
355 // Operation creation
356 SKGUnitObject unit;
357 IFOK(err) {
358 if (account != nullptr) {
359 err = account->addOperation(currentOperation, true);
360 if (!openingbalancecreated) {
361 double initBalance;
362 account->getInitialBalance(initBalance, unit);
363 }
364 } else {
365 SKGAccountObject defAccount;
366 err = m_importer->getDefaultAccount(defAccount);
367 IFOKDO(err, defAccount.addOperation(currentOperation, true))
368 if (!openingbalancecreated) {
369 double initBalance;
370 defAccount.getInitialBalance(initBalance, unit);
371 }
372 }
373 currentOperationInitialized = true;
374 }
375
376 // Set date
377 currentOperationDate = SKGServices::stringToTime(SKGServices::dateToSqlString(val, dateFormat)).date();
378 IFOKDO(err, currentOperation.setDate(currentOperationDate))
379
380 // Set unit
381 IFOK(err) {
382 // Create unit if needed
383 // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
384 if (!unit.exist()) {
385 err = m_importer->getDefaultUnit(unit, ¤tOperationDate);
386 }
387 IFOKDO(err, currentOperation.setUnit(unit))
388 }
389
390 IFOK(err) currentOperation.save();
391
392 // Create suboperation
393 IFOKDO(err, currentOperation.addSubOperation(currentSubOperation))
394 } else if (op == 'Y') {
395 // Y Security
396 if (!div) {
397 currentUnitForInvestment = val;
398
399 SKGUnitObject unit(m_importer->getDocument());
400 if (currentUnitForInvestment.isEmpty()) {
401 IFOKDO(err, err = m_importer->getDefaultUnit(unit))
402 } else {
403 IFOKDO(err, unit.setName(currentUnitForInvestment))
404 IFOKDO(err, unit.setSymbol(currentUnitForInvestment))
405 if (unit.load().isFailed()) {
406 IFOKDO(err, unit.setType(investmentAccount ? SKGUnitObject::SHARE : SKGUnitObject::CURRENCY))
407 IFOKDO(err, unit.save(false))
408 }
409 }
410 IFOKDO(err, currentOperation.setUnit(unit))
411 } else {
412 // For dividend, if comment is empty, we set the security in comment
413 if (currentOperation.getComment().isEmpty()) {
414 err = currentOperation.setComment(val);
415 }
416 }
417 } else if (op == 'O') {
418 // O Commission
419 // Get previous quantity
420 double quantity = SKGServices::stringToDouble(val);
421 SKGObjectBase::SKGListSKGObjectBase subops;
422 payement.getSubOperations(subops);
423 if (!subops.isEmpty()) {
424 SKGSubOperationObject subpayement(subops.at(0));
425 err = subpayement.setQuantity(subpayement.getQuantity() + quantity);
426 IFOKDO(err, subpayement.save())
427 }
428
429 SKGSubOperationObject subcommission;
430 if (!payement.exist()) {
431 // We have to create a new operation
432 if (account != nullptr) {
433 err = account->addOperation(payement, true);
434 } else {
435 SKGAccountObject defAccount;
436 err = m_importer->getDefaultAccount(defAccount);
437 IFOKDO(err, defAccount.addOperation(payement, true))
438 }
439 IFOKDO(err, payement.setDate(currentOperationDate))
440 IFOK(err) {
441 // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
442 SKGUnitObject unit;
443 if ((account != nullptr) && !openingbalancecreated) {
444 double initBalance;
445 account->getInitialBalance(initBalance, unit);
446 }
447 if (!unit.exist()) {
448 err = m_importer->getDefaultUnit(unit, ¤tOperationDate);
449 }
450 IFOKDO(err, payement.setUnit(unit))
451 }
452 IFOKDO(err, payement.save())
453 }
454 IFOKDO(err, payement.addSubOperation(subcommission))
455 IFOKDO(err, subcommission.setQuantity(-quantity))
456 IFOKDO(err, subcommission.save(false, false))
457 } else if (op == 'I') {
458 // I Price
459 currentUnitPrice = SKGServices::stringToDouble(val);
460 if ((currentUnitPrice != 0.0) && !currentUnitForInvestment.isEmpty()) {
461 err = m_importer->getDocument()->addOrModifyUnitValue(currentUnitForInvestment, currentOperationDate, currentUnitPrice);
462 }
463 } else if (op == 'N') {
464 if (investmentAccount) {
465 // N Action
466 /*
467 QIF N Line Notes
468 ============ =====
469 Aktab Same as ShrsOut.
470 AktSplit Same as StkSplit.
471 Aktzu Same as ShrsIn.
472 Buy Buy shares.
473 BuyX Buy shares. Used with an L line.
474 Cash Miscellaneous cash transaction. Used with an L line.
475 CGMid Mid-term capital gains.
476 CGMidX Mid-term capital gains. For use with an L line.
477 CGLong Long-term capital gains.
478 CGLongX Long-term capital gains. For use with an L line.
479 CGShort Short-term capital gains.
480 CGShortX Short-term capital gains. For use with an L line.
481 ContribX Same as XIn. Used for tax-advantaged accounts.
482 CvrShrt Buy shares to cover a short sale.
483 CvrShrtX Buy shares to cover a short sale. Used with an L line.
484 Div Dividend received.
485 DivX Dividend received. For use with an L line.
486 Errinerg Same as Reminder.
487 Exercise Exercise an option.
488 ExercisX Exercise an option. For use with an L line.
489 Expire Mark an option as expired. (Uses D, N, Y & M lines)
490 Grant Receive a grant of stock options.
491 Int Same as IntInc.
492 IntX Same as IntIncX.
493 IntInc Interest received.
494 IntIncX Interest received. For use with an L line.
495 K.gewsp Same as CGShort. (German)
496 K.gewspX Same as CGShortX. (German)2307068
497 Kapgew Same as CGLong. Kapitalgewinnsteuer.(German)
498 KapgewX Same as CGLongX. Kapitalgewinnsteuer. (German)
499 Kauf Same as Buy. (German)
500 KaufX Same as BuyX. (German)
501 MargInt Margin interest paid.
502 MargIntX Margin interest paid. For use with an L line.
503 MiscExp Miscellaneous expense.
504 MiscExpX Miscellaneous expense. For use with an L line.
505 MiscInc Miscellaneous income.
506 MiscIncX Miscellaneous income. For use with an L line.
507 ReinvDiv Reinvested dividend.
508 ReinvInt Reinvested interest.
509 ReinvLG Reinvested long-term capital gains.
510 Reinvkur Same as ReinvLG.
511 Reinvksp Same as ReinvSh.
512 ReinvMd Reinvested mid-term capital gains.
513 ReinvSG Same as ReinvSh.
514 ReinvSh Reinvested short-term capital gains.
515 Reinvzin Same as ReinvDiv.
516 Reminder Reminder. (Uses D, N, C & M lines)
517 RtrnCap Return of capital.
518 RtrnCapX Return of capital. For use with an L line.
519 Sell Sell shares.
520 SellX Sell shares. For use with an L line.
521 ShtSell Short sale.
522 ShrsIn Deposit shares.
523 ShrsOut Withdraw shares.
524 StkSplit Share split.
525 Verkauf Same as Sell. (German)
526 VerkaufX Same as SellX. (German)
527 Vest Mark options as vested. (Uses N, Y, Q, C & M lines)
528 WithDrwX Same as XOut. Used for tax-advantaged accounts.
529 XIn Transfer cash from another account.
530 XOut Transfer cash to another account.
531 */
532 val = val.toLower();
533 if (val.contains(QStringLiteral("div")) && val != QStringLiteral("reinvdiv")) {
534 // TODO(Stephane MANKOWSKI) err=currentOperation.setProperty ( "SKG_OP_ORIGINAL_AMOUNT", "" );
535 div = true;
536 } else if (val.contains(QStringLiteral("sell")) ||
537 val.contains(QStringLiteral("verkauf")) ||
538 val.contains(QStringLiteral("miscexp")) ||
539 val.contains(QStringLiteral("shrsout"))
540 ) {
541 quantityFactor = -1;
542 }
543 // Correction 214851 vvvv
544 // err=currentOperation.setComment ( val );
545 // if ( !err ) err=currentOperation.setMode ( i18nc ( "Noun, the title of an item","Title" ) );
546 // Correction 214851 ^^^^
547 } else {
548 // N Num (check or reference number)
549 // Set number
550 bool ok;
551 int number = val.toInt(&ok);
552 if (ok && number != 0) {
553 err = currentOperation.setNumber(val);
554 } else {
555 err = currentOperation.setMode(val);
556 }
557 }
558 } else if (op == 'Q') {
559 // Q Quantity (number of shares or split ratio)
560 // Set value
561 if (!val.isEmpty()) {
562 double previousQuantity = currentSubOperation.getQuantity();
563 if (previousQuantity != 0.0) {
564 // We have to create a new operation
565 if (account != nullptr) {
566 err = account->addOperation(payement, true);
567 } else {
568 SKGAccountObject defAccount;
569 err = m_importer->getDefaultAccount(defAccount);
570 IFOKDO(err, defAccount.addOperation(payement, true))
571 }
572 IFOKDO(err, payement.setDate(currentOperationDate))
573 IFOK(err) {
574 // Create unit if needed
575 // If an initial balance is existing for the account then we use the unit else we look for the most appropriate unit
576 SKGUnitObject unit;
577 if ((account != nullptr) && !openingbalancecreated) {
578 double initBalance;
579 account->getInitialBalance(initBalance, unit);
580 }
581 if (!unit.exist()) {
582 err = m_importer->getDefaultUnit(unit, ¤tOperationDate);
583 }
584 IFOKDO(err, payement.setUnit(unit))
585 }
586 IFOKDO(err, payement.save())
587 IFOKDO(err, currentOperation.setGroupOperation(payement))
588
589 SKGSubOperationObject subpayement;
590 IFOKDO(err, payement.addSubOperation(subpayement))
591 IFOKDO(err, subpayement.setQuantity(-previousQuantity))
592 IFOKDO(err, subpayement.save())
593 }
594
595 IFOKDO(err, currentSubOperation.setQuantity(quantityFactor * SKGServices::stringToDouble(val)))
596 }
597 } else if (op == 'T') {
598 // T Amount
599 // Set value
600 checkOperationAmount = SKGServices::stringToDouble(val);
601 err = currentSubOperation.setQuantity(checkOperationAmount / currentUnitPrice);
602 if (!err && investmentAccount) {
603 err = currentOperation.setProperty(QStringLiteral("SKG_OP_ORIGINAL_AMOUNT"), val);
604 }
605 } else if (op == '$') {
606 // Dollar amount of split
607 // Set value
608 if (!investmentAccount) {
609 double vald = SKGServices::stringToDouble(val);
610 checkSuboperationsAmount += vald;
611 if (addNextAmountToTransferQuantity && !lastTransferAccount.isEmpty()) {
612 transferQuantity[transferAccount.count() - 1] += vald;
613 }
614 addNextAmountToTransferQuantity = false;
615 lastTransferAccount = QString();
616 err = currentSubOperation.setQuantity(vald);
617
618 // save
619 IFOKDO(err, currentSubOperation.save())
620
621 // Create suboperation
622 IFOKDO(err, currentOperation.addSubOperation(currentSubOperation))
623
624 latestSubCatMustBeRemoved = true;
625 }
626 } else if (op == 'P') {
627 // P Payee
628 // Set Payee
629 // Clean QIF coming from bankperfect
630 val.remove(QStringLiteral("[auto]"));
631
632 err = SKGPayeeObject::createPayee(m_importer->getDocument(), val, currentPayee);
633 IFOKDO(err, currentOperation.setPayee(currentPayee))
634 } else if (op == 'A') {
635 // A Address (up to 5 lines; 6th line is an optional message)
636 QString add = currentPayee.getAddress();
637 if (!add.isEmpty()) {
638 add += ' ';
639 }
640 add += val;
641 err = currentPayee.setAddress(add);
642 IFOKDO(err, currentPayee.save())
643 } else if (op == 'M') {
644 // M Memo
645 // Set Memo
646 err = currentOperation.setComment(val);
647 } else if (op == 'E') {
648 // E Memo in split
649 // Set Memo
650 err = currentSubOperation.setComment(val);
651 } else if (op == 'S' || op == 'L') {
652 // S Category in split (Category/Transfer/Class)
653 // L Category (Category/Subcategory/Transfer/Class)
654 // LCategory of transaction
655 // L[Transfer account]
656 // LCategory of transaction/Class of transaction
657 // L[Transfer account]/Class of transaction// Set Category
658 if (!val.isEmpty()) {
659 if (val[0] == '[') {
660 addNextAmountToTransferQuantity = true;
661
662 int pos = val.indexOf(']');
663 if (pos != -1) {
664 SKGPayeeObject payeeObj;
665 currentOperation.getPayee(payeeObj);
666 bool opening = (payeeObj.getName().compare(OPENINGBALANCE, Qt::CaseInsensitive) == 0);
667
668 // If the very first Bank transaction in the file has a payee of "Opening Balance", the L line contains the name of the account that the file describes. This is not a transfer
669 if (op == 'L' && automaticAccount && (account != nullptr) && opening) {
670 QString accountName = val.mid(1, pos - 1);
671
672 SKGAccountObject newAccount(m_importer->getDocument());
673 err = newAccount.setName(accountName);
674 IFOK(err) {
675 if (newAccount.exist()) {
676 // Oups, the real account is existing and it is another one
677 err = newAccount.load();
678
679 // We move the operation in the right account
680 IFOKDO(err, currentOperation.setParentAccount(newAccount))
681 IFOKDO(err, currentOperation.save())
682
683 // We delete the previous account if empty
684 IFOK(err) {
685 if (account->getNbOperation() == 0) {
686 err = account->remove();
687 }
688 delete account;
689 account = new SKGAccountObject(newAccount);
690 }
691 } else {
692 err = account->setName(accountName);
693 IFOKDO(err, account->save())
694 }
695 }
696 }
697 // if ( op=='L' && currentOperation.getPayee().compare ( "Opening Balance", Qt::CaseInsensitive ) !=0 && !investmentAccount)
698 if (!opening) {
699 lastTransferAccount = val.mid(1, pos - 1);
700 if ((account != nullptr) && lastTransferAccount == account->getName()) {
701 lastTransferAccount = QString();
702 }
703
704 if (!lastTransferAccount.isEmpty() &&
705 (transferAccount.count() == 0 ||
706 transferAccount.at(transferAccount.count() - 1) != lastTransferAccount ||
707 transferQuantity.at(transferQuantity.count() - 1) != 0.0
708 )
709 ) {
710 transferAccount.append(lastTransferAccount);
711 transferQuantity.append(0.0);
712 }
713 }
714 val = val.mid(pos + 2);
715 }
716 }
717 if (!err && !val.isEmpty()) {
718 auto cat_tag = SKGServices::splitCSVLine(val, '/', false);
719 val = cat_tag.at(0);
720 SKGCategoryObject Category;
721 val.replace('/', OBJECTSEPARATOR);
722 val.replace(':', OBJECTSEPARATOR);
723 val.replace(',', OBJECTSEPARATOR);
724 val.replace(';', OBJECTSEPARATOR);
725 err = SKGCategoryObject::createPathCategory(m_importer->getDocument(), val, Category);
726 IFOKDO(err, currentSubOperation.setCategory(Category))
727
728 if (!err && cat_tag.count() > 1) {
729 SKGTrackerObject tracker;
730 err = SKGTrackerObject::createTracker(m_importer->getDocument(), cat_tag.at(1), tracker);
731 IFOKDO(err, currentSubOperation.setTracker(tracker))
732 }
733 }
734 }
735 } else if (op == 'C') {
736 // C Cleared status
737 // Set status
738 err = currentOperation.setStatus((val == QStringLiteral("C") || val == QStringLiteral("*") ? SKGOperationObject::POINTED : (val == QStringLiteral("R") || val == QStringLiteral("X") ? SKGOperationObject::CHECKED : SKGOperationObject::NONE)));
739 } else if (op == '^') {
740 // ^ End of entry
741 // save
742
743 if (currentOperationInitialized) {
744 QByteArray hash = QCryptographicHash::hash(stringForHash.toUtf8(), QCryptographicHash::Md5);
745 SKGPayeeObject payeeObj;
746 currentOperation.getPayee(payeeObj);
747 bool opening = (payeeObj.getName().compare(OPENINGBALANCE, Qt::CaseInsensitive) == 0);
748 if (!err && opening) {
749 // Specific values for initial balance
750 err = currentOperation.setStatus(SKGOperationObject::CHECKED);
751 IFOKDO(err, currentOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
752 IFOKDO(err, currentSubOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
753 openingbalancecreated = true;
754 }
755
756 IFOKDO(err, currentOperation.setImportID(hash.toHex()))
757 IFOKDO(err, currentOperation.save())
758 if (!latestSubCatMustBeRemoved && !err) {
759 err = currentSubOperation.save();
760 }
761
762 // Create transfers if needed
763 // Get origin op
764 SKGOperationObject opOrigin(m_importer->getDocument(), currentOperation.getID());
765 SKGAccountObject accountOrigin;
766 IFOKDO(err, opOrigin.getParentAccount(accountOrigin))
767 int nbTransfers = transferAccount.count();
768 for (int j = 0; !err && j < nbTransfers; ++j) {
769 bool merged = false;
770 double tq = transferQuantity.at(j);
771 const QString& ta = transferAccount.at(j);
772 // Is the transfert operation already existing?
773 double qua = tq == 0.0 && addNextAmountToTransferQuantity ? SKGServices::stringToDouble(opOrigin.getAttribute(QStringLiteral("f_QUANTITY"))) : tq;
774 QString wc = "t_ACCOUNT='" % SKGServices::stringToSqlString(ta) %
775 "' AND t_TOACCOUNT='" % SKGServices::stringToSqlString(accountOrigin.getName()) %
776 "' AND ABS(f_QUANTITY-(" % SKGServices::doubleToString(-qua) % "))<0.0001"
777 " AND ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "'))<1"
778 " ORDER BY ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "')) ASC";
779 SKGObjectBase::SKGListSKGObjectBase obs;
780 m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display"), wc, obs);
781 if (!obs.isEmpty()) {
782 // We have to merge them and we do not need to create the transfer
783 SKGOperationObject firstOne(obs.at(0));
784
785 // Remove all operation attached to this transfer
786 SKGObjectBase::SKGListSKGObjectBase list;
787 IFOKDO(err, firstOne.getGroupedOperations(list))
788 for (const auto& o : qAsConst(list)) {
789 SKGOperationObject op2(o);
790 if (op2 != firstOne) {
791 IFOKDO(err, op2.setStatus(SKGOperationObject::NONE))
792 IFOKDO(err, op2.remove(false, true))
793 }
794 }
795
796 // Attach myself
797 IFOKDO(err, currentOperation.setGroupOperation(firstOne))
798 IFOKDO(err, currentOperation.save())
799
800 merged = true;
801 } else {
802 // Is the operation already created as a transfer of an other one?
803 QString wc = "t_import_id='QIF TRANSFER-" % SKGServices::stringToSqlString(ta) % "' AND t_ACCOUNT='" % SKGServices::stringToSqlString(accountOrigin.getName()) %
804 "' AND (ABS(f_CURRENTAMOUNT-(" % SKGServices::doubleToString(opOrigin.getCurrentAmount()) % "))<0.0001 OR f_QUANTITY=" % SKGServices::doubleToString(qua) % ")"
805 " AND ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "'))<1"
806 " ORDER BY ABS(julianday(d_date) - julianday('" % SKGServices::dateToSqlString(opOrigin.getDate()) % "')) ASC";
807 m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display"), wc, obs);
808 if (!obs.isEmpty()) {
809 // We have to merge them and we do not need to create the transfer
810 SKGOperationObject firstOne(obs.at(0));
811 err = opOrigin.setStatus(SKGOperationObject::NONE); // To be sure we can delete it
812 IFOKDO(err, opOrigin.save())
813 IFOKDO(err, firstOne.mergeAttribute(opOrigin))
814
815 SKGObjectBase::SKGListSKGObjectBase list;
816 IFOKDO(err, currentOperation.getGroupedOperations(list))
817 for (const auto& o : qAsConst(list)) {
818 SKGOperationObject op2(o);
819 IFOKDO(err, op2.setStatus(SKGOperationObject::NONE))
820 IFOKDO(err, op2.remove(false, true))
821 }
822 merged = true;
823 }
824 }
825
826 if (!merged) {
827 // Create target account if needed
828 SKGAccountObject accountTransfer(m_importer->getDocument());
829 if (m_accountCache.contains(ta)) {
830 accountTransfer = m_accountCache[ta];
831 } else {
832 accountTransfer.setName(ta);
833 if (!accountTransfer.exist()) {
834 // The account is created in the same bank by default
835 SKGBankObject bankOrigin;
836 IFOKDO(err, accountOrigin.getBank(bankOrigin))
837 IFOKDO(err, accountTransfer.setBank(bankOrigin))
838 IFOKDO(err, accountTransfer.save(false, true))
839 } else {
840 err = accountTransfer.load();
841 }
842
843 m_accountCache[ta] = accountTransfer;
844 }
845
846 // Create operation
847 SKGUnitObject unit;
848 opOrigin.getUnit(unit);
849
850 SKGOperationObject opTransfer;
851 IFOKDO(err, accountTransfer.addOperation(opTransfer, true))
852 IFOKDO(err, opTransfer.setDate(opOrigin.getDate()))
853 IFOKDO(err, opTransfer.setComment(opOrigin.getComment()))
854 SKGPayeeObject payeeObj2;
855 opTransfer.getPayee(payeeObj2);
856 IFOKDO(err, opTransfer.setPayee(payeeObj2))
857 IFOKDO(err, opTransfer.setStatus(opOrigin.getStatus()))
858 IFOKDO(err, opTransfer.setUnit(unit))
859 IFOKDO(err, opTransfer.setImportID("QIF TRANSFER-" % accountOrigin.getName()))
860 IFOKDO(err, opTransfer.save()) // save needed before setGroupOperation
861 IFOKDO(err, opTransfer.setGroupOperation(opOrigin))
862 IFOKDO(err, opOrigin.load()) // Must be reload because of setGroupOperation modified it
863 IFOKDO(err, opTransfer.save())
864
865 SKGSubOperationObject subopTransfer;
866 IFOKDO(err, opTransfer.addSubOperation(subopTransfer))
867 IFOKDO(err, subopTransfer.setQuantity(-qua))
868 IFOKDO(err, subopTransfer.save())
869 }
870 }
871 }
872
873 // Check Sum($)=T for incident 214462
874 QString checkOperationAmountString = SKGServices::doubleToString(checkOperationAmount);
875 QString checkSuboperationsAmountString = SKGServices::doubleToString(checkSuboperationsAmount);
876 if (!err && checkOperationAmount != 0 && checkSuboperationsAmount != 0 && checkOperationAmountString != checkSuboperationsAmountString) {
877 SKGSubOperationObject suboprepair;
878 IFOKDO(err, currentOperation.addSubOperation(suboprepair))
879 IFOKDO(err, suboprepair.setQuantity(checkOperationAmount - checkSuboperationsAmount))
880 IFOKDO(err, suboprepair.setComment(i18nc("An information message", "Auto repaired operation")))
881 IFOKDO(err, suboprepair.save())
882
883 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "The total amount of the operation (%1) was different to the sum of the sub-operations (%2). The operation has been repaired.", checkOperationAmountString, checkSuboperationsAmountString), SKGDocument::Warning))
884 }
885
886 // Initialize variables
887 currentOperationInitialized = false;
888 latestSubCatMustBeRemoved = false;
889 currentUnitForInvestment = QString();
890 quantityFactor = 1;
891 currentUnitPrice = 1;
892 stringForHash = QString();
893 checkOperationAmount = 0;
894 checkSuboperationsAmount = 0;
895 lastTransferAccount = QString();
896 transferAccount.clear();
897 transferQuantity.clear();
898 payement = SKGOperationObject();
899 } else {
900 // A Address (up to five lines; the sixth line is an optional message)
901 }
902 }
903
904 if (!err && i % 500 == 0) {
905 err = m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE"));
906 }
907 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
908 }
909
910 delete account;
911 account = nullptr;
912 SKGENDTRANSACTION(m_importer->getDocument(), err)
913
914 // Lines treated
915 IFOKDO(err, m_importer->getDocument()->stepForward(3))
916 }
917 }
918 }
919 SKGENDTRANSACTION(m_importer->getDocument(), err)
920
921 return err;
922 }
923
isExportPossible()924 bool SKGImportPluginQif::isExportPossible()
925 {
926 SKGTRACEINFUNC(10)
927 return (m_importer == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("QIF"));
928 }
929
exportFile()930 SKGError SKGImportPluginQif::exportFile()
931 {
932 if (m_importer == nullptr) {
933 return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters"));
934 }
935 SKGError err;
936 SKGTRACEINFUNCRC(2, err)
937
938 // Read parameters
939 auto listUUIDs = SKGServices::splitCSVLine(m_exportParameters.value(QStringLiteral("uuid_of_selected_accounts_or_operations")));
940
941 QStringList listOperationsToExport;
942 listOperationsToExport.reserve(listUUIDs.count());
943 QStringList listAccountsToExport;
944 listAccountsToExport.reserve(listUUIDs.count());
945 for (const auto& uuid : qAsConst(listUUIDs)) {
946 if (uuid.endsWith(QLatin1String("-operation"))) {
947 listOperationsToExport.push_back(uuid);
948 } else if (uuid.endsWith(QLatin1String("-account"))) {
949 listAccountsToExport.push_back(uuid);
950 }
951 }
952
953 if ((listAccountsToExport.count() != 0) || (listOperationsToExport.count() != 0)) {
954 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Only selected accounts and operations have been exported")))
955 }
956
957 // Open file
958 QSaveFile file(m_importer->getLocalFileName(false));
959 if (!file.open(QIODevice::WriteOnly)) {
960 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Save file '%1' failed", m_importer->getFileName().toDisplayString()));
961 } else {
962 QTextStream stream(&file);
963 if (!m_importer->getCodec().isEmpty()) {
964 stream.setCodec(m_importer->getCodec().toLatin1().constData());
965 }
966
967 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export %1 file", "QIF"), 3);
968 IFOK(err) {
969 // Export categories
970 SKGObjectBase::SKGListSKGObjectBase categories;
971 IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_category_display_tmp"), QStringLiteral("1=1 ORDER BY t_fullname, id"), categories))
972 int nbcat = categories.count();
973 if (!err && (nbcat != 0)) {
974 stream << "!Type:Cat\n";
975 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export categories"), nbcat);
976 for (int i = 0; !err && i < nbcat; ++i) {
977 SKGCategoryObject cat(categories.at(i));
978 QString catName = cat.getFullName();
979 if (!catName.isEmpty()) {
980 stream << QStringLiteral("N") << catName.replace(OBJECTSEPARATOR, QStringLiteral(":")) << SKGENDL;
981 if (SKGServices::stringToDouble(cat.getAttribute(QStringLiteral("f_REALCURRENTAMOUNT"))) < 0) {
982 stream << "E" << SKGENDL;
983 } else {
984 stream << "I" << SKGENDL;
985 }
986 stream << "^" << SKGENDL;
987 }
988 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
989 }
990
991 SKGENDTRANSACTION(m_importer->getDocument(), err)
992 }
993 IFOKDO(err, m_importer->getDocument()->stepForward(1))
994
995 SKGServices::SKGUnitInfo primaryUnit = m_importer->getDocument()->getPrimaryUnit();
996
997 // Get operations
998 QString currentAccountName;
999 SKGObjectBase::SKGListSKGObjectBase operations;
1000 IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_operation_display_all"), QStringLiteral("t_template='N' ORDER BY t_ACCOUNT, d_date, id"), operations))
1001 int nb = operations.count();
1002 IFOK(err) {
1003 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export operations"), nb);
1004 for (int i = 0; !err && i < nb; ++i) {
1005 SKGOperationObject operation(operations.at(i));
1006 SKGAccountObject a;
1007 operation.getParentAccount(a);
1008 if ((listOperationsToExport.isEmpty() || listOperationsToExport.contains(operation.getUniqueID())) &&
1009 (listAccountsToExport.isEmpty() || listAccountsToExport.contains(a.getUniqueID()))) {
1010 // Get account name
1011 QString accountName = operation.getAttribute(QStringLiteral("t_ACCOUNT"));
1012
1013 // In the same account ?
1014 if (accountName != currentAccountName) {
1015 SKGAccountObject account(m_importer->getDocument());
1016 account.setName(accountName);
1017 account.load();
1018
1019 SKGBankObject bank;
1020 account.getBank(bank);
1021
1022 // Write header
1023 stream << "!Account\n";
1024 stream << 'N' << accountName << SKGENDL;
1025 QString type = (account.getType() == SKGAccountObject::CURRENT ? QStringLiteral("Bank") : (account.getType() == SKGAccountObject::CREDITCARD ? QStringLiteral("CCard") : (account.getType() == SKGAccountObject::INVESTMENT ? QStringLiteral("Invst") : (account.getType() == SKGAccountObject::ASSETS ? QStringLiteral("Oth A") : QStringLiteral("Cash")))));
1026 stream << 'T' << type << SKGENDL;
1027 QString number = bank.getNumber();
1028 QString bnumber = account.getAgencyNumber();
1029 QString cnumber = account.getNumber();
1030 if (!bnumber.isEmpty()) {
1031 if (!number.isEmpty()) {
1032 number += '-';
1033 }
1034 number += bnumber;
1035 }
1036 if (!cnumber.isEmpty()) {
1037 if (!number.isEmpty()) {
1038 number += '-';
1039 }
1040 number += cnumber;
1041 }
1042 stream << 'D' << number << SKGENDL;
1043 // stream << "/" Statement balance date
1044 // stream << "$" Statement balance amount
1045 stream << '^' << SKGENDL;
1046 currentAccountName = accountName;
1047
1048 stream << "!Type:" << type << "\n";
1049 }
1050
1051
1052 // Write operation
1053 /*
1054 DONE D Date
1055 DONE T Amount
1056 N/A U Transaction amount (higher possible value than T)
1057 DONE C Cleared status
1058 DONE N Number (check or reference number)
1059 DONE P Payee/description
1060 DONE M Memo
1061 N/A A Address (up to 5 lines; 6th line is an optional message)
1062 DONE L Category (category/class or transfer/class)
1063 DONE S Category in split (category/class or transfer/class)
1064 DONE E Memo in split
1065 DONE $ Dollar amount of split
1066 N/A % Percentage of split if percentages are used
1067 N/A F Reimbursable business expense flag
1068 N/A X Small Business extensions
1069 DONE Y Security
1070 DONE I Price
1071 DONE Q Quantity (number of shares or split ratio)
1072 N/A O Commission
1073 DONE ^ End of entry
1074 */
1075 SKGUnitObject unit;
1076 operation.getUnit(unit);
1077 bool investment = false;
1078 bool unitExported = false;
1079 if (unit.getSymbol() != primaryUnit.Symbol && !primaryUnit.Symbol.isEmpty()) {
1080 unitExported = true;
1081 }
1082 if (unit.getType() == SKGUnitObject::SHARE) {
1083 unitExported = true;
1084 investment = true;
1085 }
1086
1087 QString date = SKGServices::dateToSqlString(operation.getDate());
1088 if (date.isEmpty()) {
1089 // This is an opening balance
1090 date = QStringLiteral("0000-00-00");
1091 }
1092 stream << 'D' << date << SKGENDL;
1093 if (!unitExported) {
1094 stream << 'T' << SKGServices::doubleToString(operation.getCurrentAmount()) << SKGENDL;
1095 }
1096
1097 if (!investment) {
1098 auto number = operation.getNumber();
1099 if (!number.isEmpty()) {
1100 stream << 'N' << operation.getNumber() << SKGENDL;
1101 }
1102 } else {
1103 stream << 'N' << (operation.getCurrentAmount() > 0 ? "Buy" : "Sell") << SKGENDL;
1104 }
1105
1106 if (unitExported) {
1107 stream << 'Y' << unit.getSymbol() << SKGENDL;
1108 }
1109
1110 SKGPayeeObject payeeObj;
1111 operation.getPayee(payeeObj);
1112 QString payee = payeeObj.getName();
1113 QString address = payeeObj.getAddress();
1114 if (date == QStringLiteral("0000-00-00")) {
1115 payee = OPENINGBALANCE;
1116 }
1117 if (!payee.isEmpty()) {
1118 stream << 'P' << payee << SKGENDL;
1119 }
1120 if (!address.isEmpty()) {
1121 stream << 'A' << address << SKGENDL;
1122 }
1123
1124 QString memo = operation.getMode() % " " % operation.getComment();
1125 memo = memo.trimmed();
1126 if (!memo.isEmpty()) {
1127 stream << 'M' << memo << SKGENDL;
1128 }
1129
1130 SKGOperationObject::OperationStatus status = operation.getStatus();
1131 stream << 'C' << (status == SKGOperationObject::POINTED ? "C" : (status == SKGOperationObject::CHECKED ? "R" : "")) << SKGENDL;
1132
1133 // Get sub operations
1134 SKGObjectBase::SKGListSKGObjectBase suboperations;
1135 err = operation.getSubOperations(suboperations);
1136 IFOK(err) {
1137 int nbSubOps = suboperations.size();
1138 QString category;
1139 if (nbSubOps == 1) {
1140 SKGSubOperationObject suboperation(suboperations.at(0));
1141 // Dump quantity
1142 if (unitExported) {
1143 stream << 'Q' << SKGServices::doubleToString(qAbs(suboperation.getQuantity())) << SKGENDL;
1144 stream << 'I' << SKGServices::doubleToString(qAbs(operation.getCurrentAmount() / suboperation.getQuantity())) << SKGENDL;
1145 }
1146
1147 // Get category of this simple operation
1148 SKGCategoryObject cat;
1149 suboperation.getCategory(cat);
1150 category = cat.getFullName().replace(OBJECTSEPARATOR, QStringLiteral(":"));
1151 }
1152
1153 // Is it a transfer
1154 SKGOperationObject transfer;
1155 if (operation.isTransfer(transfer)) {
1156 if (!category.isEmpty()) {
1157 category.prepend('/');
1158 }
1159
1160 SKGAccountObject transferAccount;
1161 err = transfer.getParentAccount(transferAccount);
1162 IFOK(err) category.prepend('[' % transferAccount.getName() % ']');
1163 }
1164 if (!category.isEmpty()) {
1165 stream << 'L' << category << SKGENDL;
1166 }
1167
1168 if (nbSubOps > 1) {
1169 // Split operation
1170 for (int k = 0; k < nbSubOps; ++k) {
1171 SKGSubOperationObject suboperation(suboperations.at(k));
1172 SKGCategoryObject cat;
1173 suboperation.getCategory(cat);
1174
1175 QString category2 = cat.getFullName().replace(OBJECTSEPARATOR, QStringLiteral(":"));
1176 if (!category2.isEmpty()) {
1177 stream << 'S' << category2 << SKGENDL;
1178 }
1179 QString memo2 = suboperation.getComment();
1180 memo2 = memo2.trimmed();
1181 if (!memo2.isEmpty()) {
1182 stream << 'E' << memo2 << SKGENDL;
1183 }
1184 stream << '$' << SKGServices::doubleToString(suboperation.getQuantity()) << SKGENDL;
1185 }
1186 }
1187 }
1188
1189 stream << '^' << SKGENDL;
1190 }
1191 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
1192 }
1193
1194 SKGENDTRANSACTION(m_importer->getDocument(), err)
1195 }
1196 IFOKDO(err, m_importer->getDocument()->stepForward(2))
1197
1198 // Export prices
1199 SKGObjectBase::SKGListSKGObjectBase unitvalues;
1200 IFOKDO(err, m_importer->getDocument()->getObjects(QStringLiteral("v_unitvalue"), QStringLiteral("1=1 ORDER BY (select t_name from unit where v_unitvalue.rd_unit_id=unit.id), d_date"), unitvalues))
1201 nb = unitvalues.count();
1202 if (!err && (nb != 0)) {
1203 stream << "!Type:Prices\n";
1204 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export units"), nb);
1205 for (int i = 0; !err && i < nb; ++i) {
1206 SKGUnitValueObject unitVal(unitvalues.at(i));
1207 SKGUnitObject unit;
1208 err = unitVal.getUnit(unit);
1209 IFOK(err) {
1210 QStringList vals;
1211 QString v = unit.getSymbol();
1212 if (v.isEmpty()) {
1213 v = unit.getName();
1214 }
1215 vals.push_back(v);
1216 vals.push_back(SKGServices::doubleToString(unitVal.getQuantity()));
1217 vals.push_back(SKGServices::dateToSqlString(unitVal.getDate()));
1218
1219 stream << SKGServices::stringsToCsv(vals) << SKGENDL;
1220 }
1221 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
1222 }
1223 stream << "^" << SKGENDL;
1224
1225 SKGENDTRANSACTION(m_importer->getDocument(), err)
1226 }
1227 IFOKDO(err, m_importer->getDocument()->stepForward(3))
1228 SKGENDTRANSACTION(m_importer->getDocument(), err)
1229 }
1230
1231 // Close file
1232 file.commit();
1233 }
1234
1235 return err;
1236 }
1237
getMimeTypeFilter() const1238 QString SKGImportPluginQif::getMimeTypeFilter() const
1239 {
1240 return "*.qif|" % i18nc("A file format", "QIF file");
1241 }
1242
1243 #include <skgimportpluginqif.moc>
1244