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  * A skrooge plugin to search and process operations
8  *
9  * @author Stephane MANKOWSKI
10  */
11 #include "skgsearchplugin.h"
12 
13 #include <kaboutdata.h>
14 #include <kactioncollection.h>
15 #include <kpluginfactory.h>
16 #include <kstandardaction.h>
17 
18 #include <qdom.h>
19 
20 #include "skgalarmboardwidget.h"
21 #include "skgcategoryobject.h"
22 #include "skgdocumentbank.h"
23 #include "skgerror.h"
24 #include "skghtmlboardwidget.h"
25 #include "skgmainpanel.h"
26 #include "skgruleobject.h"
27 #include "skgsearch_settings.h"
28 #include "skgsearchpluginwidget.h"
29 #include "skgtraces.h"
30 #include "skgtransactionmng.h"
31 
32 /**
33  * This plugin factory.
34  */
K_PLUGIN_FACTORY(SKGSearchPluginFactory,registerPlugin<SKGSearchPlugin> ();)35 K_PLUGIN_FACTORY(SKGSearchPluginFactory, registerPlugin<SKGSearchPlugin>();)
36 
37 SKGSearchPlugin::SKGSearchPlugin(QWidget* iWidget, QObject* iParent, const QVariantList& /*iArg*/) :
38     SKGInterfacePlugin(iParent), m_currentBankDocument(nullptr)
39 {
40     Q_UNUSED(iWidget)
41     SKGTRACEINFUNC(10)
42     m_timer.setSingleShot(true);
43     connect(&m_timer, &QTimer::timeout, this, &SKGSearchPlugin::raiseAlarms, Qt::QueuedConnection);
44 }
45 
~SKGSearchPlugin()46 SKGSearchPlugin::~SKGSearchPlugin()
47 {
48     SKGTRACEINFUNC(10)
49     m_currentBankDocument = nullptr;
50 }
51 
getNbDashboardWidgets()52 int SKGSearchPlugin::getNbDashboardWidgets()
53 {
54     return 1;
55 }
56 
getDashboardWidgetTitle(int iIndex)57 QString SKGSearchPlugin::getDashboardWidgetTitle(int iIndex)
58 {
59     Q_UNUSED(iIndex)
60     return i18nc("Noun, alarms", "Alarms");
61 }
62 
getDashboardWidget(int iIndex)63 SKGBoardWidget* SKGSearchPlugin::getDashboardWidget(int iIndex)
64 {
65     Q_UNUSED(iIndex)
66     // Get QML mode for dashboard
67     KConfigSkeleton* skl = SKGMainPanel::getMainPanel()->getPluginByName(QStringLiteral("Dashboard plugin"))->getPreferenceSkeleton();
68     KConfigSkeletonItem* sklItem = skl->findItem(QStringLiteral("qmlmode"));
69     bool qml = sklItem->property().toBool();
70     if (qml) {
71         return new SKGHtmlBoardWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument,
72                                       getDashboardWidgetTitle(iIndex),
73                                       QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/html/default/alarm.qml")),
74                                       QStringList() << QStringLiteral("operation") << QStringLiteral("rule"));
75     }
76     return new SKGAlarmBoardWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument);
77 }
78 
setupActions(SKGDocument * iDocument)79 bool SKGSearchPlugin::setupActions(SKGDocument* iDocument)
80 {
81     SKGTRACEINFUNC(10)
82 
83     m_currentBankDocument = qobject_cast<SKGDocumentBank*>(iDocument);
84     if (m_currentBankDocument == nullptr) {
85         return false;
86     }
87 
88     setComponentName(QStringLiteral("skrooge_search"), title());
89     setXMLFile(QStringLiteral("skrooge_search.rc"));
90 
91     // Create yours actions here
92     // Execute on all operation
93     auto actExecuteAll = new QAction(SKGServices::fromTheme(QStringLiteral("system-run")), i18nc("Verb, action to execute", "Execute on all operations"), this);
94     connect(actExecuteAll, &QAction::triggered, this, [ = ] { execute(SKGRuleObject::ALL); });
95     registerGlobalAction(QStringLiteral("execute_all"), actExecuteAll, QStringList() << QStringLiteral("rule"), 1, -1, 501);
96 
97     // Execute on not checked
98     {
99         QStringList overlaycsv;
100         overlaycsv.push_back(QStringLiteral("document-import"));
101         auto actExecuteImported = new QAction(SKGServices::fromTheme(QStringLiteral("system-run"), overlaycsv), i18nc("Verb, action to execute", "Execute on not checked operations"), this);
102         connect(actExecuteImported, &QAction::triggered, this, [ = ] { execute(SKGRuleObject::NOTCHECKED); });
103         registerGlobalAction(QStringLiteral("execute_notchecked"), actExecuteImported, QStringList() << QStringLiteral("rule"), 1, -1, 502);
104     }
105 
106     // Execute on imported operation
107     {
108         QStringList overlaycsv;
109         overlaycsv.push_back(QStringLiteral("document-import"));
110         auto actExecuteImported = new QAction(SKGServices::fromTheme(QStringLiteral("system-run"), overlaycsv), i18nc("Verb, action to execute", "Execute on imported operations"), this);
111         connect(actExecuteImported, &QAction::triggered, this, [ = ] { execute(SKGRuleObject::IMPORTED); });
112         registerGlobalAction(QStringLiteral("execute_imported"), actExecuteImported, QStringList() << QStringLiteral("rule"), 1, -1, 502);
113     }
114 
115     // Execute on not validated
116     {
117         QStringList overlaycsv;
118         overlaycsv.push_back(QStringLiteral("dialog-ok"));
119         auto actExecuteNotValidated = new QAction(SKGServices::fromTheme(QStringLiteral("system-run"), overlaycsv), i18nc("Verb, action to execute", "Execute on not validated operations"), this);
120         connect(actExecuteNotValidated, &QAction::triggered, this, [ = ] { execute(SKGRuleObject::IMPORTEDNOTVALIDATE); });
121         registerGlobalAction(QStringLiteral("execute_not_validated"), actExecuteNotValidated, QStringList() << QStringLiteral("rule"), 1, -1, 503);
122     }
123 
124     // Search
125     QAction* actSearch = actionCollection()->addAction(KStandardAction::Find, QStringLiteral("edit_find"), this, SLOT(find()));
126     registerGlobalAction(QStringLiteral("edit_find"), actSearch);  // Global
127     auto actSearch2 = new QAction(actSearch->icon(), actSearch->text(), this);
128     connect(actSearch2, &QAction::triggered, this, &SKGSearchPlugin::find);
129     registerGlobalAction(QStringLiteral("edit_find_ctx"), actSearch2, QStringList() << QStringLiteral("account") << QStringLiteral("category") << QStringLiteral("refund") << QStringLiteral("payee") << QStringLiteral("operation") << QStringLiteral("suboperation"), 1, -1, 130);   // For contextual menus
130 
131     return true;
132 }
133 
refresh()134 void SKGSearchPlugin::refresh()
135 {
136     SKGTRACEINFUNC(10)
137     // Start alarm
138     if ((m_currentBankDocument != nullptr) && m_currentBankDocument->getMainDatabase() != nullptr) {
139         QString doc_id = m_currentBankDocument->getUniqueIdentifier();
140         if (m_docUniqueIdentifier != doc_id) {
141             m_docUniqueIdentifier = doc_id;
142 
143             raiseAlarms();
144         }
145     }
146 }
147 
raiseAlarms()148 void SKGSearchPlugin::raiseAlarms()
149 {
150     if (m_currentBankDocument != nullptr) {
151         SKGObjectBase::SKGListSKGObjectBase rules;
152         SKGError err = m_currentBankDocument->getObjects(QStringLiteral("v_rule"), QStringLiteral("t_action_type='A' ORDER BY i_ORDER"), rules);
153         int nb = rules.count();
154         if (!err && (nb != 0)) {
155             for (int i = 0; !err && i < nb; ++i) {
156                 SKGRuleObject rule(rules.at(i));
157                 err = rule.execute();
158             }
159         }
160 
161         // Display error
162         SKGMainPanel::displayErrorMessage(err);
163 
164         m_timer.start(skgsearch_settings::alarm_frequency() * 60 * 1000);
165     }
166 }
167 
execute(SKGRuleObject::ProcessMode iMode)168 void SKGSearchPlugin::execute(SKGRuleObject::ProcessMode iMode)
169 {
170     SKGError err;
171     SKGTRACEINFUNCRC(1, err)
172 
173     // Get rules
174     SKGObjectBase::SKGListSKGObjectBase rules = SKGMainPanel::getMainPanel()->getSelectedObjects();
175 
176     int nb = rules.count();
177     if (m_currentBankDocument != nullptr) {
178         SKGBEGINPROGRESSTRANSACTION(*m_currentBankDocument, i18nc("Noun, name of the user action", "Process execution"), err, nb)
179         for (int i = 0; !err && i < nb; ++i) {
180             SKGRuleObject rule(rules.at(i));
181             err = rule.execute(iMode);
182             IFOKDO(err, m_currentBankDocument->stepForward(i + 1))
183         }
184     }
185 
186     // status bar
187     IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Process executed")))
188     else {
189         err.addError(ERR_FAIL, i18nc("Error message",  "Process execution failed"));
190     }
191 
192     // Display error
193     SKGMainPanel::displayErrorMessage(err);
194 }
195 
find()196 void SKGSearchPlugin::find()
197 {
198     SKGError err;
199     SKGTRACEINFUNCRC(10, err)
200     if (SKGMainPanel::getMainPanel() != nullptr) {
201         SKGObjectBase::SKGListSKGObjectBase selection = SKGMainPanel::getMainPanel()->getSelectedObjects();
202 
203         QString xmlsearchcondition;
204         int nb = selection.count();
205         if (nb > 0) {
206             QString table = selection.at(0).getRealTable();
207             if (table == QStringLiteral("account")) {
208                 QDomDocument doc(QStringLiteral("SKGML"));
209                 QDomElement element = doc.createElement(QStringLiteral("element"));
210                 doc.appendChild(element);
211                 for (int i = 0; i < nb; ++i) {
212                     QDomElement elementLine = doc.createElement(QStringLiteral("element"));
213                     element.appendChild(elementLine);
214 
215                     QDomElement elementElement = doc.createElement(QStringLiteral("element"));
216                     elementLine.appendChild(elementElement);
217 
218                     elementElement.setAttribute(QStringLiteral("attribute"), QStringLiteral("t_ACCOUNT"));
219                     elementElement.setAttribute(QStringLiteral("operator"), QStringLiteral("#ATT#='#V1S#'"));
220                     elementElement.setAttribute(QStringLiteral("value"), selection.at(i).getAttribute(QStringLiteral("t_name")));
221                 }
222                 xmlsearchcondition = doc.toString();
223             } else if (table == QStringLiteral("category")) {
224                 QDomDocument doc(QStringLiteral("SKGML"));
225                 QDomElement element = doc.createElement(QStringLiteral("element"));
226                 doc.appendChild(element);
227                 for (int i = 0; i < nb; ++i) {
228                     QDomElement elementLine = doc.createElement(QStringLiteral("element"));
229                     element.appendChild(elementLine);
230 
231                     QDomElement elementElement = doc.createElement(QStringLiteral("element"));
232                     elementLine.appendChild(elementElement);
233 
234                     elementElement.setAttribute(QStringLiteral("attribute"), QStringLiteral("t_REALCATEGORY"));
235                     elementElement.setAttribute(QStringLiteral("operator"), QStringLiteral("#ATT#='#V1S#'"));
236                     SKGCategoryObject cat(selection.at(i));
237                     elementElement.setAttribute(QStringLiteral("value"), cat.getFullName());
238                 }
239                 xmlsearchcondition = doc.toString();
240             } else if (table == QStringLiteral("refund")) {
241                 QDomDocument doc(QStringLiteral("SKGML"));
242                 QDomElement element = doc.createElement(QStringLiteral("element"));
243                 doc.appendChild(element);
244                 for (int i = 0; i < nb; ++i) {
245                     QDomElement elementLine = doc.createElement(QStringLiteral("element"));
246                     element.appendChild(elementLine);
247 
248                     QDomElement elementElement = doc.createElement(QStringLiteral("element"));
249                     elementLine.appendChild(elementElement);
250 
251                     elementElement.setAttribute(QStringLiteral("attribute"), QStringLiteral("t_REALREFUND"));
252                     elementElement.setAttribute(QStringLiteral("operator"), QStringLiteral("#ATT#='#V1S#'"));
253                     elementElement.setAttribute(QStringLiteral("value"), selection.at(i).getAttribute(QStringLiteral("t_name")));
254                 }
255                 xmlsearchcondition = doc.toString();
256             } else if (table == QStringLiteral("payee")) {
257                 QDomDocument doc(QStringLiteral("SKGML"));
258                 QDomElement element = doc.createElement(QStringLiteral("element"));
259                 doc.appendChild(element);
260                 for (int i = 0; i < nb; ++i) {
261                     QDomElement elementLine = doc.createElement(QStringLiteral("element"));
262                     element.appendChild(elementLine);
263 
264                     QDomElement elementElement = doc.createElement(QStringLiteral("element"));
265                     elementLine.appendChild(elementElement);
266 
267                     elementElement.setAttribute(QStringLiteral("attribute"), QStringLiteral("t_PAYEE"));
268                     elementElement.setAttribute(QStringLiteral("operator"), QStringLiteral("#ATT#='#V1S#'"));
269                     elementElement.setAttribute(QStringLiteral("value"), selection.at(i).getAttribute(QStringLiteral("t_name")));
270                 }
271                 xmlsearchcondition = doc.toString();
272             } else if (table == QStringLiteral("operation") || table == QStringLiteral("suboperation")) {
273                 QStringList attributeForQuery;
274                 attributeForQuery << QStringLiteral("d_date") << QStringLiteral("t_number") << QStringLiteral("t_mode") << QStringLiteral("t_PAYEE") << QStringLiteral("t_comment") << QStringLiteral("t_REALCATEGORY") << QStringLiteral("t_status") << QStringLiteral("t_bookmarked") << QStringLiteral("t_imported") << QStringLiteral("t_ACCOUNT") << QStringLiteral("f_REALCURRENTAMOUNT") << QStringLiteral("t_REALREFUND");
275 
276                 QDomDocument doc(QStringLiteral("SKGML"));
277                 QDomElement element = doc.createElement(QStringLiteral("element"));
278                 doc.appendChild(element);
279                 for (int i = 0; i < nb; ++i) {
280                     QDomElement elementLine = doc.createElement(QStringLiteral("element"));
281                     element.appendChild(elementLine);
282 
283                     for (int j = 0; j < attributeForQuery.count(); ++j) {
284                         const QString& att = attributeForQuery.at(j);
285                         QString attRead = (att == QStringLiteral("t_REALCATEGORY") ? QStringLiteral("t_CATEGORY") : (att == QStringLiteral("f_REALCURRENTAMOUNT") ? QStringLiteral("f_CURRENTAMOUNT") : (att == QStringLiteral("t_REALREFUND") ? QStringLiteral("t_REFUND") : att)));
286 
287                         SKGObjectBase op(selection.at(i).getDocument(), QStringLiteral("v_operation_display_all"), selection.at(i).getID());
288                         op.load();
289 
290                         QString val = op.getAttribute(attRead);
291                         if (!val.isEmpty()) {
292                             QDomElement elementElement = doc.createElement(QStringLiteral("element"));
293                             elementLine.appendChild(elementElement);
294 
295                             elementElement.setAttribute(QStringLiteral("attribute"), att);
296                             elementElement.setAttribute(QStringLiteral("operator"), att.startsWith(QLatin1String("f_")) || att.startsWith(QLatin1String("i_")) ? QStringLiteral("#ATT#=#V1#") : QStringLiteral("#ATT#='#V1S#'"));
297                             elementElement.setAttribute(QStringLiteral("value"), val);
298                         }
299                     }
300                 }
301                 xmlsearchcondition = doc.toString();
302             }
303         }
304 
305         // Call search plugin
306         SKGMainPanel::getMainPanel()->openPage("skg://skrooge_search_plugin/?currentPage=0&xmlsearchcondition=" % SKGServices::encodeForUrl(xmlsearchcondition));
307     }
308 }
309 
getWidget()310 SKGTabPage* SKGSearchPlugin::getWidget()
311 {
312     SKGTRACEINFUNC(10)
313     return new SKGSearchPluginWidget(SKGMainPanel::getMainPanel(), m_currentBankDocument);
314 }
315 
getPreferenceWidget()316 QWidget* SKGSearchPlugin::getPreferenceWidget()
317 {
318     SKGTRACEINFUNC(10)
319     auto w = new QWidget();
320     ui.setupUi(w);
321     return w;
322 }
323 
getPreferenceSkeleton()324 KConfigSkeleton* SKGSearchPlugin::getPreferenceSkeleton()
325 {
326     return skgsearch_settings::self();
327 }
328 
title() const329 QString SKGSearchPlugin::title() const
330 {
331     return i18nc("Noun", "Search and process");
332 }
333 
icon() const334 QString SKGSearchPlugin::icon() const
335 {
336     return QStringLiteral("edit-find");
337 }
338 
toolTip() const339 QString SKGSearchPlugin::toolTip() const
340 {
341     return i18nc("Noun", "Search and process management");
342 }
343 
getOrder() const344 int SKGSearchPlugin::getOrder() const
345 {
346     return 35;
347 }
348 
tips() const349 QStringList SKGSearchPlugin::tips() const
350 {
351     QStringList output;
352     output.push_back(i18nc("Description of a tips", "<p>... skrooge can <a href=\"skg://skrooge_search_plugin\">search</a> and automatically process some operations.</p>"));
353     output.push_back(i18nc("Description of a tips", "<p>... you can create alarms based on <a href=\"skg://skrooge_search_plugin\">searches</a>.</p>"));
354     return output;
355 }
356 
isInPagesChooser() const357 bool SKGSearchPlugin::isInPagesChooser() const
358 {
359     return true;
360 }
361 
advice(const QStringList & iIgnoredAdvice)362 SKGAdviceList SKGSearchPlugin::advice(const QStringList& iIgnoredAdvice)
363 {
364     SKGTRACEINFUNC(10)
365     SKGAdviceList output;
366 
367     // Alarms
368     if (!iIgnoredAdvice.contains(QStringLiteral("skgsearchplugin_alarm"))) {
369         SKGObjectBase::SKGListSKGObjectBase rules;
370         SKGError err = m_currentBankDocument->getObjects(QStringLiteral("v_rule"), QStringLiteral("t_action_type='A' ORDER BY i_ORDER"), rules);
371         int nb = rules.count();
372         if (nb != 0) {
373             SKGServices::SKGUnitInfo primary = m_currentBankDocument->getPrimaryUnit();
374             SKGAdvice::SKGAdviceActionList autoCorrections;
375             for (int i = 0; !err && i < nb; ++i) {
376                 SKGRuleObject rule(rules.at(i));
377                 SKGRuleObject::SKGAlarmInfo alarm = rule.getAlarmInfo();
378                 if (alarm.Raised) {
379                     double percent = 100 * alarm.Amount / alarm.Limit;
380                     if (percent >= 70) {
381                         SKGAdvice ad;
382                         ad.setUUID("skgsearchplugin_alarm|" % SKGServices::intToString(rule.getID()));
383                         ad.setPriority(percent >= 90 ? 9 : 6);
384 
385                         QString msg = alarm.Message;
386                         // Build the message
387                         if (alarm.Message.contains(QLatin1String("%3"))) {
388                             msg = alarm.Message.arg(m_currentBankDocument->formatMoney(alarm.Amount, primary, false), m_currentBankDocument->formatMoney(alarm.Limit, primary, false), m_currentBankDocument->formatMoney(alarm.Amount - alarm.Limit, primary, false));
389                         } else if (alarm.Message.contains(QLatin1String("%2"))) {
390                             msg = alarm.Message.arg(m_currentBankDocument->formatMoney(alarm.Amount, primary, false), m_currentBankDocument->formatMoney(alarm.Limit, primary, false));
391                         } else if (alarm.Message.contains(QLatin1String("%1"))) {
392                             msg = alarm.Message.arg(m_currentBankDocument->formatMoney(alarm.Amount, primary, false));
393                         }
394 
395                         ad.setShortMessage(msg);
396                         ad.setLongMessage(i18nc("Advice on making the best (long)", "Take care to your alarms.<br> %1.", msg));
397                         autoCorrections.resize(0);
398                         {
399                             SKGAdvice::SKGAdviceAction a;
400                             a.Title = i18nc("Advice on making the best (action)", "Open operations corresponding to this alarm");
401                             a.IconName = QStringLiteral("quickopen");
402                             a.IsRecommended = false;
403                             autoCorrections.push_back(a);
404                         }
405                         ad.setAutoCorrections(autoCorrections);
406                         output.push_back(ad);
407                     }
408                 }
409             }
410         }
411     }
412 
413     return output;
414 }
415 
executeAdviceCorrection(const QString & iAdviceIdentifier,int iSolution)416 SKGError SKGSearchPlugin::executeAdviceCorrection(const QString& iAdviceIdentifier, int iSolution)
417 {
418     if ((m_currentBankDocument != nullptr) && iAdviceIdentifier.startsWith(QLatin1String("skgsearchplugin_alarm|"))) {
419         // Get parameters
420         QString id = iAdviceIdentifier.right(iAdviceIdentifier.length() - 22);
421         SKGSearchPluginWidget::open(SKGRuleObject(m_currentBankDocument, SKGServices::stringToInt(id)));
422         return SKGError();
423     }
424 
425     return SKGInterfacePlugin::executeAdviceCorrection(iAdviceIdentifier, iSolution);
426 }
427 
428 #include <skgsearchplugin.moc>
429