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 to generate categories.
8  *
9  * @author Stephane MANKOWSKI / Guillaume DE BURE
10  */
11 #include "skgcategoriesplugin.h"
12 
13 #include <kaboutdata.h>
14 #include <kactioncollection.h>
15 #include <kpluginfactory.h>
16 #include <kselectaction.h>
17 
18 #include <qaction.h>
19 #include <qdiriterator.h>
20 #include <qfileinfo.h>
21 #include <qstandardpaths.h>
22 
23 #include "skgcategoriespluginwidget.h"
24 #include "skgcategoryobject.h"
25 #include "skgdocumentbank.h"
26 #include "skghtmlboardwidget.h"
27 #include "skgimportexportmanager.h"
28 #include "skgmainpanel.h"
29 #include "skgtraces.h"
30 #include "skgtransactionmng.h"
31 
32 /**
33  * This plugin factory.
34  */
K_PLUGIN_FACTORY(SKGCategoriesPluginFactory,registerPlugin<SKGCategoriesPlugin> ();)35 K_PLUGIN_FACTORY(SKGCategoriesPluginFactory, registerPlugin<SKGCategoriesPlugin>();)
36 
37 SKGCategoriesPlugin::SKGCategoriesPlugin(QWidget* iWidget, QObject* iParent, const QVariantList& /*iArg*/)
38     : SKGInterfacePlugin(iParent), m_currentBankDocument(nullptr)
39 {
40     Q_UNUSED(iWidget)
41     SKGTRACEINFUNC(10)
42 }
43 
~SKGCategoriesPlugin()44 SKGCategoriesPlugin::~SKGCategoriesPlugin()
45 {
46     SKGTRACEINFUNC(10)
47     m_currentBankDocument = nullptr;
48 }
49 
setupActions(SKGDocument * iDocument)50 bool SKGCategoriesPlugin::setupActions(SKGDocument* iDocument)
51 {
52     SKGTRACEINFUNC(10)
53 
54     m_currentBankDocument = qobject_cast<SKGDocumentBank*>(iDocument);
55     if (m_currentBankDocument == nullptr) {
56         return false;
57     }
58 
59     setComponentName(QStringLiteral("skrooge_categories"), title());
60     setXMLFile(QStringLiteral("skrooge_categories.rc"));
61 
62     // Import categories
63     QStringList overlaycategories;
64     overlaycategories.push_back(icon());
65 
66     QStringList overlaydelete;
67     overlaydelete.push_back(QStringLiteral("edit-delete"));
68 
69     auto contextMenu = new KSelectAction(SKGServices::fromTheme(QStringLiteral("document-import"), overlaycategories), i18nc("Verb", "Import categories"), this);
70     registerGlobalAction(QStringLiteral("import_categories"), contextMenu);
71 
72     QAction* actImportStdCat = contextMenu->addAction(SKGServices::fromTheme(QStringLiteral("document-import"), overlaycategories), i18nc("Verb", "Import standard categories"));
73     actImportStdCat->setCheckable(false);
74     connect(actImportStdCat, &QAction::triggered, this, &SKGCategoriesPlugin::importStandardCategories);
75     registerGlobalAction(QStringLiteral("import_standard_categories"), actImportStdCat);
76 
77     const auto dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, "skrooge/categories/" % QLocale().name().split(QStringLiteral("_")).at(0), QStandardPaths::LocateDirectory);
78     for (const auto& dir : dirs) {
79         QDirIterator it(dir, QStringList() << QStringLiteral("*.qif"));
80         while (it.hasNext()) {
81             QString cat = it.next();
82 
83             QString name = QFileInfo(cat).baseName().replace('_', ' ');
84             QAction* act = contextMenu->addAction(SKGServices::fromTheme(QStringLiteral("document-import"), overlaycategories), i18nc("Verb", "Import categories [%1]", name));
85             act->setCheckable(false);
86             act->setData(cat);
87             connect(act, &QAction::triggered, this, &SKGCategoriesPlugin::importCategories);
88             registerGlobalAction("import_categories_" % name, act);
89         }
90     }
91 
92     auto deleteUnusedCategoriesAction = new QAction(SKGServices::fromTheme(icon(), overlaydelete), i18nc("Verb", "Delete unused categories"), this);
93     connect(deleteUnusedCategoriesAction, &QAction::triggered, this, &SKGCategoriesPlugin::deleteUnusedCategories);
94     registerGlobalAction(QStringLiteral("clean_delete_unused_categories"), deleteUnusedCategoriesAction);
95 
96     // ------------
97     auto actTmp = new QAction(SKGServices::fromTheme(icon()), i18nc("Verb", "Open similar categories..."), this);
98     actTmp->setData(QString("skg://skrooge_categories_plugin/?title_icon=" % icon() % "&title=" % SKGServices::encodeForUrl(i18nc("Noun, a list of items", "Similar categories")) %
99                             "&whereClause=" % SKGServices::encodeForUrl(QStringLiteral("id IN (SELECT category.id FROM category, ("
100                                     "SELECT * FROM category C WHERE EXISTS (SELECT 1 FROM category p2 WHERE p2.id<>C.id AND upper(p2.t_fullname)=upper(C.t_fullname) AND p2.t_fullname<>C.t_fullname)) "
101                                     "AS C WHERE category.t_fullname=C.t_fullname OR C.t_fullname LIKE category.t_fullname||' > %')"))));
102     connect(actTmp, &QAction::triggered, SKGMainPanel::getMainPanel(),  [ = ]() {
103         SKGMainPanel::getMainPanel()->SKGMainPanel::openPage();
104     });
105     registerGlobalAction(QStringLiteral("view_open_similar_categories"), actTmp);
106     /*TODO
107     WITH RECURSIVE
108     cat(t_fullname,id, level) AS (
109     SELECT t_fullname, id, 0 FROM v_category_display WHERE EXISTS (SELECT 1 FROM category p2 WHERE p2.id<>v_category_display.id AND upper(p2.t_fullname)=upper(v_category_display.t_fullname) AND p2.t_fullname<>v_category_display.t_fullname)
110     UNION ALL
111     SELECT v_category_display.t_name, v_category_display.id, cat.level+1
112     FROM v_category_display JOIN cat ON v_category_display.rd_category_id=cat.id
113     ORDER BY 2
114     ) SELECT substr('..........',1,level*3) || t_fullname FROM cat;
115     */
116     return true;
117 }
118 
getNbDashboardWidgets()119 int SKGCategoriesPlugin::getNbDashboardWidgets()
120 {
121     SKGTRACEINFUNC(1)
122     return 4;
123 }
124 
getDashboardWidgetTitle(int iIndex)125 QString SKGCategoriesPlugin::getDashboardWidgetTitle(int iIndex)
126 {
127     SKGTRACEINFUNC(1)
128     if (iIndex == 0) {
129         return i18nc("Report header",  "5 main categories of expenditure");
130     }
131     if (iIndex == 1) {
132         return i18nc("Report header",  "5 main variations");
133     }
134     if (iIndex == 2) {
135         return i18nc("Report header",  "Budget");
136     }
137     return i18nc("Report header",  "5 main variations (issues)");
138 }
139 
getDashboardWidget(int iIndex)140 SKGBoardWidget* SKGCategoriesPlugin::getDashboardWidget(int iIndex)
141 {
142     SKGTRACEINFUNC(1)
143     // Get QML mode for dashboard
144     KConfigSkeleton* skl = SKGMainPanel::getMainPanel()->getPluginByName(QStringLiteral("Dashboard plugin"))->getPreferenceSkeleton();
145     KConfigSkeletonItem* sklItem = skl->findItem(QStringLiteral("qmlmode"));
146     bool qml = sklItem->property().toBool();
147 
148     if (iIndex == 0)  {
149         return new SKGHtmlBoardWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument,
150                                       getDashboardWidgetTitle(iIndex),
151                                       QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/html/default/categories_period_table.") % (qml ?  QStringLiteral("qml") :  QStringLiteral("html"))),
152                                       QStringList() << QStringLiteral("v_suboperation_consolidated"), SKGSimplePeriodEdit::ALL_PERIODS);
153     }
154     if (iIndex == 1) {
155         return new SKGHtmlBoardWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument,
156                                       getDashboardWidgetTitle(iIndex) % " - %1",
157                                       QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/html/default/categories_variations.") % (qml ?  QStringLiteral("qml") :  QStringLiteral("html"))),
158                                       QStringList() << QStringLiteral("v_suboperation_consolidated"), SKGSimplePeriodEdit::PREVIOUS_AND_CURRENT_PERIODS);
159     }
160     if (iIndex == 2) {
161         return new SKGHtmlBoardWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument,
162                                       getDashboardWidgetTitle(iIndex) % " - %1",
163                                       QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/html/default/budget_table.") % (qml ?  QStringLiteral("qml") :  QStringLiteral("html"))),
164                                       QStringList() << QStringLiteral("v_budget"), SKGSimplePeriodEdit::PREVIOUS_AND_CURRENT_MONTHS);
165     }
166     return new SKGHtmlBoardWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument,
167                                   getDashboardWidgetTitle(iIndex) % " - %1",
168                                   QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/html/default/categories_variations_issues.") % (qml ?  QStringLiteral("qml") :  QStringLiteral("html"))),
169                                   QStringList() << QStringLiteral("v_suboperation_consolidated"), SKGSimplePeriodEdit::PREVIOUS_AND_CURRENT_PERIODS);
170 }
171 
refresh()172 void SKGCategoriesPlugin::refresh()
173 {
174     SKGTRACEINFUNC(10)
175     if (m_currentBankDocument != nullptr) {
176         // Automatic categories creation
177         if (m_currentBankDocument->getMainDatabase() != nullptr) {
178             QString doc_id = m_currentBankDocument->getUniqueIdentifier();
179             if (m_docUniqueIdentifier != doc_id) {
180                 m_docUniqueIdentifier = doc_id;
181 
182                 bool exist = false;
183                 SKGError err = m_currentBankDocument->existObjects(QStringLiteral("category"), QString(), exist);
184                 if (!err && !exist) {
185                     importStandardCategories();
186 
187                     // The file is considered has not modified
188                     m_currentBankDocument->setFileNotModified();
189                 }
190             }
191         }
192     }
193 }
194 
importCategories()195 void SKGCategoriesPlugin::importCategories()
196 {
197     SKGTRACEINFUNC(10)
198     SKGError err;
199     auto* act = qobject_cast< QAction* >(sender());
200     if (act != nullptr) {
201         QString fileName = act->data().toString();
202         QString name = QFileInfo(fileName).baseName().replace('_', ' ');
203         {
204             SKGBEGINTRANSACTION(*m_currentBankDocument, i18nc("Verb", "Import categories [%1]", name), err)
205 
206             SKGImportExportManager imp(m_currentBankDocument, QUrl(fileName));
207             err = imp.importFile();
208             IFOKDO(err, m_currentBankDocument->removeMessages(m_currentBankDocument->getCurrentTransaction()))
209         }
210 
211 
212         // status
213         IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Categories imported.")))
214         else {
215             err.addError(ERR_FAIL, i18nc("Error message", "Importing categories failed."));
216         }
217 
218         // Display error
219         SKGMainPanel::displayErrorMessage(err);
220     }
221 }
222 
importStandardCategories()223 void SKGCategoriesPlugin::importStandardCategories()
224 {
225     SKGTRACEINFUNC(10)
226     SKGError err;
227     {
228         QString cats = i18nc("List of categories. It is not needed to translate each item. You can set the list you want. ';' must be used to separate categories. ' > ' must be used to separate category and sub category (no limit of level).",
229                              "Alimony;Auto;Auto > Fuel;Auto > Insurance;Auto > Lease;Auto > Loan;Auto > Registration;Auto > Service;Bank Charges;Bank Charges > Interest Paid;Bank Charges > Service Charge;Bills;Bills > Electricity;"
230                              "Bills > Fuel Oil;Bills > Local Taxes;Bills > Mortgage;Bills > Natural Gas;Bills > Rent;Bills > TV;Bills > Telephone;Bills > Water & Sewage;Bonus;Business;Business > Auto;Business > Capital Goods;Business > Legal Expenses;Business > Office Rent;"
231                              "Business > Office Supplies;Business > Other;Business > Revenue;Business > Taxes;Business > Travel;Business > Utilities;Business > Wages & Salary;Car;Car > Fuel;Car > Insurance;Car > Lease;Car > Loan;Car > Registration;Car > Service;"
232                              "Cash Withdrawal;Charity;Charity > Donations;Child Care;Child Support;Clothing;Disability;Div Income;Div Income > Ord dividend;Div Income > Stock dividend;Education;Education > Board;Education > Books;Education > Fees;Education > Loans;"
233                              "Education > Tuition;Employment;Employment > Benefits;Employment > Foreign;Employment > Lump sums;Employment > Other employ;Employment > Salary & wages;Food;Food > Dining Out;Food > Groceries;Gardening;"
234                              "Gift Received;Gifts;Healthcare;Healthcare > Dental;Healthcare > Doctor;Healthcare > Hospital;Healthcare > Optician;Healthcare > Prescriptions;Holidays;Holidays > Accomodation;Holidays > Travel;Household;"
235                              "Household > Furnishings;Household > Repairs;Insurance;Insurance > Auto;Insurance > Disability;Insurance > Home and Contents;Insurance > Life;Insurance > Medical;Int Inc;Int Inc > Bank Interest;Int Inc > Gross;Int Inc > Net;"
236                              "Int Inc > Other savings;Invest. income;Invest. income > 1st option;Invest. income > Dividend;Invest. income > Foreign;Invest. income > Other savings;Invest. income > Other trusts;Invest. income > Other trusts#Capital;"
237                              "Invest. income > Other trusts#Dist. rec'd;Invest. income > Other trusts#Estate;Investment Income;Investment Income > Dividends;Investment Income > Interest;Investment Income > Long-Term Capital Gains;"
238                              "Investment Income > Short-Term Capital Gains;Investment Income > Tax-Exempt Interest;Job Expense;Job Expense > Non-Reimbursed;Job Expense > Reimbursed;Legal Fees;Leisure;Leisure > Books & Magazines;Leisure > Entertaining;"
239                              "Leisure > Films & Video Rentals;Leisure > Hobbies;Leisure > Sporting Events;Leisure > Sports Goods;Leisure > Tapes & CDs;Leisure > Theatre & Concerts etc;Leisure > Toys & Games;Loan;Loan > Loan Interest;Long-Term Capital gains;Mortgage;Mortgage > Interest;Mortgage > PMI;Mortgage > Principle;Motor;Motor > Fuel;Motor > Loan;Motor > Service;Other Expense;Other Expense > Unknown;Other Income;Other Income > Child Support;"
240                              "Other Income > Employee Share Option;Other Income > Gifts Received;Other Income > Loan Principal Received;Other Income > Lottery or Premium Bond Prizes;Other Income > Student loan;Other Income > Tax Refund;"
241                              "Other Income > Unemployment Benefit;Pension;Pension > Employer;Personal Care;Pet Care;Pet Care > Food;Pet Care > Supplies;Pet Care > Vet's Bills;Recreation;Retirement Accounts;Retirement Accounts > 401(k)403(b) Plan Contributions;"
242                              "Retirement Accounts > 529 Plan Contributions;Retirement Accounts > IRA Contributions;Retirement Income;Retirement Income > 401(k);Retirement Income > 401(k) > 403(b) Distributions;Retirement Income > IRA Distributions;"
243                              "Retirement Income > Pensions & Annuities;Retirement Income > State Pension Benefits;Short-Term Capital gains;Social Security Benefits;Taxes;Taxes > AMT;Taxes > Federal Tax;Taxes > Federal Taxes;Taxes > Local Tax;Taxes > Local Taxes;"
244                              "Taxes > Other Invest;Taxes > Other Tax;Taxes > Property Taxes;Taxes > Social Security;Taxes > State Tax;Taxes > State Taxes;Travel;Travel > Accomodations;Travel > Car Rental;Travel > Fares;Utilities;Utilities > Electricity;"
245                              "Utilities > Garbage & Recycling;Utilities > Gas;Utilities > Sewer;Utilities > Telephone;Utilities > Water;Wages & Salary;Wages & Salary > Benefits;Wages & Salary > Bonus;Wages & Salary > Commission;"
246                              "Wages & Salary > Employer Pension Contributions;Wages & Salary > Gross Pay;Wages & Salary > Net Pay;Wages & Salary > Overtime;Wages & Salary > Workman's Comp");
247 
248         SKGBEGINTRANSACTION(*m_currentBankDocument, i18nc("Noun, name of the user action", "Import standard categories"), err)
249 
250         const auto items = SKGServices::splitCSVLine(cats, ';');
251         for (const auto& item : items) {
252             QString line = item.trimmed();
253             if (!line.isEmpty()) {
254                 SKGCategoryObject cat;
255                 err = SKGCategoryObject::createPathCategory(m_currentBankDocument, line, cat);
256             }
257         }
258     }
259 
260 
261     // status
262     IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Categories imported.")))
263     else {
264         err.addError(ERR_FAIL, i18nc("Error message", "Importing categories failed."));
265     }
266 
267     // Display error
268     SKGMainPanel::displayErrorMessage(err);
269 }
270 
getWidget()271 SKGTabPage* SKGCategoriesPlugin::getWidget()
272 {
273     SKGTRACEINFUNC(10)
274     return new SKGCategoriesPluginWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument);
275 }
276 
title() const277 QString SKGCategoriesPlugin::title() const
278 {
279     return i18nc("Noun, categories of items", "Categories");
280 }
281 
icon() const282 QString SKGCategoriesPlugin::icon() const
283 {
284     return QStringLiteral("view-categories");
285 }
286 
toolTip() const287 QString SKGCategoriesPlugin::toolTip() const
288 {
289     return i18nc("A tool tip", "Categories management");
290 }
291 
tips() const292 QStringList SKGCategoriesPlugin::tips() const
293 {
294     QStringList output;
295     output.push_back(i18nc("Description of a tips", "<p>... <a href=\"skg://skrooge_categories_plugin\">categories</a> can be reorganized by drag & drop.</p>"));
296     output.push_back(i18nc("Description of a tips", "<p>... if you delete a <a href=\"skg://skrooge_categories_plugin\">category</a>, all operations affected by this category will be associated to its parent category.</p>"));
297     return output;
298 }
299 
getOrder() const300 int SKGCategoriesPlugin::getOrder() const
301 {
302     return 30;
303 }
304 
isInPagesChooser() const305 bool SKGCategoriesPlugin::isInPagesChooser() const
306 {
307     return true;
308 }
309 
advice(const QStringList & iIgnoredAdvice)310 SKGAdviceList SKGCategoriesPlugin::advice(const QStringList& iIgnoredAdvice)
311 {
312     SKGTRACEINFUNC(10)
313     SKGAdviceList output;
314     // Check unused categories
315     if (!iIgnoredAdvice.contains(QStringLiteral("skgcategoriesplugin_unused"))) {
316         bool exist = false;
317         m_currentBankDocument->existObjects(QStringLiteral("v_category_used2"), QStringLiteral("t_ISUSEDCASCADE='N'"), exist);
318         if (exist) {
319             SKGAdvice ad;
320             ad.setUUID(QStringLiteral("skgcategoriesplugin_unused"));
321             ad.setPriority(5);
322             ad.setShortMessage(i18nc("Advice on making the best (short)", "Many unused categories"));
323             ad.setLongMessage(i18nc("Advice on making the best (long)", "You can improve performances by removing categories that have no operations."));
324             SKGAdvice::SKGAdviceActionList autoCorrections;
325             {
326                 SKGAdvice::SKGAdviceAction a;
327                 a.Title = QStringLiteral("skg://clean_delete_unused_categories");
328                 a.IsRecommended = true;
329                 autoCorrections.push_back(a);
330             }
331             ad.setAutoCorrections(autoCorrections);
332             output.push_back(ad);
333         }
334     }
335 
336     // Check operations not validated
337     if (!iIgnoredAdvice.contains(QStringLiteral("skgmonthlyplugin_maincategoriesvariation"))) {
338         QString month = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
339         QDate datepreviousmonth = QDate::currentDate().addDays(-QDate::currentDate().day());
340         QString previousmonth = datepreviousmonth.toString(QStringLiteral("yyyy-MM"));
341 
342         QStringList listCategories;
343         QStringList listVariations = m_currentBankDocument->get5MainCategoriesVariationList(month, previousmonth, true, &listCategories);
344 
345         int nb = listVariations.count();
346         SKGAdvice::SKGAdviceActionList autoCorrections;
347         for (int i = 0; i < nb; ++i) {
348             SKGAdvice ad;
349             ad.setUUID("skgmonthlyplugin_maincategoriesvariation|" % listCategories.at(i));
350             ad.setPriority(7);
351             ad.setShortMessage(i18nc("Advice on making the best (short)", "Important variation for '%1'", listCategories.at(i)));
352             ad.setLongMessage(listVariations.at(i));
353             autoCorrections.resize(0);
354             {
355                 SKGAdvice::SKGAdviceAction a;
356                 a.Title = i18nc("Advice on making the best (action)", "Open sub operations with category containing '%1'", listCategories.at(i));
357                 a.IconName = QStringLiteral("quickopen");
358                 a.IsRecommended = false;
359                 autoCorrections.push_back(a);
360             }
361             ad.setAutoCorrections(autoCorrections);
362             output.push_back(ad);
363         }
364     }
365 
366     // Check categories with different case
367     if (!iIgnoredAdvice.contains(QStringLiteral("skgcategoriesplugin_case"))) {
368         bool exist = false;
369         m_currentBankDocument->existObjects(QStringLiteral("category"), QStringLiteral("EXISTS (SELECT 1 FROM category p2 WHERE p2.id<>category.id AND upper(p2.t_fullname)=upper(category.t_fullname) AND p2.t_fullname<>category.t_fullname)"), exist);
370         if (exist) {
371             SKGAdvice ad;
372             ad.setUUID(QStringLiteral("skgcategoriesplugin_case"));
373             ad.setPriority(3);
374             ad.setShortMessage(i18nc("Advice on making the best (short)", "Some categories seem to be identical"));
375             ad.setLongMessage(i18nc("Advice on making the best (long)", "Some categories seem to be identical but with different syntax. They could be merged."));
376             SKGAdvice::SKGAdviceActionList autoCorrections;
377             {
378                 SKGAdvice::SKGAdviceAction a;
379                 a.Title = QStringLiteral("skg://view_open_similar_categories");
380                 a.IsRecommended = false;
381                 autoCorrections.push_back(a);
382             }
383             ad.setAutoCorrections(autoCorrections);
384             output.push_back(ad);
385         }
386     }
387     return output;
388 }
389 
executeAdviceCorrection(const QString & iAdviceIdentifier,int iSolution)390 SKGError SKGCategoriesPlugin::executeAdviceCorrection(const QString& iAdviceIdentifier, int iSolution)
391 {
392     if ((m_currentBankDocument != nullptr) && iAdviceIdentifier.startsWith(QLatin1String("skgmonthlyplugin_maincategoriesvariation|"))) {
393         // Get parameters
394         QString category = iAdviceIdentifier.right(iAdviceIdentifier.length() - 41);
395         QString month = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
396 
397         // Call operation plugin
398         SKGMainPanel::getMainPanel()->openPage("skg://skrooge_operation_plugin/SKGOPERATION_CONSOLIDATED_DEFAULT_PARAMETERS/?currentPage=-1&title_icon=" % icon() % "&operationTable=v_suboperation_consolidated&title=" %
399                                                SKGServices::encodeForUrl(i18nc("Noun, a list of items", "Sub operations with category containing '%1'",  category)) % "&operationWhereClause=" % SKGServices::encodeForUrl("d_DATEMONTH='" % month % "' AND t_REALCATEGORY='" % SKGServices::stringToSqlString(category) % '\''));
400         return SKGError();
401     }
402     return SKGInterfacePlugin::executeAdviceCorrection(iAdviceIdentifier, iSolution);
403 }
404 
deleteUnusedCategories() const405 void SKGCategoriesPlugin::deleteUnusedCategories() const
406 {
407     SKGError err;
408     _SKGTRACEINFUNCRC(10, err)
409     if (m_currentBankDocument != nullptr) {
410         SKGBEGINTRANSACTION(*m_currentBankDocument, i18nc("Noun, name of the user action", "Delete unused categories"), err)
411 
412         QStringList categoriesUsed;
413         err = m_currentBankDocument->getDistinctValues(QStringLiteral("category"), QStringLiteral("t_fullname"), QStringLiteral("t_fullname in ("
414                 "SELECT DISTINCT(category.t_fullname) FROM category, suboperation WHERE suboperation.r_category_id=category.id UNION ALL "
415                 "SELECT DISTINCT(category.t_fullname) FROM category, budget WHERE budget.rc_category_id=category.id UNION ALL "
416                 "SELECT DISTINCT(category.t_fullname) FROM category, budgetrule WHERE budgetrule.rc_category_id=category.id UNION ALL "
417                 "SELECT DISTINCT(category.t_fullname) FROM category, budgetrule WHERE budgetrule.rc_category_id_target=category.id)"), categoriesUsed);
418 
419         for (int i = 0; i < categoriesUsed.count(); ++i) {  // Warning categoriesUsed is modified in the loop
420             QString cat = categoriesUsed.at(i);
421             categoriesUsed[i] = SKGServices::stringToSqlString(cat);
422             int pos = cat.lastIndexOf(OBJECTSEPARATOR);
423             if (pos != -1) {
424                 categoriesUsed.push_back(cat.left(pos));
425             }
426         }
427 
428         IFOK(err) {
429             QString sql;
430             if (!categoriesUsed.isEmpty()) {
431                 sql = "DELETE FROM category WHERE t_fullname NOT IN ('" % categoriesUsed.join(QStringLiteral("','")) % "')";
432             } else {
433                 sql = QStringLiteral("DELETE FROM category");
434             }
435             err = m_currentBankDocument->executeSqliteOrder(sql);
436         }
437     }
438 
439     // status bar
440     IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Unused categories deleted")))
441     else {
442         err.addError(ERR_FAIL, i18nc("Error message", "Unused categories deletion failed"));
443     }
444 
445     // Display error
446     SKGMainPanel::displayErrorMessage(err);
447 }
448 
449 #include <skgcategoriesplugin.moc>
450