From 01339a782c91a8d931cc9d7462ddce19afc60635 Mon Sep 17 00:00:00 2001 From: David Cousens Date: Tue, 28 Aug 2018 22:36:42 +1000 Subject: [PATCH 1/3] Stage 1 Extracting code for assigning a transfer account from gnc_gen_trans_row_activated_cb() into a new function gnc_gen_trans_assign_transfer account() and calling that function from within gnc_gen_trans_row_activated_cb(). Tested and working as per the original code. --- gnucash/import-export/import-main-matcher.c | 50 +++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/gnucash/import-export/import-main-matcher.c b/gnucash/import-export/import-main-matcher.c index 78d46af775..2c23dd1eb1 100644 --- a/gnucash/import-export/import-main-matcher.c +++ b/gnucash/import-export/import-main-matcher.c @@ -95,6 +95,11 @@ void on_matcher_help_clicked (GtkButton *button, gpointer user_data); void on_matcher_help_close_clicked (GtkButton *button, gpointer user_data); /* Local prototypes */ +static Account * +gnc_gen_trans_assign_transfer_account(GNCImportMainMatcher *gui, + gboolean first, + GtkTreePath *path, + Account *new_acc); static void refresh_model_row(GNCImportMainMatcher *gui, GtkTreeModel *model, GtkTreeIter *iter, GNCImportTransInfo *info); @@ -357,19 +362,21 @@ gnc_gen_trans_update_toggled_cb (GtkCellRendererToggle *cell_renderer, refresh_model_row(gui, model, &iter, trans_info); } -static void -gnc_gen_trans_row_activated_cb (GtkTreeView *view, - GtkTreePath *path, - GtkTreeViewColumn *column, - GNCImportMainMatcher *gui) +static Account * +gnc_gen_trans_assign_transfer_account(GNCImportMainMatcher *gui, + gboolean first, + GtkTreePath *path, + Account *new_acc) { 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; + return NULL; gtk_tree_model_get(model, &iter, DOWNLOADED_COL_DATA, &trans_info, -1); switch (gnc_import_TransInfo_get_action (trans_info)) @@ -377,7 +384,21 @@ gnc_gen_trans_row_activated_cb (GtkTreeView *view, case GNCImport_ADD: if (gnc_import_TransInfo_is_balanced(trans_info) == FALSE) { - run_account_picker_dialog (gui, model, &iter, trans_info); + old_acc = gnc_import_TransInfo_get_destacc (trans_info); + if (first) + { + new_acc = gnc_import_select_account(gui->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; + } + if (ok_pressed) + gnc_import_TransInfo_set_destacc (trans_info, new_acc, TRUE); } break; case GNCImport_CLEAR: @@ -388,10 +409,23 @@ gnc_gen_trans_row_activated_cb (GtkTreeView *view, /*The information displayed is only informative, until you select an action*/ break; default: - PERR("I don't know what to do! (Yet...)"); + PERR("InvalidGNCImportValue"); break; } refresh_model_row(gui, model, &iter, trans_info); + return new_acc; +} + +static void +gnc_gen_trans_row_activated_cb (GtkTreeView *view, + GtkTreePath *path, + GtkTreeViewColumn *column, + GNCImportMainMatcher *gui) +{ + Account *assigned_account =NULL; + gboolean first = TRUE; + + assigned_account = gnc_gen_trans_assign_transfer_account(gui, first, path, assigned_account); } static void From cae8ecde8f9ddd615ff47c783ce8136395bd71a9 Mon Sep 17 00:00:00 2001 From: David Cousens Date: Thu, 6 Sep 2018 12:43:34 +1000 Subject: [PATCH 2/3] Changes to import-main-matcher.c and dialog-import.glade to implement multiple selection and assignment of a destination account to the selection. --- gnucash/gtkbuilder/dialog-import.glade | 31 ++- gnucash/import-export/import-main-matcher.c | 286 ++++++++++++++++---- 2 files changed, 257 insertions(+), 60 deletions(-) diff --git a/gnucash/gtkbuilder/dialog-import.glade b/gnucash/gtkbuilder/dialog-import.glade index b8243dc309..ebbc18945e 100644 --- a/gnucash/gtkbuilder/dialog-import.glade +++ b/gnucash/gtkbuilder/dialog-import.glade @@ -1,5 +1,5 @@ - + @@ -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 + @@ -1130,6 +1150,9 @@ 400 dialog + + + True diff --git a/gnucash/import-export/import-main-matcher.c b/gnucash/import-export/import-main-matcher.c index 2c23dd1eb1..580b09f4a8 100644 --- a/gnucash/import-export/import-main-matcher.c +++ b/gnucash/import-export/import-main-matcher.c @@ -95,14 +95,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 Account * -gnc_gen_trans_assign_transfer_account(GNCImportMainMatcher *gui, - gboolean first, - GtkTreePath *path, - Account *new_acc); -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) { @@ -362,11 +376,13 @@ gnc_gen_trans_update_toggled_cb (GtkCellRendererToggle *cell_renderer, refresh_model_row(gui, model, &iter, trans_info); } -static Account * -gnc_gen_trans_assign_transfer_account(GNCImportMainMatcher *gui, - gboolean first, +static void +gnc_gen_trans_assign_transfer_account(GtkTreeView *treeview, + gboolean *first, + gboolean *is_selection, GtkTreePath *path, - Account *new_acc) + Account **new_acc, + GNCImportMainMatcher *info) { GtkTreeModel *model; GtkTreeIter iter; @@ -374,72 +390,225 @@ gnc_gen_trans_assign_transfer_account(GNCImportMainMatcher *gui, Account *old_acc; gboolean ok_pressed; - model = gtk_tree_view_get_model(gui->view); - if (!gtk_tree_model_get_iter(model, &iter, path)) - return NULL; - 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)) { - old_acc = gnc_import_TransInfo_get_destacc (trans_info); - if (first) + case GNCImport_ADD: + if (gnc_import_TransInfo_is_balanced(trans_info) == FALSE) { - new_acc = gnc_import_select_account(gui->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; + 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); } - 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("InvalidGNCImportValue"); - break; + refresh_model_row(info, model, &iter, trans_info); } - refresh_model_row(gui, model, &iter, trans_info); - return new_acc; + LEAVE(""); } static void -gnc_gen_trans_row_activated_cb (GtkTreeView *view, - GtkTreePath *path, - GtkTreeViewColumn *column, - GNCImportMainMatcher *gui) +gnc_gen_trans_assign_transfer_account_to_selection_cb (GtkMenuItem *menuitem, + GNCImportMainMatcher *info) { - Account *assigned_account =NULL; - gboolean first = TRUE; + 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); + } - assigned_account = gnc_gen_trans_assign_transfer_account(gui, first, path, assigned_account); + 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) { @@ -552,6 +721,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); } From 5f9020016a5f1be28b7673ddc2d5f3db8c68ee3f Mon Sep 17 00:00:00 2001 From: thetedmunds Date: Mon, 15 Apr 2019 14:31:23 -0700 Subject: [PATCH 3/3] Amended commit to address pull-request comments. --- libgnucash/app-utils/fin.scm | 168 +++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/libgnucash/app-utils/fin.scm b/libgnucash/app-utils/fin.scm index be56e543fd..c3ee43ce61 100644 --- a/libgnucash/app-utils/fin.scm +++ b/libgnucash/app-utils/fin.scm @@ -185,3 +185,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)) + ) +)