diff --git a/CMakeLists.txt b/CMakeLists.txt index 23eb5e9ed0..903ebcc5df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,13 +12,13 @@ enable_testing() # Version number of gnucash set (GNUCASH_MAJOR_VERSION 3) -set (GNUCASH_MINOR_VERSION 5) +set (GNUCASH_MINOR_VERSION 900) set (VERSION "${GNUCASH_MAJOR_VERSION}.${GNUCASH_MINOR_VERSION}") set (GNUCASH_LATEST_STABLE_SERIES 3.x) set (PACKAGE gnucash) set (PACKAGE_NAME GnuCash) -set (PACKAGE_VERSION 3.5) +set (PACKAGE_VERSION 3.900) set (PACKAGE_BUGREPORT "https://bugs.gnucash.org") set (PACKAGE_TARNAME ${PACKAGE}) set (PACKAGE_STRING "${PACKAGE_NAME} ${PACKAGE_VERSION}") diff --git a/gnucash/gtkbuilder/dialog-import.glade b/gnucash/gtkbuilder/dialog-import.glade index 59ed257be9..fd8935b5ac 100644 --- a/gnucash/gtkbuilder/dialog-import.glade +++ b/gnucash/gtkbuilder/dialog-import.glade @@ -14,6 +14,9 @@ 600 dialog + + + True @@ -165,7 +168,6 @@ 10 - False False Choose a format False @@ -173,6 +175,9 @@ 600 400 dialog + + + True @@ -301,6 +306,9 @@ True False Preferences + + + True @@ -554,6 +562,9 @@ 600 400 dialog + + + True @@ -717,7 +728,8 @@ This transaction requires your intervention or it will NOT be imported. - Double click on the transaction to change the matching transaction to reconcile, or the destination account of the auto-balance split (if required). + Double click on the transaction to change the matching transaction to reconcile, or the destination account of the auto-balance split (if required). +Multiply select rows using Ctrl-Click, Shift-Click and Rubber banding . Right-Click or Shift-F10 to bring up popup menu and click or select to assign a destination account to the selected rows. False @@ -726,6 +738,9 @@ False dialog + + + True @@ -764,6 +779,7 @@ True False + 42 6 12 @@ -990,6 +1006,7 @@ True True + 2 5 5 False @@ -1107,8 +1124,11 @@ True True False + True - + + multiple + @@ -1145,6 +1165,9 @@ 400 dialog + + + True diff --git a/gnucash/import-export/import-main-matcher.c b/gnucash/import-export/import-main-matcher.c index 7734296022..7172c52e27 100644 --- a/gnucash/import-export/import-main-matcher.c +++ b/gnucash/import-export/import-main-matcher.c @@ -98,9 +98,28 @@ void on_matcher_help_clicked (GtkButton *button, gpointer user_data); void on_matcher_help_close_clicked (GtkButton *button, gpointer user_data); /* Local prototypes */ -static void -refresh_model_row(GNCImportMainMatcher *gui, GtkTreeModel *model, +static void gnc_gen_trans_assign_transfer_account( + GtkTreeView *treeview, + gboolean *first, + gboolean *is_selection, + GtkTreePath *path, + Account **new_acc, + GNCImportMainMatcher *info); +static void gnc_gen_trans_assign_transfer_account_to_selection_cb ( + GtkMenuItem *menuitem, + GNCImportMainMatcher *info); +static void gnc_gen_trans_view_popup_menu (GtkTreeView *treeview, + GdkEvent *event, + GNCImportMainMatcher *info); +static gboolean gnc_gen_trans_onButtonPressed_cb (GtkTreeView *treeview, + GdkEvent *event, + GNCImportMainMatcher *info); +static gboolean gnc_gen_trans_onPopupMenu_cb (GtkTreeView *treeview, + GdkEvent *event, + GNCImportMainMatcher *info); +static void refresh_model_row(GNCImportMainMatcher *gui, GtkTreeModel *model, GtkTreeIter *iter, GNCImportTransInfo *info); +/* end local prototypes */ void gnc_gen_trans_list_delete (GNCImportMainMatcher *info) { @@ -361,54 +380,238 @@ gnc_gen_trans_update_toggled_cb (GtkCellRendererToggle *cell_renderer, } static void -gnc_gen_trans_row_activated_cb (GtkTreeView *view, - GtkTreePath *path, - GtkTreeViewColumn *column, - GNCImportMainMatcher *gui) +gnc_gen_trans_assign_transfer_account(GtkTreeView *treeview, + gboolean *first, + gboolean *is_selection, + GtkTreePath *path, + Account **new_acc, + GNCImportMainMatcher *info) { GtkTreeModel *model; GtkTreeIter iter; GNCImportTransInfo *trans_info; + Account *old_acc; + gboolean ok_pressed; - model = gtk_tree_view_get_model(gui->view); - if (!gtk_tree_model_get_iter(model, &iter, path)) - return; - gtk_tree_model_get(model, &iter, DOWNLOADED_COL_DATA, &trans_info, -1); - - switch (gnc_import_TransInfo_get_action (trans_info)) + ENTER(""); + DEBUG("first = %s",*first?"true":"false"); + DEBUG("is_selection = %s",*is_selection?"true":"false"); + DEBUG("path = %s", gtk_tree_path_to_string(path)); + DEBUG("account passed in = %s", gnc_get_account_name_for_register(*new_acc)); + model = gtk_tree_view_get_model(treeview); + if (gtk_tree_model_get_iter(model, &iter, path)) { - case GNCImport_ADD: - if (gnc_import_TransInfo_is_balanced(trans_info) == FALSE) + gtk_tree_model_get(model, &iter, DOWNLOADED_COL_DATA, &trans_info, -1); + + switch (gnc_import_TransInfo_get_action (trans_info)) { - run_account_picker_dialog (gui, model, &iter, trans_info); + case GNCImport_ADD: + if (gnc_import_TransInfo_is_balanced(trans_info) == FALSE) + { + ok_pressed = TRUE; + old_acc = gnc_import_TransInfo_get_destacc (trans_info); + if (*first) + { + ok_pressed = FALSE; + *new_acc = gnc_import_select_account(info->main_widget, + NULL, + TRUE, + _("Destination account for the auto-balance split."), + xaccTransGetCurrency( + gnc_import_TransInfo_get_trans(trans_info)), + ACCT_TYPE_NONE, + old_acc, + &ok_pressed); + *first = FALSE; + DEBUG("account selected = %s", + gnc_account_get_full_name(*new_acc)); + } + if (ok_pressed) + gnc_import_TransInfo_set_destacc (trans_info, + *new_acc, TRUE); + } + break; + case GNCImport_CLEAR: + case GNCImport_UPDATE: + if (first && !is_selection) run_match_dialog (info, trans_info); + break; + case GNCImport_SKIP: + break; + default: + PERR("InvalidGNCImportValue"); + break; } - break; - case GNCImport_CLEAR: - case GNCImport_UPDATE: - run_match_dialog (gui, trans_info); - break; - case GNCImport_SKIP: - /*The information displayed is only informative, until you select an action*/ - break; - default: - PERR("I don't know what to do! (Yet...)"); - break; + refresh_model_row(info, model, &iter, trans_info); } - refresh_model_row(gui, model, &iter, trans_info); + LEAVE(""); +} + +static void +gnc_gen_trans_assign_transfer_account_to_selection_cb (GtkMenuItem *menuitem, + GNCImportMainMatcher *info) +{ + GtkTreeView *treeview; + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter iter; + GNCImportTransInfo *trans_info; + Account *assigned_account; + GList *selected_rows, *l; + gboolean first, is_selection; + + ENTER(""); + treeview = GTK_TREE_VIEW(info->view); + model = gtk_tree_view_get_model(treeview); + selection = gtk_tree_view_get_selection(treeview); + selected_rows = gtk_tree_selection_get_selected_rows (selection, &model); + assigned_account = NULL; + first = TRUE; + is_selection = TRUE; + DEBUG("Rows in selection = %i", + gtk_tree_selection_count_selected_rows(selection)); + DEBUG("Entering loop over selection"); + + for (l = selected_rows; l != NULL; l = l->next) + { + DEBUG("passing first = %s", + first?"true":"false"); + DEBUG("passing is_selection = %s", + is_selection?"true":"false"); + DEBUG("passing path = %s", + gtk_tree_path_to_string(l->data)); + DEBUG("passing account value = %s", + gnc_account_get_full_name(assigned_account)); + gnc_gen_trans_assign_transfer_account(treeview, + &first, &is_selection, l->data, &assigned_account, info); + DEBUG("returned value of account = %s", + gnc_account_get_full_name(assigned_account)); + DEBUG("returned value of first = %s",first?"true":"false"); + if (assigned_account == NULL) break; + gtk_tree_selection_unselect_path(selection, l->data); + } + + g_list_free_full (selected_rows, (GDestroyNotify) gtk_tree_path_free); + g_list_free_full(l, (GDestroyNotify) gtk_tree_path_free); + LEAVE(""); +} + +static void +gnc_gen_trans_row_activated_cb (GtkTreeView *treeview, + GtkTreePath *path, + GtkTreeViewColumn *column, + GNCImportMainMatcher *info) +{ + Account *assigned_account; + gboolean first, is_selection; + + ENTER(""); + assigned_account = NULL; + first = TRUE; + is_selection = FALSE; + gnc_gen_trans_assign_transfer_account(treeview, + &first, &is_selection, path, + &assigned_account, info); + DEBUG("account returned = %s", gnc_account_get_full_name(assigned_account)); + LEAVE(""); } static void gnc_gen_trans_row_changed_cb (GtkTreeSelection *selection, - GNCImportMainMatcher *gui) + GNCImportMainMatcher *info) { GtkTreeModel *model; GtkTreeIter iter; + GtkSelectionMode mode; - if (!gtk_tree_selection_get_selected(selection, &model, &iter)) - return; - gtk_tree_selection_unselect_iter(selection, &iter); + ENTER(""); + mode = gtk_tree_selection_get_mode(selection); + switch (mode) + { + case GTK_SELECTION_MULTIPLE: + DEBUG("mode = GTK_SELECTION_MULTIPLE, no action"); + break; + case GTK_SELECTION_NONE: + DEBUG("mode = GTK_SELECTION_NONE, no action"); + break; + case GTK_SELECTION_BROWSE: + DEBUG("mode = GTK_SELECTION_BROWSE->default"); + case GTK_SELECTION_SINGLE: + DEBUG("mode = GTK_SELECTION_SINGLE->default"); + default: + DEBUG("mode = default unselect selected row"); + if (gtk_tree_selection_get_selected(selection, &model, &iter)) + { + gtk_tree_selection_unselect_iter(selection, &iter); + } + } + LEAVE(""); +} + +static void +gnc_gen_trans_view_popup_menu (GtkTreeView *treeview, + GdkEvent *event, + GNCImportMainMatcher *info) +{ + GtkWidget *menu, *menuitem; + GdkEventButton *event_button; + + ENTER (""); + menu = gtk_menu_new(); + menuitem = gtk_menu_item_new_with_label(_("Assign a transfer account.")); + g_signal_connect(menuitem, "activate", + G_CALLBACK( + gnc_gen_trans_assign_transfer_account_to_selection_cb), + info); + DEBUG("Callback to assign destination account to selection connected"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + gtk_widget_show_all(menu); + event_button = (GdkEventButton *) event; + /* Note: event can be NULL here when called from view_onPopupMenu; + * gdk_event_get_time() accepts a NULL argument */ + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, + (event_button != NULL) ? event_button->button : 0, + gdk_event_get_time((GdkEvent*)event)); + LEAVE (""); +} + +static gboolean +gnc_gen_trans_onButtonPressed_cb (GtkTreeView *treeview, + GdkEvent *event, + GNCImportMainMatcher *info) +{ + GdkEventButton *event_button; + ENTER(""); + g_return_val_if_fail (treeview != NULL, FALSE); + g_return_val_if_fail (event != NULL, FALSE); + /* handle single click with the right mouse button? */ + if (event->type == GDK_BUTTON_PRESS) + { + event_button = (GdkEventButton *) event; + if (event_button->button == GDK_BUTTON_SECONDARY) + { + DEBUG("Right mouseClick detected- popup the menu."); + gnc_gen_trans_view_popup_menu(treeview, event, info); + LEAVE("return TRUE"); + return TRUE; + } + } + LEAVE("return FALSE"); + return FALSE; } +static gboolean +gnc_gen_trans_onPopupMenu_cb (GtkTreeView *treeview, + GdkEvent *event, + GNCImportMainMatcher *info) +{ + ENTER(""); + /* respond to Shift-F10 popup menu hotkey */ + gnc_gen_trans_view_popup_menu(treeview, NULL, info); + LEAVE (""); + return TRUE; +} + + static GtkTreeViewColumn * add_text_column(GtkTreeView *view, const gchar *title, int col_num) { @@ -536,6 +739,11 @@ gnc_gen_trans_init_view (GNCImportMainMatcher *info, G_CALLBACK(gnc_gen_trans_row_activated_cb), info); g_signal_connect(selection, "changed", G_CALLBACK(gnc_gen_trans_row_changed_cb), info); + + g_signal_connect(view, "button-press-event", + G_CALLBACK(gnc_gen_trans_onButtonPressed_cb), info); + g_signal_connect(view, "popup-menu", + G_CALLBACK(gnc_gen_trans_onPopupMenu_cb), info); } static void diff --git a/libgnucash/app-utils/fin.scm b/libgnucash/app-utils/fin.scm index a8edf0506c..4ffc3c1faa 100644 --- a/libgnucash/app-utils/fin.scm +++ b/libgnucash/app-utils/fin.scm @@ -182,3 +182,171 @@ ) ) ) + +;; Further options to match what some (several? many?) lenders do (at +;; least in Canada): +;; The posted interest rate is an annual rate that has a specified +;; compounding frequency per year (2 for mortgages in Canada). +;; A payment frequency and amortization length are selected (e.g. +;; monthly payments for 25 years). +;; The posted nominal rate is converted from the specified compounding +;; frequency to the equivalent rate at the payment frequency. +;; The required payment is calculated. +;; The payment is rounded up to the next dollar (or $10 dollars, +;; or whatever...) +;; Each payment period, interest is calculated on the outstanding +;; balance. +;; The interest is rounded to the nearest cent and added to the +;; balance. +;; The payment is subtracted from the balance. +;; The final payment will be smaller because all the other payments +;; were rounded up. +;; +;; For the purpose of creating scheduled transactions that properly +;; debit a source account while crediting the loan account and the +;; interest expense account, the first part (the calculation of the +;; required payment) doesn't really matter. You have agreed +;; with the lender what the payment terms (interest rate, payment +;; frequency, payment amount) will be; you keep paying until the +;; balance is zero. +;; +;; To create the scheduled transactions, we need to build an +;; amortization table. +;; If it weren't for the rounding of the interest to the nearest cent +;; each period, we could calculate the ith row of the amortization +;; table directly from the general annuity equation (as is done by +;; gnc:ipmt and gnc:ppmt). But to deal with the intermediate +;; rounding, the amortization table has to be constructed iteratively +;; (as is done by the AMORT worksheet on the TI BA II Plus +;; financial calculator). +;; +;; ================================= +;; EXAMPLE: +;; Say you borrow $100,000 at 5%/yr, compounded semi-annually. +;; You amortize the loan over 2 years with 24 monthly payments. +;; This calls for payments of $4,384.8418 at the end of each month. +;; The lender rounds this up to $4,385. +;; +;; If you calculate the balance at each period directly using the annuity +;; formula (like calc-principal does), and then use the those values to calculate +;; the principal and interest paid, the first 10 rows of the amortization table +;; look like this (the values are rounded to the nearest cent for _display_, but +;; not for calculating the next period): +;; +;; PERIOD | Open | Interest | Principal | End +;; 1 |$100,000.00 | $412.39 | $3,972.61 | $96,027.39 +;; 2 | $96,027.39 | $396.01 | $3,988.99 | $92,038.40 +;; 3 | $92,038.40 | $379.56 | $4,005.44 | $88,032.96 +;; 4 | $88,032.96 | $363.04 | $4,021.96 | $84,011.00 +;; 5 | $84,011.00 | $346.45 | $4,038.55 | $79,972.45 +;; 6 | $79,972.45 | $329.80 | $4,055.20 | $75,917.25 +;; 7 | $75,917.25 | $313.08 | $4,071.92 | $71,845.33 +;; 8 | $71,845.33 | $296.28 | $4,088.72 | $67,756.61 +;; 9 | $67,756.61 | $279.43 | $4,105.57 | $63,651.04 +;; 10 | $63,651.04 | $262.49 | $4,122.51 | $59,528.53 +;; +;; If you calculate each period sequentially (rounding the interest and balance +;; at each step), you get: +;; +;; PERIOD | Open | Interest | Principal | End +;; 1 |$100,000.00 | $412.39 | $3,972.61 | $96,027.39 +;; 2 | $96,027.39 | $396.01 | $3,988.99 | $92,038.40 +;; 3 | $92,038.40 | $379.56 | $4,005.44 | $88,032.96 +;; 4 | $88,032.96 | $363.04 | $4,021.96 | $84,011.00 +;; 5 | $84,011.00 | $346.45 | $4,038.55 | $79,972.45 +;; 6 | $79,972.45 | $329.80 | $4,055.20 | $75,917.25 +;; 7 | $75,917.25 | $313.08 | $4,071.92 | $71,845.33 +;; 8 | $71,845.33 | $296.28 | $4,088.72 | $67,756.61 +;; 9 | $67,756.61 | $279.42 | $4,105.58 | $63,651.03 <- Different +;; 10 | $63,651.03 | $262.49 | $4,122.51 | $59,528.52 <- still $0.01 off +;; +;; ================================= +;; +;; For the following functions the argument names are: +;; py: payment frequency (number of payments per year) +;; cy: compounding frequency of the nominal rate (per year) +;; iy: nominal annual interest rate +;; pv: the present value (opening balance) +;; pmt: the size of the periodic payment +;; n: the payment period we are asking about (the first payment is n=1) +;; places: number of decimal places to round the interest amount to +;; at each payment (999 does no rounding) +;; +;; Note: only ordinary annuities are supported (payments at the end of +;; each period, not at the beginning of each period) +;; +;; Unlike the AMORT worksheet on the BA II Plus, these methods will +;; handle the smaller payment (bringing the balance to zero, then +;; zeroing future payments) +;; +;; The present value (pv) must be non-negative. If not, the balance will be +;; treated as 0. +;; The payment (pmt) can be positive (paying interest, and hopefully +;; reducing the balance each payment), or negative (increasing the balance +; each payment). +;; The payment number (n) must be positive for amort_pmt, amort_ppmt, and +;; amort_ipmt. I.e., the first payment is payment 1. +;; The payment number (n) must be non-negative for amort_balance. (In this +;; case, payment zero is at the _beginning_ of the first period, so +;; amort_balance will just be the initial balance.) +;; If the above conditions on n are violated, the functions return -1 (#f is +;; not used, because it causes gnucash to crash). +;; +;; A negative interest rate works (if you can find a lender who charges +;; negative rates), but negative compounding frequency, or negative payment +;; frequency is a bad idea. + +;; Calculate the balance remaining after the nth payment +;; (n must be greater than or equal to zero) +(define (gnc:amort_pmt py cy iy pv pmt n places) + (if (< n 1) -1 ;; Returning #f here causes gnucash to crash on startup + (let* ((prevBal (gnc:amort_balance py cy iy pv pmt (- n 1) places)) + (balBeforePayment + (amort_balanceAfterInterest prevBal py cy iy places)) + (balAfterPayment (amort_balanceAfterPayment balBeforePayment pmt))) + (- balBeforePayment balAfterPayment)))) + +;; Calculate the amount of the nth payment that is principal +;; (n must be greater than zero) +(define (gnc:amort_ppmt py cy iy pv pmt n places) + (if (< n 1) -1 + (let* ((prevBal (gnc:amort_balance py cy iy pv pmt (- n 1) places)) + (bal-after-int (amort_balanceAfterInterest prevBal py cy iy places)) + (newBal (amort_balanceAfterPayment bal-after-int pmt))) + (- prevBal newBal)))) + +;; Calculate the amount of the nth payment that is interest +;; (n must be greater than zero) +(define (gnc:amort_ipmt py cy iy pv pmt n places) + (if (< n 1) -1 + (let* ((prevBal(gnc:amort_balance py cy iy pv pmt (- n 1) places))) + (amort_interest prevBal py cy iy places)))) + +;; "Private" helper functions: + +;; Calculate the amount of interest on the current balance, +;; rounded to the specified number of decimal places +(define (amort_interest balance py cy iy places) + (roundToPlaces (* balance (gnc:periodic_rate iy py cy)) places) +) + +;; Calculate the new balance after applying the interest, but before +;; applying the payment +(define (amort_balanceAfterInterest prevBalance py cy iy places) + (+ prevBalance (amort_interest prevBalance py cy iy places)) +) + +;; Apply the payment to the balance (after the interest has been +;; added), without letting the balance go below zero. +(define (amort_balanceAfterPayment balanceBeforePmt pmt) + (max 0 (- balanceBeforePmt pmt)) +) + +;; Round the value to the specified number of decimal places. +;; 999 places means no rounding (#f is not used, because only numbers can be +;; entered in the scheduled transaction editor) +(define (roundToPlaces value places) + (if (= places 999) value + (/ (round (* value (expt 10 places))) (expt 10 places)) + ) +)