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