1 /* HomeBank -- Free, easy, personal accounting for everyone.
2 * Copyright (C) 1995-2021 Maxime DOYEN
3 *
4 * This file is part of HomeBank.
5 *
6 * HomeBank is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * HomeBank is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "homebank.h"
21 #include "hb-assign.h"
22
23 #define MYDEBUG 0
24
25 #if MYDEBUG
26 #define DB(x) (x);
27 #else
28 #define DB(x);
29 #endif
30
31 /* our global datas */
32 extern struct HomeBank *GLOBALS;
33
34
35 /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
36
37 void
da_asg_free(Assign * item)38 da_asg_free(Assign *item)
39 {
40 DB( g_print("da_asg_free\n") );
41 if(item != NULL)
42 {
43 DB( g_print(" => %d, %s\n", item->key, item->search) );
44
45 g_free(item->search);
46 g_free(item->notes);
47 g_free(item);
48 }
49 }
50
51
52 Assign *
da_asg_malloc(void)53 da_asg_malloc(void)
54 {
55 DB( g_print("da_asg_malloc\n") );
56 return g_malloc0(sizeof(Assign));
57 }
58
59
60 void
da_asg_destroy(void)61 da_asg_destroy(void)
62 {
63 DB( g_print("da_asg_destroy\n") );
64 g_hash_table_destroy(GLOBALS->h_rul);
65 }
66
67
68 void
da_asg_new(void)69 da_asg_new(void)
70 {
71 DB( g_print("da_asg_new\n") );
72 GLOBALS->h_rul = g_hash_table_new_full(g_int_hash, g_int_equal, (GDestroyNotify)g_free, (GDestroyNotify)da_asg_free);
73 }
74
75
76 /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
da_asg_max_key_ghfunc(gpointer key,Assign * item,guint32 * max_key)77 static void da_asg_max_key_ghfunc(gpointer key, Assign *item, guint32 *max_key)
78 {
79 *max_key = MAX(*max_key, item->key);
80 }
81
da_asg_name_grfunc(gpointer key,Assign * item,gchar * name)82 static gboolean da_asg_name_grfunc(gpointer key, Assign *item, gchar *name)
83 {
84 if( name && item->search )
85 {
86 if(!strcasecmp(name, item->search))
87 return TRUE;
88 }
89 return FALSE;
90 }
91
92 /**
93 * da_asg_length:
94 *
95 * Return value: the number of elements
96 */
97 guint
da_asg_length(void)98 da_asg_length(void)
99 {
100 return g_hash_table_size(GLOBALS->h_rul);
101 }
102
103 /**
104 * da_asg_remove:
105 *
106 * delete an rul from the GHashTable
107 *
108 * Return value: TRUE if the key was found and deleted
109 *
110 */
111 gboolean
da_asg_remove(guint32 key)112 da_asg_remove(guint32 key)
113 {
114 DB( g_print("da_asg_remove %d\n", key) );
115
116 return g_hash_table_remove(GLOBALS->h_rul, &key);
117 }
118
119
120 //#1889659: ensure name != null/empty
121 static gboolean
da_asg_ensure_name(Assign * item)122 da_asg_ensure_name(Assign *item)
123 {
124 // (no assign) does not exists
125 if( item->key > 0 )
126 {
127 if( item->search == NULL || strlen(item->search) == 0 )
128 {
129 g_free(item->search);
130 item->search = g_strdup_printf("no name %d", item->key);
131 return TRUE;
132 }
133 }
134 return FALSE;
135 }
136
137
138 /**
139 * da_asg_insert:
140 *
141 * insert an rul into the GHashTable
142 *
143 * Return value: TRUE if inserted
144 *
145 */
146 gboolean
da_asg_insert(Assign * item)147 da_asg_insert(Assign *item)
148 {
149 guint32 *new_key;
150
151 DB( g_print("da_asg_insert\n") );
152
153 new_key = g_new0(guint32, 1);
154 *new_key = item->key;
155
156 //#1889659: ensure name != null/empty
157 da_asg_ensure_name(item);
158
159 g_hash_table_insert(GLOBALS->h_rul, new_key, item);
160
161 return TRUE;
162 }
163
164
165 /**
166 * da_asg_append:
167 *
168 * append a new rul into the GHashTable
169 *
170 * Return value: TRUE if inserted
171 *
172 */
173 gboolean
da_asg_append(Assign * item)174 da_asg_append(Assign *item)
175 {
176 Assign *existitem;
177 guint32 *new_key;
178
179 DB( g_print("da_asg_append\n") );
180
181 DB( g_print(" -> try append: %s\n", item->search) );
182
183 if( item->search != NULL )
184 {
185 /* ensure no duplicate */
186 existitem = da_asg_get_by_name( item->search );
187 if( existitem == NULL )
188 {
189 new_key = g_new0(guint32, 1);
190 *new_key = da_asg_get_max_key() + 1;
191 item->key = *new_key;
192 //added 5.3.3
193 item->pos = da_asg_length() + 1;
194
195 DB( g_print(" -> append id: %d\n", *new_key) );
196
197 g_hash_table_insert(GLOBALS->h_rul, new_key, item);
198 return TRUE;
199 }
200 }
201
202 DB( g_print(" -> %s already exist: %d\n", item->search, item->key) );
203
204 return FALSE;
205 }
206
207 /**
208 * da_asg_get_max_key:
209 *
210 * Get the biggest key from the GHashTable
211 *
212 * Return value: the biggest key value
213 *
214 */
215 guint32
da_asg_get_max_key(void)216 da_asg_get_max_key(void)
217 {
218 guint32 max_key = 0;
219
220 g_hash_table_foreach(GLOBALS->h_rul, (GHFunc)da_asg_max_key_ghfunc, &max_key);
221 return max_key;
222 }
223
224
225
226
227 /**
228 * da_asg_get_by_name:
229 *
230 * Get an rul structure by its name
231 *
232 * Return value: rul * or NULL if not found
233 *
234 */
235 Assign *
da_asg_get_by_name(gchar * name)236 da_asg_get_by_name(gchar *name)
237 {
238 DB( g_print("da_asg_get_by_name\n") );
239
240 return g_hash_table_find(GLOBALS->h_rul, (GHRFunc)da_asg_name_grfunc, name);
241 }
242
243
244
245 /**
246 * da_asg_get:
247 *
248 * Get an rul structure by key
249 *
250 * Return value: rul * or NULL if not found
251 *
252 */
253 Assign *
da_asg_get(guint32 key)254 da_asg_get(guint32 key)
255 {
256 DB( g_print("da_asg_get_rul\n") );
257
258 return g_hash_table_lookup(GLOBALS->h_rul, &key);
259 }
260
261
da_asg_consistency(Assign * item)262 void da_asg_consistency(Assign *item)
263 {
264 //5.2.4 we drop internal xfer here as it will disapear
265 //was not possible, but just in case
266 if( item->paymode == OLDPAYMODE_INTXFER )
267 item->paymode = PAYMODE_XFER;
268 }
269
270
271 /* = = = = = = = = = = = = = = = = = = = = */
272
273
da_asg_init_from_transaction(Assign * asg,Transaction * txn)274 Assign *da_asg_init_from_transaction(Assign *asg, Transaction *txn)
275 {
276 DB( g_print("\n[scheduled] init from txn\n") );
277
278 asg->search = g_strdup_printf("%s %s", _("**PREFILLED**"), txn->memo );
279 asg->flags |= (ASGF_DOPAY|ASGF_DOCAT|ASGF_DOMOD);
280 asg->kcat = txn->kcat;
281
282 if(!(txn->flags & OF_INTXFER))
283 {
284 asg->kpay = txn->kpay;
285 asg->paymode = txn->paymode;
286 }
287 return asg;
288 }
289
290
291 static gint
assign_glist_pos_compare_func(Assign * a,Assign * b)292 assign_glist_pos_compare_func(Assign *a, Assign *b)
293 {
294 return a->pos - b->pos;
295 }
296
297
298 static gint
assign_glist_key_compare_func(Assign * a,Assign * b)299 assign_glist_key_compare_func(Assign *a, Assign *b)
300 {
301 return a->key - b->key;
302 }
303
304
assign_glist_sorted(gint column)305 GList *assign_glist_sorted(gint column)
306 {
307 GList *list = g_hash_table_get_values(GLOBALS->h_rul);
308
309 switch(column)
310 {
311 case HB_GLIST_SORT_POS:
312 return g_list_sort(list, (GCompareFunc)assign_glist_pos_compare_func);
313 break;
314 default:
315 return g_list_sort(list, (GCompareFunc)assign_glist_key_compare_func);
316 break;
317 }
318 }
319
320
misc_text_match(gchar * text,gchar * searchtext,gboolean exact)321 static gboolean misc_text_match(gchar *text, gchar *searchtext, gboolean exact)
322 {
323 gboolean match = FALSE;
324
325 if(text == NULL)
326 return FALSE;
327
328 //DB( g_print("search %s in %s\n", rul->name, ope->memo) );
329 if( searchtext != NULL )
330 {
331 if( exact == TRUE )
332 {
333 if( g_strrstr(text, searchtext) != NULL )
334 {
335 DB( g_print("-- found case '%s'\n", searchtext) );
336 match = TRUE;
337 }
338 }
339 else
340 {
341 gchar *word = g_utf8_casefold(text, -1);
342 gchar *needle = g_utf8_casefold(searchtext, -1);
343
344 if( g_strrstr(word, needle) != NULL )
345 {
346 DB( g_print("-- found nocase '%s'\n", searchtext) );
347 match = TRUE;
348 }
349 g_free(word);
350 g_free(needle);
351 }
352 }
353
354 return match;
355 }
356
misc_regex_match(gchar * text,gchar * searchtext,gboolean exact)357 static gboolean misc_regex_match(gchar *text, gchar *searchtext, gboolean exact)
358 {
359 gboolean match = FALSE;
360
361 if(text == NULL)
362 return FALSE;
363
364 DB( g_print("-- match RE %s in %s\n", searchtext, text) );
365 if( searchtext != NULL )
366 {
367 match = g_regex_match_simple(searchtext, text, ((exact == TRUE)?0:G_REGEX_CASELESS) | G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY );
368 if (match == TRUE) { DB( g_print("-- found pattern '%s'\n", searchtext) ); }
369 }
370 return match;
371 }
372
373
transaction_auto_assign_eval_txn(GList * l_rul,Transaction * txn)374 static GList *transaction_auto_assign_eval_txn(GList *l_rul, Transaction *txn)
375 {
376 GList *ret_list = NULL;
377 GList *list;
378 gchar *text = NULL;
379
380 list = g_list_first(l_rul);
381 while (list != NULL)
382 {
383 Assign *rul = list->data;
384
385 text = txn->memo;
386 if(rul->field == 1) //payee
387 {
388 Payee *pay = da_pay_get(txn->kpay);
389 if(pay)
390 text = pay->name;
391 }
392
393 if( !(rul->flags & ASGF_REGEX) )
394 {
395 if( misc_text_match(text, rul->search, rul->flags & ASGF_EXACT) )
396 //TODO: perf must use preprend, see glib doc
397 ret_list = g_list_append(ret_list, rul);
398 }
399 else
400 {
401 if( misc_regex_match(text, rul->search, rul->flags & ASGF_EXACT) )
402 ret_list = g_list_append(ret_list, rul);
403 }
404
405 list = g_list_next(list);
406 }
407
408 DB( g_print("- %d rule(s) match on '%s'\n", g_list_length (ret_list), text) );
409
410 return ret_list;
411 }
412
413
transaction_auto_assign_eval(GList * l_rul,gchar * text)414 static GList *transaction_auto_assign_eval(GList *l_rul, gchar *text)
415 {
416 GList *ret_list = NULL;
417 GList *list;
418
419 list = g_list_first(l_rul);
420 while (list != NULL)
421 {
422 Assign *rul = list->data;
423
424 if( rul->field == 0 ) //memo
425 {
426 if( !(rul->flags & ASGF_REGEX) )
427 {
428 if( misc_text_match(text, rul->search, rul->flags & ASGF_EXACT) )
429 //TODO: perf must use preprend, see glib doc
430 ret_list = g_list_append(ret_list, rul);
431 }
432 else
433 {
434 if( misc_regex_match(text, rul->search, rul->flags & ASGF_EXACT) )
435 ret_list = g_list_append(ret_list, rul);
436 }
437 }
438 list = g_list_next(list);
439 }
440
441 DB( g_print("- %d rule(s) match on '%s'\n", g_list_length (ret_list), text) );
442
443 return ret_list;
444 }
445
446
transaction_auto_assign(GList * ope_list,guint32 kacc)447 guint transaction_auto_assign(GList *ope_list, guint32 kacc)
448 {
449 GList *l_ope;
450 GList *l_rul;
451 GList *l_match, *l_tmp;
452 guint changes = 0;
453
454 DB( g_print("\n[transaction] auto_assign\n") );
455
456 l_rul = assign_glist_sorted(HB_GLIST_SORT_POS);
457
458 l_ope = g_list_first(ope_list);
459 while (l_ope != NULL)
460 {
461 Transaction *ope = l_ope->data;
462 gboolean changed = FALSE;
463
464 DB( g_print("\n- curr txn '%s' : acc=%d, pay=%d, cat=%d, %s\n", ope->memo, ope->kacc, ope->kpay, ope->kcat, (ope->flags & OF_SPLIT) ? "is_split" : "" ) );
465
466 //#1215521: added kacc == 0
467 if( (kacc == ope->kacc || kacc == 0) )
468 {
469 if( !(ope->flags & OF_SPLIT) )
470 {
471 l_match = l_tmp = transaction_auto_assign_eval_txn(l_rul, ope);
472 while( l_tmp != NULL )
473 {
474 Assign *rul = l_tmp->data;
475
476 if( (ope->kpay == 0 && (rul->flags & ASGF_DOPAY)) || (rul->flags & ASGF_OVWPAY) )
477 {
478 if(ope->kpay != rul->kpay) { changed = TRUE; }
479 ope->kpay = rul->kpay;
480 }
481
482 if( (ope->kcat == 0 && (rul->flags & ASGF_DOCAT)) || (rul->flags & ASGF_OVWCAT) )
483 {
484 if(ope->kcat != rul->kcat) { changed = TRUE; }
485 ope->kcat = rul->kcat;
486 }
487
488 if( (ope->paymode == 0 && (rul->flags & ASGF_DOMOD)) || (rul->flags & ASGF_OVWMOD) )
489 {
490 //ugly hack - don't allow modify intxfer
491 if( !(ope->flags & OF_INTXFER) )
492 {
493 if(ope->paymode != rul->paymode) { changed = TRUE; }
494 ope->paymode = rul->paymode;
495 }
496 }
497 l_tmp = g_list_next(l_tmp);
498 }
499 g_list_free(l_match);
500 }
501 else
502 {
503 guint i, nbsplit = da_splits_length(ope->splits);
504
505 for(i=0;i<nbsplit;i++)
506 {
507 Split *split = da_splits_get(ope->splits, i);
508
509 DB( g_print("- eval split '%s'\n", split->memo) );
510
511 l_match = l_tmp = transaction_auto_assign_eval(l_rul, split->memo);
512 while( l_tmp != NULL )
513 {
514 Assign *rul = l_tmp->data;
515
516 //#1501144: check if user wants to set category in rule
517 if( (split->kcat == 0 || (rul->flags & ASGF_OVWCAT)) && (rul->flags & ASGF_DOCAT) )
518 {
519 if(split->kcat != rul->kcat) { changed = TRUE; }
520 split->kcat = rul->kcat;
521 }
522 l_tmp = g_list_next(l_tmp);
523 }
524 g_list_free(l_match);
525 }
526 }
527
528 if(changed == TRUE)
529 {
530 ope->flags |= OF_CHANGED;
531 changes++;
532 }
533 }
534
535 l_ope = g_list_next(l_ope);
536 }
537
538 g_list_free(l_rul);
539
540 return changes;
541 }
542
543
544
545
546 /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
547
548 #if MYDEBUG
549
550 static void
da_asg_debug_list_ghfunc(gpointer key,gpointer value,gpointer user_data)551 da_asg_debug_list_ghfunc(gpointer key, gpointer value, gpointer user_data)
552 {
553 guint32 *id = key;
554 Assign *item = value;
555
556 DB( g_print(" %d :: %s\n", *id, item->search) );
557
558 }
559
560 static void
da_asg_debug_list(void)561 da_asg_debug_list(void)
562 {
563
564 DB( g_print("\n** debug **\n") );
565
566 g_hash_table_foreach(GLOBALS->h_rul, da_asg_debug_list_ghfunc, NULL);
567
568 DB( g_print("\n** end debug **\n") );
569
570 }
571
572 #endif
573
574