GnuCash  4.8a-134-g214de30c7+
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  const 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  return NULL;
109 
110  acct_len = strlen(acct_online_id);
111  match_len = strlen(match->online_id);
112 
113  if (acct_online_id[acct_len - 1] == ' ')
114  --acct_len;
115  if (match->online_id[match_len - 1] == ' ')
116  --match_len;
117 
118  if (strncmp (acct_online_id, match->online_id, acct_len) == 0)
119  {
120  if (strncmp(acct_online_id, match->online_id, match_len) == 0)
121  return (gpointer *) acct;
122  if (match->partial_match == NULL)
123  {
124  match->partial_match = acct;
125  ++match->count;
126  }
127  else
128  {
129  const char *partial_online_id =
130  gnc_import_get_acc_online_id(match->partial_match);
131  int partial_len = strlen(partial_online_id);
132  if (partial_online_id[partial_len - 1] == ' ')
133  --partial_len;
134  /* Both partial_online_id and acct_online_id are substrings of
135  * match->online_id, but whichever is longer is the better match.
136  * Reset match->count to 1 just in case there was ambiguity on the
137  * shorter partial match.
138  */
139  if (partial_len < acct_len)
140  {
141  match->partial_match = acct;
142  match->count = 1;
143  }
144  /* If they're the same size then there are two accounts with the
145  * same online id and we don't know which one to select. Increment
146  * match->count to dissuade gnc_import_find_account from using
147  * match->online_id and log an error.
148  */
149  else if (partial_len == acct_len)
150  {
151  gchar *name1, *name2;
152  ++match->count;
153  name1 = gnc_account_get_full_name (match->partial_match);
154  name2 = gnc_account_get_full_name (acct);
155  PERR("Accounts %s and %s have the same online-id %s",
156  name1, name2, partial_online_id);
157  g_free (name1);
158  g_free (name2);
159  }
160  }
161  }
162 
163  return NULL;
164 }
165 
166 
167 /***********************************************************
168  * build_acct_tree
169  *
170  * build the account tree with the custom column, online_id
171  ************************************************************/
172 static void
173 build_acct_tree(AccountPickerDialog *picker)
174 {
175  GtkTreeView *account_tree;
176  GtkTreeViewColumn *col;
177 
178  /* Build a new account tree */
179  DEBUG("Begin");
180  account_tree = gnc_tree_view_account_new(FALSE);
181  picker->account_tree = GNC_TREE_VIEW_ACCOUNT(account_tree);
182  gtk_tree_view_set_headers_visible (account_tree, TRUE);
183  col = gnc_tree_view_find_column_by_name(GNC_TREE_VIEW(account_tree), "type");
184  g_object_set_data(G_OBJECT(col), DEFAULT_VISIBLE, GINT_TO_POINTER(1));
185 
186  /* Add our custom column. */
187  col = gnc_tree_view_account_add_property_column (picker->account_tree,
188  _("Account ID"), "online-id");
189  g_object_set_data(G_OBJECT(col), DEFAULT_VISIBLE, GINT_TO_POINTER(1));
190 
191  // the color background data function is part of the add_property_column
192  // function which will color the background based on preference setting
193 
194  gtk_container_add(GTK_CONTAINER(picker->account_tree_sw),
195  GTK_WIDGET(picker->account_tree));
196 
197  /* Configure the columns */
198  gnc_tree_view_configure_columns (GNC_TREE_VIEW(picker->account_tree));
199  g_object_set(account_tree,
200  "state-section", STATE_SECTION,
201  "show-column-menu", TRUE,
202  (gchar*) NULL);
203 }
204 
205 
206 /*******************************************************
207  * gnc_import_add_account
208  *
209  * Callback for when user clicks to create a new account
210  *******************************************************/
211 static void
212 gnc_import_add_account(GtkWidget *button, AccountPickerDialog *picker)
213 {
214  Account *selected_account, *new_account;
215  GList * valid_types = NULL;
216  GtkWindow *parent = NULL;
217 
218  if (picker->dialog != NULL)
219  parent = GTK_WINDOW (picker->dialog);
220 
221  /*DEBUG("Begin"); */
222  if (picker->new_account_default_type != ACCT_TYPE_NONE)
223  {
224  /*Yes, this is weird, but we really DO want to pass the value instead of the pointer...*/
225  valid_types = g_list_prepend(valid_types, GINT_TO_POINTER(picker->new_account_default_type));
226  }
227  selected_account = gnc_tree_view_account_get_selected_account(picker->account_tree);
228  new_account = gnc_ui_new_accounts_from_name_with_defaults (parent,
229  picker->account_human_description,
230  valid_types,
231  picker->new_account_default_commodity,
232  selected_account);
233  g_list_free(valid_types);
234  gnc_tree_view_account_set_selected_account(picker->account_tree, new_account);
235 }
236 
237 
238 /***********************************************************
239  * show_warning
240  *
241  * show the warning and disable OK button
242  ************************************************************/
243 static void
244 show_warning (AccountPickerDialog *picker, gchar *text)
245 {
246  gtk_label_set_text (GTK_LABEL(picker->warning), text);
247  gnc_label_set_alignment (picker->warning, 0.0, 0.5);
248  gtk_widget_show_all (GTK_WIDGET(picker->whbox));
249  g_free (text);
250 
251  gtk_widget_set_sensitive (picker->ok_button, FALSE); // disable OK button
252 }
253 
254 
255 /***********************************************************
256  * show_placeholder_warning
257  *
258  * show the warning when account is a place holder
259  ************************************************************/
260 static void
261 show_placeholder_warning (AccountPickerDialog *picker, const gchar *name)
262 {
263  gchar *text = g_strdup_printf (_("The account '%s' is a placeholder account and does not allow "
264  "transactions. Please choose a different account."), name);
265 
266  show_warning (picker, text);
267 }
268 
269 
270 /***********************************************************
271  * show_commodity_warning
272  *
273  * show the warning when account is a different commodity to that
274  * required
275  ************************************************************/
276 static void
277 show_commodity_warning (AccountPickerDialog *picker, const gchar *name)
278 {
279  const gchar *com_name = gnc_commodity_get_fullname (picker->new_account_default_commodity);
280  gchar *text = g_strdup_printf (_("The account '%s' has a different commodity to the "
281  "one required, '%s'. Please choose a different account."),
282  name, com_name);
283 
284  show_warning (picker, text);
285 }
286 
287 
288 /*******************************************************
289  * account_tree_row_changed_cb
290  *
291  * Callback for when user selects a different row
292  *******************************************************/
293 static void
294 account_tree_row_changed_cb (GtkTreeSelection *selection,
295  AccountPickerDialog *picker)
296 {
297  Account *sel_account = gnc_tree_view_account_get_selected_account (picker->account_tree);
298 
299  if (!sel_account)
300  {
301  gtk_widget_hide (GTK_WIDGET(picker->whbox)); // hide the warning
302  gtk_widget_set_sensitive (picker->ok_button, FALSE); // disable OK button
303  return;
304  }
305 
306  gtk_widget_set_sensitive (picker->ok_button, TRUE); // enable OK button
307 
308  /* See if the selected account is a placeholder. */
309  if (sel_account && xaccAccountGetPlaceholder (sel_account))
310  {
311  const gchar *retval_name = xaccAccountGetName (sel_account);
312 
313  show_placeholder_warning (picker, retval_name);
314  }
315  else if (picker->new_account_default_commodity &&
317  picker->new_account_default_commodity))) // check commodity
318  {
319  const gchar *retval_name = xaccAccountGetName (sel_account);
320  show_commodity_warning (picker, retval_name);
321  }
322  else
323  gtk_widget_hide (GTK_WIDGET(picker->whbox)); // hide the warning
324 }
325 
326 static gboolean
327 account_tree_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
328 {
329  // Expand the tree when the user starts typing, this will allow sub-accounts to be found.
330  if (event->length == 0)
331  return FALSE;
332 
333  switch (event->keyval)
334  {
335  case GDK_KEY_plus:
336  case GDK_KEY_minus:
337  case GDK_KEY_asterisk:
338  case GDK_KEY_slash:
339  case GDK_KEY_KP_Add:
340  case GDK_KEY_KP_Subtract:
341  case GDK_KEY_KP_Multiply:
342  case GDK_KEY_KP_Divide:
343  case GDK_KEY_Up:
344  case GDK_KEY_KP_Up:
345  case GDK_KEY_Down:
346  case GDK_KEY_KP_Down:
347  case GDK_KEY_Home:
348  case GDK_KEY_KP_Home:
349  case GDK_KEY_End:
350  case GDK_KEY_KP_End:
351  case GDK_KEY_Page_Up:
352  case GDK_KEY_KP_Page_Up:
353  case GDK_KEY_Page_Down:
354  case GDK_KEY_KP_Page_Down:
355  case GDK_KEY_Right:
356  case GDK_KEY_Left:
357  case GDK_KEY_KP_Right:
358  case GDK_KEY_KP_Left:
359  case GDK_KEY_space:
360  case GDK_KEY_KP_Space:
361  case GDK_KEY_backslash:
362  case GDK_KEY_Return:
363  case GDK_KEY_ISO_Enter:
364  case GDK_KEY_KP_Enter:
365  return FALSE;
366  break;
367  default:
368  gtk_tree_view_expand_all (GTK_TREE_VIEW(user_data));
369  return FALSE;
370  }
371  return FALSE;
372 }
373 
374 
375 /*******************************************************
376  * account_tree_row_activated_cb
377  *
378  * Callback for when user double clicks on an account
379  *******************************************************/
380 static void
381 account_tree_row_activated_cb(GtkTreeView *view, GtkTreePath *path,
382  GtkTreeViewColumn *column,
383  AccountPickerDialog *picker)
384 {
385  gtk_dialog_response(GTK_DIALOG(picker->dialog), GTK_RESPONSE_OK);
386 }
387 
388 
389 /*******************************************************
390  * gnc_import_select_account
391  *
392  * Main call for use with a dialog
393  *******************************************************/
394 Account * gnc_import_select_account(GtkWidget *parent,
395  const gchar * account_online_id_value,
396  gboolean auto_create,
397  const gchar * account_human_description,
398  const gnc_commodity * new_account_default_commodity,
399  GNCAccountType new_account_default_type,
400  Account * default_selection,
401  gboolean * ok_pressed)
402 {
403 #define ACCOUNT_DESCRIPTION_MAX_SIZE 255
404  AccountPickerDialog * picker;
405  gint response;
406  Account * retval = NULL;
407  const gchar *retval_name = NULL;
408  GtkBuilder *builder;
409  GtkTreeSelection *selection;
410  GtkWidget * online_id_label;
411  gchar account_description_text[ACCOUNT_DESCRIPTION_MAX_SIZE + 1] = "";
412  gboolean ok_pressed_retval = FALSE;
413 
414  ENTER("Default commodity received: %s", gnc_commodity_get_fullname( new_account_default_commodity));
415  DEBUG("Default account type received: %s", xaccAccountGetTypeStr( new_account_default_type));
416  picker = g_new0(AccountPickerDialog, 1);
417 
418  picker->account_online_id_value = account_online_id_value;
419  picker->account_human_description = account_human_description;
420  picker->new_account_default_commodity = new_account_default_commodity;
421  picker->new_account_default_type = new_account_default_type;
422 
423  /*DEBUG("Looking for account with online_id: \"%s\"", account_online_id_value);*/
424  if (account_online_id_value != NULL)
425  {
426  AccountOnlineMatch match = {NULL, 0, account_online_id_value};
427  retval =
428  gnc_account_foreach_descendant_until(gnc_get_current_root_account (),
429  test_acct_online_id_match,
430  (void*)&match);
431  if (!retval && match.count == 1 &&
432  new_account_default_type == ACCT_TYPE_NONE)
433  retval = match.partial_match;
434  }
435  if (retval == NULL && auto_create != 0)
436  {
437  /* load the interface */
438  builder = gtk_builder_new();
439  gnc_builder_add_from_file (builder, "dialog-import.glade", "account_new_icon");
440  gnc_builder_add_from_file (builder, "dialog-import.glade", "account_picker_dialog");
441  /* connect the signals in the interface */
442  if (builder == NULL)
443  {
444  PERR("Error opening the glade builder interface");
445  }
446  picker->dialog = GTK_WIDGET(gtk_builder_get_object (builder, "account_picker_dialog"));
447  picker->whbox = GTK_WIDGET(gtk_builder_get_object (builder, "warning_hbox"));
448  picker->warning = GTK_WIDGET(gtk_builder_get_object (builder, "warning_label"));
449  picker->ok_button = GTK_WIDGET(gtk_builder_get_object (builder, "okbutton"));
450 
451  if (parent)
452  gtk_window_set_transient_for (GTK_WINDOW (picker->dialog),
453  GTK_WINDOW (parent));
454 
455  gnc_restore_window_size (GNC_PREFS_GROUP,
456  GTK_WINDOW(picker->dialog), GTK_WINDOW (parent));
457 
458  picker->account_tree_sw = GTK_WIDGET(gtk_builder_get_object (builder, "account_tree_sw"));
459  online_id_label = GTK_WIDGET(gtk_builder_get_object (builder, "online_id_label"));
460 
461  //printf("gnc_import_select_account(): Fin get widget\n");
462 
463  if (account_human_description != NULL)
464  {
465  strncat(account_description_text, account_human_description,
466  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
467  strncat(account_description_text, "\n",
468  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
469  }
470  if (account_online_id_value != NULL)
471  {
472  strncat(account_description_text, _("(Full account ID: "),
473  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
474  strncat(account_description_text, account_online_id_value,
475  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
476  strncat(account_description_text, ")",
477  ACCOUNT_DESCRIPTION_MAX_SIZE - strlen(account_description_text));
478  }
479  gtk_label_set_text((GtkLabel*)online_id_label, account_description_text);
480  build_acct_tree(picker);
481  gtk_window_set_modal(GTK_WINDOW(picker->dialog), TRUE);
482  g_signal_connect(picker->account_tree, "row-activated",
483  G_CALLBACK(account_tree_row_activated_cb), picker);
484 
485  /* Connect key press event so we can expand the tree when the user starts typing, allowing
486  * any subaccount to match */
487  g_signal_connect (picker->account_tree, "key-press-event", G_CALLBACK (account_tree_key_press_cb), picker->account_tree);
488 
489  selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(picker->account_tree));
490  g_signal_connect(selection, "changed",
491  G_CALLBACK(account_tree_row_changed_cb), picker);
492 
493  gnc_tree_view_account_set_selected_account(picker->account_tree, default_selection);
494 
495  do
496  {
497  response = gtk_dialog_run(GTK_DIALOG(picker->dialog));
498  switch (response)
499  {
500  case GNC_RESPONSE_NEW:
501  gnc_import_add_account(NULL, picker);
502  response = GTK_RESPONSE_OK;
503  /* no break */
504 
505  case GTK_RESPONSE_OK:
506  retval = gnc_tree_view_account_get_selected_account(picker->account_tree);
507  if (retval == NULL)
508  {
509  response = GNC_RESPONSE_NEW;
510  break;
511  }
512  if (retval)
513  retval_name = xaccAccountGetName(retval);
514  if (!retval_name)
515  retval_name = "(null)";
516  DEBUG("Selected account %p, %s", retval, retval_name);
517 
518  /* See if the selected account is a placeholder. */
519  if (retval && xaccAccountGetPlaceholder (retval))
520  {
521  show_placeholder_warning (picker, retval_name);
522  response = GNC_RESPONSE_NEW;
523  break;
524  }
525  else if (picker->new_account_default_commodity &&
527  picker->new_account_default_commodity))) // check commodity
528  {
529  show_commodity_warning (picker, retval_name);
530  response = GNC_RESPONSE_NEW;
531  break;
532  }
533 
534  if ( account_online_id_value != NULL)
535  {
536  gnc_import_set_acc_online_id(retval, account_online_id_value);
537  }
538  ok_pressed_retval = TRUE;
539  break;
540 
541  default:
542  ok_pressed_retval = FALSE;
543  break;
544  }
545  }
546  while (response == GNC_RESPONSE_NEW);
547 
548  g_object_unref(G_OBJECT(builder));
549  gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(picker->dialog));
550  gtk_widget_destroy(picker->dialog);
551  }
552  else
553  {
554  retval_name = retval ? xaccAccountGetName(retval) : NULL;
555  ok_pressed_retval = TRUE; /* There was no dialog involved, so the computer "pressed" ok */
556  }
557  /*FIXME: DEBUG("WRITEME: gnc_import_select_account() Here we should check if account type is compatible, currency matches, etc.\n"); */
558  g_free(picker);
559  /*DEBUG("Return value: %p%s%s%s",retval,", account name:",xaccAccountGetName(retval),"\n");*/
560  if (ok_pressed != NULL)
561  {
562  *ok_pressed = ok_pressed_retval;
563  }
564  LEAVE("Selected account %p, %s", retval, retval_name ? retval_name : "(null)");
565  return retval;
566 }
567 
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:3210
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:3265
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:3405
gboolean xaccAccountGetPlaceholder(const Account *acc)
Get the "placeholder" flag for an account.
Definition: Account.cpp:4220
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:3258
const char * xaccAccountGetTypeStr(GNCAccountType type)
The xaccAccountGetTypeStr() routine returns a string suitable for use in the GUI/Interface.
Definition: Account.cpp:4464
Not a type.
Definition: Account.h:108
Commodity handling public routines.