GnuCash  4.12-75-g66025ae738+
import-account-matcher.c
1 /********************************************************************\
2  * import-account-matcher.c - flexible account picker/matcher *
3  * *
4  * Copyright (C) 2002 Benoit GrĂ©goire <bock@step.polymtl.ca> *
5  * Copyright (C) 2012 Robert Fewell *
6  * *
7  * This program is free software; you can redistribute it and/or *
8  * modify it under the terms of the GNU General Public License as *
9  * published by the Free Software Foundation; either version 2 of *
10  * the License, or (at your option) any later version. *
11  * *
12  * This program is distributed in the hope that it will be useful, *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15  * GNU General Public License for more details. *
16  * *
17  * You should have received a copy of the GNU General Public License*
18  * along with this program; if not, contact: *
19  * *
20  * Free Software Foundation Voice: +1-617-542-5942 *
21  * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
22  * Boston, MA 02110-1301, USA gnu@gnu.org *
23 \********************************************************************/
31 #include <config.h>
32 
33 #include <gtk/gtk.h>
34 #include <glib/gi18n.h>
35 
36 #include "import-account-matcher.h"
37 #include "import-utilities.h"
38 #include "dialog-account.h"
39 #include "dialog-utils.h"
40 
41 #include "gnc-commodity.h"
42 #include "gnc-engine.h"
43 #include "gnc-prefs.h"
44 #include "gnc-tree-view-account.h"
45 #include "gnc-ui.h"
46 
47 static QofLogModule log_module = GNC_MOD_IMPORT;
48 
49 #define STATE_SECTION "dialogs/import/generic_matcher/account_matcher"
50 
51 #define GNC_PREFS_GROUP "dialogs.import.generic.account-picker"
52 
53 typedef struct
54 {
55  Account* partial_match;
56  int count;
57  const char* online_id;
59 
60 static Account*
61 partial_match_if_valid (AccountOnlineMatch *match)
62 {
63  if (match->partial_match && match->count == 1)
64  return match->partial_match;
65  else
66  PERR("Online ID %s partially matches %d accounts and fully matches none",
67  match->online_id, match->count);
68  return NULL;
69 }
70 
71 /*-******************************************************************\
72  * Functions needed by gnc_import_select_account
73  *
74 \********************************************************************/
78 static AccountPickerDialog* gnc_import_new_account_picker(void)
79 {
80  AccountPickerDialog* picker = g_new(AccountPickerDialog, 1);
81  picker->dialog = NULL;
82  picker->account_tree = NULL;
83  picker->account_tree_sw = NULL;
84  picker->auto_create = TRUE;
85  picker->account_human_description = NULL;
86  picker->account_online_id_value = NULL;
87  picker->account_online_id_label = NULL;
88  picker->new_account_default_commodity = NULL;
89  picker->new_account_default_type = 0;
90  picker->default_account = NULL;
91  picker->retAccount = NULL;
92  return picker;
93 }
94 
95 
96 /**************************************************
97  * test_acct_online_id_match
98  *
99  * test for match of account online_ids.
100  **************************************************/
101 static gpointer test_acct_online_id_match(Account *acct, gpointer data)
102 {
103  AccountOnlineMatch *match = (AccountOnlineMatch*)data;
104  char *acct_online_id = gnc_import_get_acc_online_id(acct);
105  int acct_len, match_len;
106 
107  if (acct_online_id == NULL || match->online_id == NULL)
108  {
109  if (acct_online_id)
110  g_free (acct_online_id);
111  return NULL;
112  }
113 
114  acct_len = strlen(acct_online_id);
115  match_len = strlen(match->online_id);
116 
117  if (acct_online_id[acct_len - 1] == ' ')
118  --acct_len;
119  if (match->online_id[match_len - 1] == ' ')
120  --match_len;
121 
122  if (strncmp (acct_online_id, match->online_id, acct_len) == 0)
123  {
124  if (strncmp(acct_online_id, match->online_id, match_len) == 0)
125  {
126  g_free (acct_online_id);
127  return (gpointer *) acct;
128  }
129  if (match->partial_match == NULL)
130  {
131  match->partial_match = acct;
132  ++match->count;
133  }
134  else
135  {
136  char *partial_online_id =
137  gnc_import_get_acc_online_id(match->partial_match);
138  int partial_len = strlen(partial_online_id);
139  if (partial_online_id[partial_len - 1] == ' ')
140  --partial_len;
141  /* Both partial_online_id and acct_online_id are substrings of
142  * match->online_id, but whichever is longer is the better match.
143  * Reset match->count to 1 just in case there was ambiguity on the
144  * shorter partial match.
145  */
146  if (partial_len < acct_len)
147  {
148  match->partial_match = acct;
149  match->count = 1;
150  }
151  /* If they're the same size then there are two accounts with the
152  * same online id and we don't know which one to select. Increment
153  * match->count to dissuade gnc_import_find_account from using
154  * match->online_id and log an error.
155  */
156  else if (partial_len == acct_len)
157  {
158  gchar *name1, *name2;
159  ++match->count;
160  name1 = gnc_account_get_full_name (match->partial_match);
161  name2 = gnc_account_get_full_name (acct);
162  PERR("Accounts %s and %s have the same online-id %s",
163  name1, name2, partial_online_id);
164  g_free (name1);
165  g_free (name2);
166  }
167  g_free (partial_online_id);
168  }
169  }
170 
171  g_free (acct_online_id);
172  return NULL;
173 }
174 
175 
176 /***********************************************************
177  * build_acct_tree
178  *
179  * build the account tree with the custom column, online_id
180  ************************************************************/
181 static void
182 build_acct_tree(AccountPickerDialog *picker)
183 {
184  GtkTreeView *account_tree;
185  GtkTreeViewColumn *col;
186 
187  /* Build a new account tree */
188  DEBUG("Begin");
189  account_tree = gnc_tree_view_account_new(FALSE);
190  picker->account_tree = GNC_TREE_VIEW_ACCOUNT(account_tree);
191  gtk_tree_view_set_headers_visible (account_tree, TRUE);
192  col = gnc_tree_view_find_column_by_name(GNC_TREE_VIEW(account_tree), "type");
193  g_object_set_data(G_OBJECT(col), DEFAULT_VISIBLE, GINT_TO_POINTER(1));
194 
195  /* Add our custom column. */
196  col = gnc_tree_view_account_add_property_column (picker->account_tree,
197  _("Account ID"), "online-id");
198  g_object_set_data(G_OBJECT(col), DEFAULT_VISIBLE, GINT_TO_POINTER(1));
199 
200  // the color background data function is part of the add_property_column
201  // function which will color the background based on preference setting
202 
203  gtk_container_add(GTK_CONTAINER(picker->account_tree_sw),
204  GTK_WIDGET(picker->account_tree));
205 
206  /* Configure the columns */
207  gnc_tree_view_configure_columns (GNC_TREE_VIEW(picker->account_tree));
208  g_object_set(account_tree,
209  "state-section", STATE_SECTION,
210  "show-column-menu", TRUE,
211  (gchar*) NULL);
212 }
213 
214 
215 /*******************************************************
216  * gnc_import_add_account
217  *
218  * Callback for when user clicks to create a new account
219  *******************************************************/
220 static void
221 gnc_import_add_account(GtkWidget *button, AccountPickerDialog *picker)
222 {
223  Account *selected_account, *new_account;
224  GList * valid_types = NULL;
225  GtkWindow *parent = NULL;
226 
227  if (picker->dialog != NULL)
228  parent = GTK_WINDOW (picker->dialog);
229 
230  /*DEBUG("Begin"); */
231  if (picker->new_account_default_type != ACCT_TYPE_NONE)
232  {
233  /*Yes, this is weird, but we really DO want to pass the value instead of the pointer...*/
234  valid_types = g_list_prepend(valid_types, GINT_TO_POINTER(picker->new_account_default_type));
235  }
236  selected_account = gnc_tree_view_account_get_selected_account(picker->account_tree);
237  new_account = gnc_ui_new_accounts_from_name_with_defaults (parent,
238  picker->account_human_description,
239  valid_types,
240  picker->new_account_default_commodity,
241  selected_account);
242  g_list_free(valid_types);
243  gnc_tree_view_account_set_selected_account(picker->account_tree, new_account);
244 }
245 
246 
247 /***********************************************************
248  * show_warning
249  *
250  * show the warning and disable OK button
251  ************************************************************/
252 static void
253 show_warning (AccountPickerDialog *picker, gchar *text)
254 {
255  gtk_label_set_text (GTK_LABEL(picker->warning), text);
256  gnc_label_set_alignment (picker->warning, 0.0, 0.5);
257  gtk_widget_show_all (GTK_WIDGET(picker->whbox));
258  g_free (text);
259 
260  gtk_widget_set_sensitive (picker->ok_button, FALSE); // disable OK button
261 }
262 
263 
264 /***********************************************************
265  * show_placeholder_warning
266  *
267  * show the warning when account is a place holder
268  ************************************************************/
269 static void
270 show_placeholder_warning (AccountPickerDialog *picker, const gchar *name)
271 {
272  gchar *text = g_strdup_printf (_("The account '%s' is a placeholder account and does not allow "
273  "transactions. Please choose a different account."), name);
274 
275  show_warning (picker, text);
276 }
277 
278 
279 /***********************************************************
280  * show_commodity_warning
281  *
282  * show the warning when account is a different commodity to that
283  * required
284  ************************************************************/
285 static void
286 show_commodity_warning (AccountPickerDialog *picker, const gchar *name)
287 {
288  const gchar *com_name = gnc_commodity_get_fullname (picker->new_account_default_commodity);
289  gchar *text = g_strdup_printf (_("The account '%s' has a different commodity to the "
290  "one required, '%s'. Please choose a different account."),
291  name, com_name);
292 
293  show_warning (picker, text);
294 }
295 
296 
297 /*******************************************************
298  * account_tree_row_changed_cb
299  *
300  * Callback for when user selects a different row
301  *******************************************************/
302 static void
303 account_tree_row_changed_cb (GtkTreeSelection *selection,
304  AccountPickerDialog *picker)
305 {
306  Account *sel_account = gnc_tree_view_account_get_selected_account (picker->account_tree);
307 
308  if (!sel_account)
309  {
310  gtk_widget_hide (GTK_WIDGET(picker->whbox)); // hide the warning
311  gtk_widget_set_sensitive (picker->ok_button, FALSE); // disable OK button
312  return;
313  }
314 
315  gtk_widget_set_sensitive (picker->ok_button, TRUE); // enable OK button
316 
317  /* See if the selected account is a placeholder. */
318  if (sel_account && xaccAccountGetPlaceholder (sel_account))
319  {
320  const gchar *retval_name = xaccAccountGetName (sel_account);
321 
322  show_placeholder_warning (picker, retval_name);
323  }
324  else if (picker->new_account_default_commodity &&
326  picker->new_account_default_commodity))) // check commodity
327  {
328  const gchar *retval_name = xaccAccountGetName (sel_account);
329  show_commodity_warning (picker, retval_name);
330  }
331  else
332  gtk_widget_hide (GTK_WIDGET(picker->whbox)); // hide the warning
333 }
334 
335 static gboolean
336 account_tree_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
337 {
338  // Expand the tree when the user starts typing, this will allow sub-accounts to be found.
339  if (event->length == 0)
340  return FALSE;
341 
342  switch (event->keyval)
343  {
344  case GDK_KEY_plus:
345  case GDK_KEY_minus:
346  case GDK_KEY_asterisk:
347  case GDK_KEY_slash:
348  case GDK_KEY_KP_Add:
349  case GDK_KEY_KP_Subtract:
350  case GDK_KEY_KP_Multiply:
351  case GDK_KEY_KP_Divide:
352  case GDK_KEY_Up:
353  case GDK_KEY_KP_Up:
354  case GDK_KEY_Down:
355  case GDK_KEY_KP_Down:
356  case GDK_KEY_Home:
357  case GDK_KEY_KP_Home:
358  case GDK_KEY_End:
359  case GDK_KEY_KP_End:
360  case GDK_KEY_Page_Up:
361  case GDK_KEY_KP_Page_Up:
362  case GDK_KEY_Page_Down:
363  case GDK_KEY_KP_Page_Down:
364  case GDK_KEY_Right:
365  case GDK_KEY_Left:
366  case GDK_KEY_KP_Right:
367  case GDK_KEY_KP_Left:
368  case GDK_KEY_space:
369  case GDK_KEY_KP_Space:
370  case GDK_KEY_backslash:
371  case GDK_KEY_Return:
372  case GDK_KEY_ISO_Enter:
373  case GDK_KEY_KP_Enter:
374  return FALSE;
375  break;
376  default:
377  gtk_tree_view_expand_all (GTK_TREE_VIEW(user_data));
378  return FALSE;
379  }
380  return FALSE;
381 }
382 
383 
384 /*******************************************************
385  * account_tree_row_activated_cb
386  *
387  * Callback for when user double clicks on an account
388  *******************************************************/
389 static void
390 account_tree_row_activated_cb(GtkTreeView *view, GtkTreePath *path,
391  GtkTreeViewColumn *column,
392  AccountPickerDialog *picker)
393 {
394  gtk_dialog_response(GTK_DIALOG(picker->dialog), GTK_RESPONSE_OK);
395 }
396 
397 
398 /*******************************************************
399  * gnc_import_select_account
400  *
401  * Main call for use with a dialog
402  *******************************************************/
403 Account * gnc_import_select_account(GtkWidget *parent,
404  const gchar * account_online_id_value,
405  gboolean auto_create,
406  const gchar * account_human_description,
407  const gnc_commodity * new_account_default_commodity,
408  GNCAccountType new_account_default_type,
409  Account * default_selection,
410  gboolean * ok_pressed)
411 {
412 #define ACCOUNT_DESCRIPTION_MAX_SIZE 255
413  AccountPickerDialog * picker;
414  gint response;
415  Account * retval = NULL;
416  const gchar *retval_name = NULL;
417  GtkBuilder *builder;
418  GtkTreeSelection *selection;
419  GtkWidget * online_id_label;
420  gchar account_description_text[ACCOUNT_DESCRIPTION_MAX_SIZE + 1] = "";
421  gboolean ok_pressed_retval = FALSE;
422 
423  ENTER("Default commodity received: %s", gnc_commodity_get_fullname( new_account_default_commodity));
424  DEBUG("Default account type received: %s", xaccAccountGetTypeStr( new_account_default_type));
425  picker = g_new0(AccountPickerDialog, 1);
426 
427  picker->account_online_id_value = account_online_id_value;
428  picker->account_human_description = account_human_description;
429  picker->new_account_default_commodity = new_account_default_commodity;
430  picker->new_account_default_type = new_account_default_type;
431 
432  /*DEBUG("Looking for account with online_id: \"%s\"", account_online_id_value);*/
433  if (account_online_id_value != NULL)
434  {
435  AccountOnlineMatch match = {NULL, 0, account_online_id_value};
436  retval =
437  gnc_account_foreach_descendant_until(gnc_get_current_root_account (),
438  test_acct_online_id_match,
439  (void*)&match);
440  if (!retval && match.count == 1 &&
441  new_account_default_type == ACCT_TYPE_NONE)
442  retval = match.partial_match;
443  }
444  if (retval == NULL && auto_create != 0)
445  {
446  /* load the interface */
447  builder = gtk_builder_new();
448  gnc_builder_add_from_file (builder, "dialog-import.glade", "account_new_icon");
449  gnc_builder_add_from_file (builder, "dialog-import.glade", "account_picker_dialog");
450  /* connect the signals in the interface */
451  if (builder == NULL)
452  {
453  PERR("Error opening the glade builder interface");
454  }
455  picker->dialog = GTK_WIDGET(gtk_builder_get_object (builder, "account_picker_dialog"));
456  picker->whbox = GTK_WIDGET(gtk_builder_get_object (builder, "warning_hbox"));
457  picker->warning = GTK_WIDGET(gtk_builder_get_object (builder, "warning_label"));
458  picker->ok_button = GTK_WIDGET(gtk_builder_get_object (builder, "okbutton"));
459 
460  if (parent)
461  gtk_window_set_transient_for (GTK_WINDOW (picker->dialog),
462  GTK_WINDOW (parent));
463 
464  gnc_restore_window_size (GNC_PREFS_GROUP,
465  GTK_WINDOW(picker->dialog), GTK_WINDOW (parent));
466 
467  picker->account_tree_sw = GTK_WIDGET(gtk_builder_get_object (builder, "account_tree_sw"));
468  online_id_label = GTK_WIDGET(gtk_builder_get_object (builder, "online_id_label"));
469 
470  //printf("gnc_import_select_account(): Fin get widget\n");
471 
472  if (account_human_description != NULL)
473  {
474  strncat(account_description_text, account_human_description,
475  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
476  strncat(account_description_text, "\n",
477  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
478  }
479  if (account_online_id_value != NULL)
480  {
481  strncat(account_description_text, _("(Full account ID: "),
482  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
483  strncat(account_description_text, account_online_id_value,
484  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
485  strncat(account_description_text, ")",
486  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
487  }
488  gtk_label_set_text((GtkLabel*)online_id_label, account_description_text);
489  build_acct_tree(picker);
490  gtk_window_set_modal(GTK_WINDOW(picker->dialog), TRUE);
491  g_signal_connect(picker->account_tree, "row-activated",
492  G_CALLBACK(account_tree_row_activated_cb), picker);
493 
494  /* Connect key press event so we can expand the tree when the user starts typing, allowing
495  * any subaccount to match */
496  g_signal_connect (picker->account_tree, "key-press-event", G_CALLBACK (account_tree_key_press_cb), picker->account_tree);
497 
498  selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(picker->account_tree));
499  g_signal_connect(selection, "changed",
500  G_CALLBACK(account_tree_row_changed_cb), picker);
501 
502  gnc_tree_view_account_set_selected_account(picker->account_tree, default_selection);
503 
504  do
505  {
506  response = gtk_dialog_run(GTK_DIALOG(picker->dialog));
507  switch (response)
508  {
509  case GNC_RESPONSE_NEW:
510  gnc_import_add_account(NULL, picker);
511  response = GTK_RESPONSE_OK;
512  /* no break */
513 
514  case GTK_RESPONSE_OK:
515  retval = gnc_tree_view_account_get_selected_account(picker->account_tree);
516  if (retval == NULL)
517  {
518  response = GNC_RESPONSE_NEW;
519  break;
520  }
521  if (retval)
522  retval_name = xaccAccountGetName(retval);
523  if (!retval_name)
524  retval_name = "(null)";
525  DEBUG("Selected account %p, %s", retval, retval_name);
526 
527  /* See if the selected account is a placeholder. */
528  if (retval && xaccAccountGetPlaceholder (retval))
529  {
530  show_placeholder_warning (picker, retval_name);
531  response = GNC_RESPONSE_NEW;
532  break;
533  }
534  else if (picker->new_account_default_commodity &&
536  picker->new_account_default_commodity))) // check commodity
537  {
538  show_commodity_warning (picker, retval_name);
539  response = GNC_RESPONSE_NEW;
540  break;
541  }
542 
543  if ( account_online_id_value != NULL)
544  {
545  gnc_import_set_acc_online_id(retval, account_online_id_value);
546  }
547  ok_pressed_retval = TRUE;
548  break;
549 
550  default:
551  ok_pressed_retval = FALSE;
552  break;
553  }
554  }
555  while (response == GNC_RESPONSE_NEW);
556 
557  g_object_unref(G_OBJECT(builder));
558  gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(picker->dialog));
559  gtk_widget_destroy(picker->dialog);
560  }
561  else
562  {
563  retval_name = retval ? xaccAccountGetName(retval) : NULL;
564  ok_pressed_retval = TRUE; /* There was no dialog involved, so the computer "pressed" ok */
565  }
566  /*FIXME: DEBUG("WRITEME: gnc_import_select_account() Here we should check if account type is compatible, currency matches, etc.\n"); */
567  g_free(picker);
568  /*DEBUG("Return value: %p%s%s%s",retval,", account name:",xaccAccountGetName(retval),"\n");*/
569  if (ok_pressed != NULL)
570  {
571  *ok_pressed = ok_pressed_retval;
572  }
573  LEAVE("Selected account %p, %s", retval, retval_name ? retval_name : "(null)");
574  return retval;
575 }
576 
GtkTreeViewColumn * gnc_tree_view_account_add_property_column(GncTreeViewAccount *view, const gchar *column_title, const gchar *propname)
Add a new column to the set of columns in an account tree view.
This file contains the functions to present a gui to the user for creating a new account or editing a...
#define DEBUG(format, args...)
Print a debugging message.
Definition: qoflog.h:264
gboolean gnc_commodity_equal(const gnc_commodity *a, const gnc_commodity *b)
This routine returns TRUE if the two commodities are equal.
gpointer gnc_account_foreach_descendant_until(const Account *acc, AccountCb2 thunk, gpointer user_data)
This method will traverse all children of this accounts and their descendants, calling &#39;func&#39; on each...
Definition: Account.cpp:3253
Account * gnc_import_select_account(GtkWidget *parent, const gchar *account_online_id_value, gboolean auto_create, const gchar *account_human_description, const gnc_commodity *new_account_default_commodity, GNCAccountType new_account_default_type, Account *default_selection, gboolean *ok_pressed)
Must be called with a string containing a unique identifier for the account.
Generic and very flexible account matcher/picker.
#define PERR(format, args...)
Log a serious error.
Definition: qoflog.h:244
GtkTreeViewColumn * gnc_tree_view_find_column_by_name(GncTreeView *view, const gchar *wanted)
Find a tree column given the "pref name" used with saved state.
#define ENTER(format, args...)
Print a function entry debugging message.
Definition: qoflog.h:272
gchar * gnc_account_get_full_name(const Account *account)
The gnc_account_get_full_name routine returns the fully qualified name of the account using the given...
Definition: Account.cpp:3308
GtkTreeView implementation for gnucash account tree.
GtkTreeView * gnc_tree_view_account_new(gboolean show_root)
Create a new account tree view.
void gnc_tree_view_configure_columns(GncTreeView *view)
Make all the correct columns visible, respecting their default visibility setting, their "always" visibility setting, and the last saved state if available.
const char * gnc_commodity_get_fullname(const gnc_commodity *cm)
Retrieve the full name for the specified commodity.
All type declarations for the whole Gnucash engine.
GNCAccountType
The account types are used to determine how the transaction data in the account is displayed...
Definition: Account.h:105
Generic api to store and retrieve preferences.
void gnc_tree_view_account_set_selected_account(GncTreeViewAccount *view, Account *account)
This function selects an account in the account tree view.
Account * gnc_ui_new_accounts_from_name_with_defaults(GtkWindow *parent, const char *name, GList *valid_types, const gnc_commodity *default_commodity, Account *parent_acct)
Display a modal window for creating a new account.
gnc_commodity * xaccAccountGetCommodity(const Account *acc)
Get the account&#39;s commodity.
Definition: Account.cpp:3448
gboolean xaccAccountGetPlaceholder(const Account *acc)
Get the "placeholder" flag for an account.
Definition: Account.cpp:4262
Account * gnc_tree_view_account_get_selected_account(GncTreeViewAccount *view)
This function returns the account associated with the selected item in the account tree view...
Utility functions for writing import modules.
#define LEAVE(format, args...)
Print a function exit debugging message.
Definition: qoflog.h:282
const char * xaccAccountGetName(const Account *acc)
Get the account&#39;s name.
Definition: Account.cpp:3301
const char * xaccAccountGetTypeStr(GNCAccountType type)
The xaccAccountGetTypeStr() routine returns a string suitable for use in the GUI/Interface.
Definition: Account.cpp:4518
Not a type.
Definition: Account.h:108
Commodity handling public routines.