2002-07-10 Joshua Sled <jsled@asynchronous.org>

* src/gnome/druid-loan.[ch]: Added; Initial implementation of
	Gnome Druid for setting up loan-repayment Scheduled Transactions.

	* src/doc/loans.txt: Added; notes about how loans will be dealt
	with in GnuCash.

	* src/gnome/glade/sched-xact.glade: Added loan-druid.

	* src/gnome/window-main.c (gnc_main_window_create_menus): Added
	'Mortgage/Loan Repayment Setup' Druid invocation menu item.

	* src/scm/fin.scm: Added. Implementations of 'ipmt', 'ppmt', 'pmt'
	and supporting code.

	* src/app-utils/test/test-exp-parser.c (test_parser): Added tests
	for functions-in-expressions.  Added [passed] test for Conrad's
	bug.

	* src/app-utils/gnc-exp-parser.c (func_op): Added. gnc-side
	callback for dealing with a function in an expression.

	* src/calculation/expression_parser.c: Added initial, undocumented
	support for functions in expressions.


git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/trunk@7115 57a11ea4-9604-0410-9ed3-97b8803252fd
This commit is contained in:
Joshua Sled 2002-07-11 05:17:04 +00:00
parent 5165819059
commit 0ebf4e00d1
16 changed files with 3135 additions and 28 deletions

View File

@ -1,3 +1,29 @@
2002-07-10 Joshua Sled <jsled@asynchronous.org>
* src/gnome/druid-loan.[ch]: Added; Initial implementation of
Gnome Druid for setting up loan-repayment Scheduled Transactions.
* src/doc/loans.txt: Added; notes about how loans will be dealt
with in GnuCash.
* src/gnome/glade/sched-xact.glade: Added loan-druid.
* src/gnome/window-main.c (gnc_main_window_create_menus): Added
'Mortgage/Loan Repayment Setup' Druid invocation menu item.
* src/scm/fin.scm: Added. Implementations of 'ipmt', 'ppmt', 'pmt'
and supporting code.
* src/app-utils/test/test-exp-parser.c (test_parser): Added tests
for functions-in-expressions. Added [passed] test for Conrad's
bug.
* src/app-utils/gnc-exp-parser.c (func_op): Added. gnc-side
callback for dealing with a function in an expression.
* src/calculation/expression_parser.c: Added initial, undocumented
support for functions in expressions.
2002-07-10 Derek Atkins <derek@ihtfp.com>
* add Billable flag and Bill-To (owner) to line-item entries.

View File

@ -22,6 +22,8 @@
#include <ctype.h>
#include <locale.h>
#include <string.h>
#include <glib.h>
#include <guile/gh.h>
@ -341,6 +343,40 @@ update_variables (var_store_ptr vars)
}
}
static
void*
func_op( const char *fname,
int argc, void **argv )
{
SCM scmFn, scmArgs, scmTmp;
int i;
gnc_numeric n, *result;
GString *realFnName;
realFnName = g_string_sized_new( strlen(fname) + 5 );
g_string_sprintf( realFnName, "gnc:%s", fname );
scmFn = gh_eval_str_with_standard_handler( realFnName->str );
g_string_free( realFnName, TRUE );
if ( ! gh_procedure_p( scmFn ) ) {
/* FIXME: handle errors correctly. */
printf( "gnc:\"%s\" is not a scm procedure\n", fname );
return NULL;
}
scmArgs = gh_list( SCM_UNDEFINED );
for ( i=0; i<argc; i++ ) {
/* cons together back-to-front. */
n = *(gnc_numeric*)argv[argc - i - 1];
scmTmp = gh_double2scm( gnc_numeric_to_double( n ) );
scmArgs = gh_cons( scmTmp, scmArgs );
}
scmTmp = gh_apply( scmFn, scmArgs );
result = g_new0( gnc_numeric, 1 );
*result = double_to_gnc_numeric( gh_scm2double(scmTmp), 1,
GNC_DENOM_SIGFIG | (6<<8) );
return (void*)result;
}
static void *
trans_numeric(const char *digit_str,
char radix_point,
@ -455,7 +491,8 @@ gnc_exp_parser_parse_separate_vars (const char * expression,
lc = gnc_localeconv ();
pe = init_parser (vars, *lc->mon_decimal_point, *lc->mon_thousands_sep,
trans_numeric, numeric_ops, negate_numeric, g_free);
trans_numeric, numeric_ops, negate_numeric, g_free,
func_op);
error_loc = parse_string (&result, expression, pe);
@ -545,6 +582,8 @@ gnc_exp_parser_error_string (void)
return _("Undefined character");
case NOT_A_VARIABLE:
return _("Not a variable");
case NOT_A_FUNC:
return _("Not a defined function");
case PARSER_OUT_OF_MEMORY:
return _("Out of memory");
case NUMERIC_ERROR:

View File

@ -1,5 +1,8 @@
#include <glib.h>
#include <stdlib.h>
#include <stdio.h>
#include "guile/gh.h"
#include "gnc-exp-parser.h"
#include "gnc-numeric.h"
@ -54,7 +57,19 @@ run_parser_test (TestNode *node)
gnc_numeric result;
char *error_loc;
result = gnc_numeric_error( -1 );
printf( "Running test \"%s\" = ", node->test_name );
succeeded = gnc_exp_parser_parse (node->exp, &result, &error_loc);
{
int pass;
pass = succeeded;
pass ^= node->should_succeed;
pass = !pass;
printf( "%0.2f [%s]\n",
( ( pass && node->should_succeed ) ?
gnc_numeric_to_double( result ) : 0.0 ),
(pass ? "PASS" : "FAIL" ) );
}
if (succeeded != node->should_succeed)
{
@ -116,6 +131,31 @@ test_parser (void)
add_pass_test ("5 * 6", NULL, gnc_numeric_create (30, 1));
add_pass_test (" 34 / (22) ", NULL, gnc_numeric_create (34, 22));
add_pass_test (" (4 + 5 * 2) - 7 / 3", NULL, gnc_numeric_create (35, 3));
add_pass_test ("1 + 2 * 3 + 4 + 5 * 6 * 7", NULL, gnc_numeric_create(221, 1) );
add_pass_test( "1 - 2 * 3 + 4 - 5 * 6 * 7", NULL,
gnc_numeric_create(-211, 1) );
add_pass_test( "Conrad's bug",
"22.32 * 2 + 16.8 + 34.2 * 2 + 18.81 + 85.44"
"- 42.72 + 13.32 + 15.48 + 23.4 + 115.4",
gnc_numeric_create(35897, 100) );
gh_eval_str( "(define (plus a b) (+ a b))" );
add_pass_test( "plus( 1 : 2 ) + 3", NULL, gnc_numeric_create( 6, 1 ) );
add_pass_test( "plus( 1 : 2 ) * 3", NULL, gnc_numeric_create( 9, 1 ) );
add_pass_test( "plus( 1 + 2 : 3 ) * 5", NULL, gnc_numeric_create( 30, 1 ) );
add_pass_test( "plus( ( 1 + 2 ) * 3 : 4 ) + 5", NULL, gnc_numeric_create( 18, 1) );
add_pass_test( "5 + plus( ( 1 + 2 ) * 3 : 4 )", NULL, gnc_numeric_create( 18, 1) );
add_pass_test( "plus( plus( 1 : 2 ) : 3 )", NULL, gnc_numeric_create( 6, 1 ) );
add_pass_test( "plus( 4 : plus( plus( 1 : 2 ) : 3))", NULL, gnc_numeric_create( 10, 1 ) );
gh_eval_str( "(define (foo a b) (+ a b))" );
add_pass_test( "foo( 1 : 2 ) + 4", NULL, gnc_numeric_create( 7, 1 ) );
add_pass_test( "foo( (1 + 2 * 3) : 4 ) + 5",
NULL, gnc_numeric_create( 16, 1 ) );
add_pass_test( "foo( 1 : 2 ) + foo( 3 : 4 ) + 5",
NULL, gnc_numeric_create( 15, 1 ) );
add_pass_test( "foo( a = 42 : foo( foo( 1 : 2 ) : 6 * 7 )) + a",
NULL, gnc_numeric_create( 129, 1 ) );
run_parser_tests ();
@ -124,10 +164,17 @@ test_parser (void)
}
int
main (int argc, char **argv)
real_main (int argc, char **argv)
{
/* set_should_print_success (TRUE); */
test_parser();
print_test_results();
exit(get_rv());
}
int main( int argc, char **argv )
{
/* do things this way so we can test scheme function calls from expressions */
gh_enter( argc, argv, real_main );
}

View File

@ -365,9 +365,15 @@
#include <glib.h>
#include <guile/gh.h>
#include "../engine/gnc-numeric.h"
#define EXPRESSION_PARSER_STATICS
#include "finvar.h"
#define MAX_FUNC_ARG_LEN 255
/* structure to hold parser environment - environment particular to
* each caller */
typedef struct parser_env
@ -375,11 +381,8 @@ typedef struct parser_env
unsigned stack_cnt;
unsigned stack_size;
var_store_ptr *stack;
var_store_ptr predefined_vars;
var_store_ptr named_vars;
var_store_ptr unnamed_vars;
const char *parse_str;
@ -402,6 +405,7 @@ typedef struct parser_env
void *(*numeric_ops) (char op_sym, void *left_value, void *right_value);
void *(*negate_numeric) (void *value);
void (*free_numeric) (void *numeric_value);
void *(*func_op)( const char *fname, int argc, void **argv );
}
parser_env;
@ -409,6 +413,8 @@ parser_env;
#include "fin_static_proto.h"
#include "fin_spl_protos.h"
#define FN_TOKEN 'F'
#define ARG_TOKEN ':'
#define VAR_TOKEN 'V'
#define NUM_TOKEN 'I'
@ -418,7 +424,7 @@ parser_env;
#define NAMED_INCR 5
static char allowed_operators[] = "+-*/()=";
static char allowed_operators[] = "+-*/()=:";
parser_env_ptr
init_parser (var_store_ptr predefined_vars,
@ -432,7 +438,9 @@ init_parser (var_store_ptr predefined_vars,
void *left_value,
void *right_value),
void *negate_numeric (void *value),
void free_numeric (void *numeric_value))
void free_numeric (void *numeric_value),
void *func_op( const char *fname,
int argc, void **argv ))
{
parser_env_ptr pe = g_new0 (parser_env, 1);
@ -450,6 +458,7 @@ init_parser (var_store_ptr predefined_vars,
pe->numeric_ops = numeric_ops;
pe->negate_numeric = negate_numeric;
pe->free_numeric = free_numeric;
pe->func_op = func_op;
return pe;
} /* init_parser */
@ -741,27 +750,14 @@ next_token (parser_env_ptr pe)
{
add_token (pe, EOS);
}
/* test for name */
else if (isalpha (*str_parse) || (*str_parse == '_'))
{
add_token (pe, VAR_TOKEN);
nstr = pe->name;
do
{
*nstr++ = *str_parse++;
}
while ((*str_parse == '_') ||
isalpha (*str_parse) ||
isdigit (*str_parse));
*nstr = EOS;
}
/* test for possible operator */
else if (strchr (allowed_operators, *str_parse))
{
add_token (pe, *str_parse++);
if (*str_parse == ASN_OP)
{
/* BUG/FIXME: this allows '(=' and ')=' [?], neither of which make
* sense. */
if (pe->Token != ASN_OP)
{
str_parse++;
@ -772,6 +768,38 @@ next_token (parser_env_ptr pe)
pe->error_code = UNDEFINED_CHARACTER;
} /* endif */
}
/* test for name */
else if (isalpha (*str_parse)
|| (*str_parse == '_'))
{
int funcFlag = 0;
/* Check for variable or function */
/* If variable: add token. */
/* If function: parse args, build struct, add token. */
nstr = pe->name;
do
{
if ( *str_parse == '(' ) {
funcFlag = 1;
str_parse++;
break;
}
*nstr++ = *str_parse++;
}
while ((*str_parse == '_')
|| (*str_parse == '(')
|| isalpha (*str_parse)
|| isdigit (*str_parse));
*nstr = EOS;
if ( funcFlag ) {
add_token( pe, FN_TOKEN );
} else {
add_token (pe, VAR_TOKEN);
}
}
/* test for numeric token */
else if ((number = pe->trans_numeric (str_parse, pe->radix_point,
pe->group_char, &nstr)))
@ -790,6 +818,15 @@ next_token (parser_env_ptr pe)
pe->parse_str = str_parse;
} /* next_token */
/* evaluate function operators
* <name>( arg0, arg1, ... )
*/
static void
function_op( parser_env_ptr pe )
{
}
/* evaluate assignment operators,
* =
* +=
@ -857,8 +894,9 @@ assignment_op (parser_env_ptr pe)
vl->value = vr->value;
vr->value = NULL;
}
else
else {
pe->numeric_ops (ASN_OP, vl->value, vr->value);
}
free_var (vr, pe);
} /* endif */
@ -997,11 +1035,14 @@ multiply_divide_op (parser_env_ptr pe)
* named variables
* numerics
* grouped expressions, "()"
* functions [ <name>( [exp, exp, ..., exp] ) ]
*/
static void
primary_exp (parser_env_ptr pe)
{
var_store_ptr rslt = NULL;
char *fnIdent;
int funcArgCount;
char LToken = pe->Token;
next_token (pe);
@ -1011,7 +1052,6 @@ primary_exp (parser_env_ptr pe)
switch (LToken)
{
case '(':
/*add_sub_op(pe); */
assignment_op (pe);
if (pe->error_code)
return;
@ -1058,6 +1098,51 @@ primary_exp (parser_env_ptr pe)
pe->numeric_value = NULL;
break;
case FN_TOKEN:
fnIdent = pe->name;
funcArgCount = 0;
do {
assignment_op(pe);
if ( pe->error_code )
return;
funcArgCount++;
if ( pe->Token == ')' ) {
break;
}
next_token(pe);
} while ( pe->Token != ARG_TOKEN );
if ( pe->Token != ')' ) {
add_token( pe, EOS );
pe->error_code = UNBALANCED_PARENS;
}
{
int i;
var_store_ptr val;
void **argv;
argv = g_new0( void*, funcArgCount );
for ( i=0; i<funcArgCount; i++ ) {
/* fill back-to-front */
val = pop(pe);
argv[funcArgCount - i - 1] = val->value;
}
rslt = get_unnamed_var(pe);
rslt->value = (*pe->func_op)( fnIdent, funcArgCount, argv );
g_free( argv );
if ( rslt->value == NULL ) {
pe->error_code = NOT_A_FUNC;
add_token( pe, EOS );
return;
}
}
next_token(pe);
break;
case VAR_TOKEN:
rslt = get_named_var (pe);
break;
@ -1065,4 +1150,5 @@ primary_exp (parser_env_ptr pe)
if (rslt != NULL)
push (rslt, pe);
} /* primary_exp */

View File

@ -220,6 +220,10 @@ void parse_error(unsigned error_code,
case NOT_A_VARIABLE:
err_str = "Need a Variable on Left side of assignment operator, '='\n";
break;
case NOT_A_FUNC:
err_str = "Need a valid Function name.\n";
break;
} /* endswitch */
printf(err_str);
printf("%s\n",buf_start);

View File

@ -37,6 +37,8 @@ parser_env_ptr init_parser(
void *left_value,
void *right_value),
void *negate_numeric(void *value),
void free_numeric(void *numeric_value));
void free_numeric(void *numeric_value),
void *func_op( const char *fname,
int argc, void **argv ) );
#endif

View File

@ -45,6 +45,7 @@ typedef enum
STACK_UNDERFLOW,
UNDEFINED_CHARACTER,
NOT_A_VARIABLE,
NOT_A_FUNC,
PARSER_OUT_OF_MEMORY,
NUMERIC_ERROR,
PARSER_NUM_ERRORS

475
src/doc/loans.txt Normal file
View File

@ -0,0 +1,475 @@
Handling loan repayment in GnuCash::Scheduled Transactions
------------------------------------------------------------
July, 2002 - jsled@asychronous.org
Bugs 84892 and 84877 detail a request for a new Loan/Mortgage account type,
and Scheduled Transaction support for loan repayment. While it's debatable
that a new account type is required, there is definitely a need for Scheduled
Transaction support for interest/payment computation for a parameterized
"loan repayment SX".
The nature of this support will not create a new top-level account type, but
instead will result in the following changes:
a. Support in the SX credit/debit formulas for such calculations.
b. A Druid to assist in the creation of such SXes.
[c. budgeting/tool bench support in the future]
We define loan repayment values in the following terms:
Identifiers:
P : the original principal. This is the overall principal afforded by the
loan at the time of it's creation.
P' : The beginning principal. This is the principal at the time of entry
into GnuCash.
I : The interest rate associated with the loan. Note that this may change
over time [based on an addition to the Prime rate, for instance], at
various frequencies [yearly, monthly, quarterly...]. Ideally, we can
use the FreqSpec mechanism to facilitate the interest rate adjustment.
N : The length of the loan in periods.
m : The minimum periodic payment.
n : The current period of the repayment.
Functions:
PMT : Total equal periodic payment, as per Gnumeric/Excel's definitions
[see end for more detail].
IPMT : Monthly payment interest portion, ""
PPMT : Monthly payment principal portion, ""
[ NOTE: 'PMT(I,N,P) = IPMT(I, n, N, P) + PPMT(I, n, N, P)' for 0 <= n < N ]
The formula entered into the SX template for a loan may then look like:
Example 1:
Desc/Memo | Account | Credit | Debit
----------+-----------------------------+----------------+-------------------
Repayment | Assets:Bank:Checking | | =PMT(I,n,N,P)
| | | + fixed_amt
Interest | Expenses:Loan_Name:Interest | =IPMT(I,n,N,P) |
PMI | Expenses:Loan_Name:Misc | fixed_amt |
Principal | Liabilities:Loan_Name | =PPMT(I,n,N,P) |
-----------------------------------------------------------------------------
Or, in the case where an escrow account is involved [with thanks to warlord
for the review and fixes]:
Example 2:
Desc/Memo | Account | Credit | Debit
---------------+-----------------------------+----------------+--------------
Repayment | Assets:Bank:Checking | | =PMT(I,n,N,P)
| | | + escrow_amt
| | | + fixed_amt
| | | + pre_payment
Escrow | Assets:Loan_Escrow_acct | escrow_amt |
Interest | Expenses:Loan_Name:Interest | =IPMT(I,n,N,P) |
PMI | Expenses:Loan_Name:Misc | fixed_amt |
Principal | Liabilities:Loan_Name | =PPMT(I,n,N,P) |
| | + pre_payment |
FreqSpec = 1 month
-----------------------------------------------------------------------------
Desc/Memo | Account | Credit | Debit
---------------+-----------------------------+----------------+--------------
Insurance | Assets:Loan_Escrow_acct | | insurance_amt
Insurance | Expenses:Home_Insurance | insurance_amt |
FreqSpec = 1 year
-----------------------------------------------------------------------------
Desc/Memo | Account | Credit | Debit
---------------+-----------------------------+----------------+--------------
Taxes | Assets:Loan_Escrow_acct | | taxes_amt
Taxes | Expenses:Property_Taxes | taxes_amt |
FreqSpec = Quarterly
-----------------------------------------------------------------------------
-------------------------
Practical questions regarding the implementation of this facility are:
-----
| 1. The transactions as in Example 2 are not going to be scheduled for the
| same day; are their values linked at all / do they need to share the
| same var bindings?
Yes, they would want to be linked. More precisely, the insurance/tax amounts
are very likely linked to the escrow_amt in Ex.2. Unfortunately, these are
very likely separate SXes...
-----
| 2. How does this effect the SX implementation of variables?
Vastly.
It becomes clear that multiple SXes will be related. While they'll have
separate FreqSpecs and template transactions, they'll share some state. For
both visualization [i.e., the SX list] and processing [credit/debit cell
value computation] we'll want some manner of dealing with this.
It becomes clear as well that the nature of variables and functions needs to
be more clearly defined with respect to these issues. We probably want to
institute a clear policy for the scoping of variables. As well, since the
SXes will have different instantiation dates, we'll need a method and
implementation for the relation of SXes to each other.
A substantial hurdle is that if a set of SXes are [strongly] related, there
is no-longer a single instantiation date for a set of related SXes. In fact,
there may be different frequencies of recurrence.
One option -- on the surface -- to relate them would be to maintain an
instance variable-binding frame cache, which would store user-entered and
computed variable bindings. The first instantiated SX of the set would create
the frame, and the "last" instance would clean it up. First "last" instance
is defined by the last-occurring SX in a related set, in a given time range.
For example: a loan SX-set is defined by two monthly SXes ["repayment" and
"insurance"], and a quarterly "tax" SX. The first monthly SX would create a
frame, which would be passed two the second monthly SX. This would occur for
the 3 months of interest. The Quarterly SX would get all 3 frames for it's
creation, and use them in an /appropriate/ [read: to be defined through a lot
of pain] way. As the time-based dependency relationship between the frames
plays out, the frame can be removed from the system.
Another option is to toss this idea entirely and instead let the user DTRT
manually.
A related option is to add the necessary grouping mechanism to the SX
storage/data structure: immediately allowing visual grouping of related SXes,
and potentially allowing a storage place for such frame data in the future
with less file-versioning headache. This is the option that will be pursued.
Another element implicit in the original requirements to support
loans/repayment calculations is implicit variables. These are symbolic names
which can be used and are automagically bound to values. The known implicit
variables to support loan/repayment are:
P [loan principal amount], N [loan repayment periods], I [interest], m
[minimum payment] and n [current period]. Some of these [P, N, I, m] are
fixed over many instances; some [n] are rebound specific to the instance.
See the 'variable-scope-frame' below for a method of handling these
variables.
And yet-another element implicit in the original requirement is support for
detecting and computing the result of functions in the template transaction's
credit/debit cells. Changes to the src/app-utils/gnc-exp-parser.[hc] and
src/calculation/expression_parser.[ch] to support functions would be
necessitated. It is conceivable that after parsing, the parsed expression
could be passed to scheme for evaluation. Hopefully this would make it
easier to add support for new functions to the SX code via Scheme.
-----
| 3. How do we deal with periodic [yearly, semi-yearly] updating of various
| "fixed" variables?
Another change in the way variables are used is that some SXes -- especially
loan-repayment -- may involve variables which are not tied to the instance of
the SX, but rather to variables which:
. are also involved in another SX
. change with a frequency different than the SX
. are represented by a relationship to the outside world ["prime + 1.7"]
A partial fix for this problem is to provide multiple levels of scope for
variable bindings, and expose this to the user by a method of assigning
[perhaps time-dependent] values to these variables. Variables bound in this
manner would absolve the user of the need to bind them at SX-creation time.
An added benefit of this would be to allow some users [see Bug#85707] have
"fixed variable" values for a group of SXes.
In combination with the SX Grouping, this would provide most of a fix for the
problem described in #2, above. The variable_frame could be used to provide
the shared-state between related SXes, without imposing quite the same
burden. This approach is slightly less flexible, but that allows it to be
implemented more readily, and understood more easily.
A question which comes up when thinking about yearly-changing values such as
interest rates is if the historical information needs to be versioned. For
now, we punt on this issue, but hopefully will provide enough of a framework
for this to be reasonably added in the future.
We define four types of variables supported by this scheme:
implicit : provided only by the system
e.g.: 'n', the current index of the repayment
transient : have user-defined values, bound at instantiation time.
e.g.: existing ad-hoc variables in SXes.
static : have a user-defined values, and are not expected to change with
any measurable frequency. The user may change these at their
leisure, but no facility to assist or encourage this is
provided.
e.g.: paycheck amount, loan principal amount
periodic : have user-defined values which change at specific points in
time [July 1, yearly]. After the expiration of a variable value,
it's re-binding will prevent any dependent SXes from being
created.
e.g.: loan tax amount, loan interest rate
-----
| 4. From where do we get the dollar amount against which to do the [PI]PMT
| calculation?
The user will specify the parameters of the Loan via some UI... then where
does the data go?
. KVP data for that account?
. KVP data for the SX?
. Do we have a different top-level "Loan" object?
. Present only in the SX template transactions/variable-frames?
I believe that the only location of the data after Druid creation is in the
variable-binding frames and the formulae in the template transactions. The
Druid would thus simply assist the user in creating the following SX-related
structures:
. SXGroup: Loan Repayment
. variable_frame
. P [static]
. N [static]
. n [implicit]
. I [periodic]
. pmi_amount [periodic]
. tax_amount [periodic]
. pre_payment [periodic]
. insurance_amount [periodic]
. SX: Payment
. Bank -> { Escrow,
Liability:Loan:Principal,
Expense:Loan:Interest,
Expense:Loan:Insurance }
. SX: Tax
. Escrow -> Expense:Tax
. SX: Insurance
. Escrow -> Expense:Insurance
--------------------------------------------------
Questions
1/ UI - visible should all this machination be to the user? Should they even
see them as such. The current SX since-last-run UI makes them pretty
visible, and in my estimation it actually helps to make them a bit more
formal and visible. At the same time, it may be overwhelming for the user
to have to create formal variables with weird types like "implicit",
"transient", "static", and "periodic".
--------------------------------------------------
Priorities, Plan
The above represents an "ideal" set of extensions to the SX framework to
enable multiple "enhancement"-level functionalities. Therefore, the
following is the prioritized schedule, with annotations:
1. Functions [PMT, [IP]PMT] in exp_parser; implicit variables [n].
2. [Visual-only] SX grouping
3. Loan-repayment creation Druid
4. SX-only static vars
5. SX-only periodic vars
6. SX-group vars, var_frames
After the completion of item 4, the feature can safely be called "finished".
Items 5 and 6 only serve to increase the robustness of the facility and make
the user's life slightly easier, at the cost of making _my_ life harder. :)
--------------------------------------------------
Reference
-------------------------
Other software:
----------
Gnumeric supports the following functions WRT payment calculation:
* PMT( rate, nper, pv [, fv, type] )
PMT returns the amount of payment for a loan based on a constant interest
rate and constant payments (ea. payment equal).
@rate : constant interest rate
@nper : overall number of payments
@pv : present value
@fv : future value
@type : payment type
. 0 : end of period
. 1 : beginning of period
* IPMT( rate, per, nper, pv, fv, type )
IPMT calculates the amount of a payment of an annuity going towards
interest. Formula for IPMT is:
IPMT(per) = - principal(per-1) * interest_rate
where:
principal(per-1) = amount of the remaining principal from last period.
* ISPMT( rate, per, nper, pv )
ISPMT returns the interest paid on a given period.
If @per < 1 or @per > @nper, returns #NUM! err.
* PPMT(rate, per, nper, pv [, fv, type] )
PPMT calculates the amount of a payment of an annuity going towards
principal.
PPMT(per) = PMT - IPMT(per)
where: PMT is payment
IPMT is interest for period per
* PV( rate, nper, pmt [, fv, type] )
Calculates the present value of an investment
@rate : periodic interest rate
@nper : number of compounding periods
@pmt : payment made each period
@fv : future value
--------------------------------------------------
--------------------------------------------------
Day Two:
As per warlord's comments, the definition of IPMT needs to be updated to
account for principal pre-payment. IPMT is actually defined by computation
of the value of an account at a specified point in time. This is significant
if the loan repayments involve interest.
In the face of creating multiple scheduled transactions for a time range, it
may be the case that the relevant account balance is not actually posted to
the account at the time of the variable binding. If we intend to show the
user an estimation of the IPMT cell value during variable binding, then we
would need to do something creative about this ... but as it stands, we'll
leave this as an Excercise for the Reader. :)
-----
Druid thoughts...
Page Order:
Intro ->
Params ->
Opts ->
Repayment ->
[Insurance ->]
[PMI ->]
[Taxes ->]
Review/Approval
--------------------------------------------------
| Intro
--------------------------------------------------
"This is a step-by-step method of creating a loan
repayment setup within GnuCash. In this Druid,
you can input the parameters of your loan and
it's repayment and give the details of it's
payback. Using that information, the appropriate
Scheduled Transactions will be created.
"If you make a mistake or want to make changes
later, you can edit the created Scheduled
Transactions directly."
--------------------------------------------------
--------------------------------------------------
| Params
--------------------------------------------------
Principal : [amount entry]
Actual Principal : [[optional] amount entry]
Interest Rate : [numeric entry] %
Type : [ ] Fixed
[ ] Variable ---------+
| Type : 10/1,7/1,...|
| When : [freqspec?] |
+---------------------+
Start Date : [Gnome Date Entry]
Length : [num entry] [years|v]
Remaining : [num entry]
--------------------------------------------------
--------------------------------------------------
| Options
--------------------------------------------------
Do you...
[ ] ... utilize an escrow account?
Account: [ acct select |v]
[ ] ... pay PMI?
[ ] Via the Escrow account?
[ ] ... pay insurance?
[ ] Via the Escrow account?
[ ] ... pay taxes?
[ ] Via the Escrow account?
[ ] ... have some other expense not listed above?
[ ] Via the Escrow account?
--------------------------------------------------
--------------------------------------------------
| Repayment
--------------------------------------------------
Amount : [ amount entry ]
Assets from : [ account sel |v]
Princiapl to : [ account sel |v]
Interest to : [ account sel |v]
Escrow to : [ account sel |v]
Remainder to : [{escrow,principal,interest}|v]
Frequency : +- freqspec ----------------+
| .... |
+---------------------------+
--------------------------------------------------
--------------------------------------------------
| Insurance
--------------------------------------------------
Amount : [ amount entry ]
Account : [ account sel |v]
Frequency :
[ ] Part of Repayment Transaction
[ ] Other: +- freqspec ----------------+
| .... |
+---------------------------+
--------------------------------------------------
--------------------------------------------------
| Taxes/PMI/Other
--------------------------------------------------
[ same as Insurance ]
--------------------------------------------------
===========================================================================
===========================================================================
Options in repayment:
. loan freq != repayment freq
. floated
. not
. Where does over-payment go?
. where
. into the escrow account
. directly applied
. how
. towards principal [interest is then re-calculated]
. towards interest [principal is then re-calculated]
. still to do...
. expression parser/gnc-exp-parser extensions to handle...
. ...symbols [account names] into functions
. ...errors better
. ...iter/count/implicit vars
. druid...
. add ipmt', ppmt' calculations, using above
. kvp storage of "real" data
. sx grouping
http://www.interest.com/hugh/calc/mort_links.html

View File

@ -41,6 +41,7 @@ libgncgnome_la_SOURCES = \
dialog-userpass.c \
dialog-scheduledxaction.c \
druid-hierarchy.c \
druid-loan.c \
druid-stock-split.c \
gnc-network.c \
gnc-splash.c \
@ -77,6 +78,7 @@ noinst_HEADERS = \
dialog-transfer.h \
dialog-scheduledxaction.h \
druid-hierarchy.h \
druid-loan.h \
gnc-network.h \
gnc-splash.h \
gw-gnc.h \

995
src/gnome/druid-loan.c Normal file
View File

@ -0,0 +1,995 @@
/********************************************************************\
* druid-loan.c : A Gnome Druid for setting up loan-repayment *
* scheduled transactions. *
* Copyright (C) 2002 Joshua Sled <jsled@asynchronous.org> *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 59 Temple Place - Suite 330 Fax: +1-617-542-2652 *
* Boston, MA 02111-1307, USA gnu@gnu.org *
\********************************************************************/
#include "config.h"
#include <string.h>
#include <gtk/gtk.h>
#include <glib.h>
#include <glade/glade.h>
#include <gnome.h>
#include "druid-loan.h"
#include "gnc-component-manager.h"
#include "dialog-utils.h"
#include "Account.h"
#include "FreqSpec.h"
#include "gnc-ui.h"
#include "gnc-gui-query.h"
#include "gnc-ui-util.h"
#include "gnc-frequency.h"
#define DIALOG_LOAN_DRUID_CM_CLASS "druid-loan-setup"
#define SX_GLADE_FILE "sched-xact.glade"
#define LOAN_DRUID_WIN_GLADE_NAME "loan_druid_win"
#define LOAN_DRUID_GLADE_NAME "loan_druid"
#define PG_INTRO "loan_intro_pg"
#define PG_INFO "loan_info_pg"
# define PARAM_TABLE "param_table"
# define ORIG_PRINC_GNE "orig_princ_gne"
# define ORIG_PRINC_ENTRY "orig_princ_ent"
# define CUR_PRINC_GNE "cur_princ_gne"
# define CUR_PRINC_ENTRY "cur_princ_ent"
# define IRATE_SPIN "irate_spin"
# define TYPE_OPT "type_opt"
# define VAR_CONTAINER "type_freq_frame"
# define START_DATE "start_gde"
# define LENGTH_SPIN "len_spin"
# define LENGTH_OPT "len_opt"
# define REMAIN_SPIN "rem_spin"
#define PG_OPTS "loan_opts_pg"
# define OPT_CONTAINER "opt_vbox"
# define OPT_ESCROW "opt_escrow"
# define OPT_ESCROW_CONTAINER "opt_escrow_hbox"
#define PG_REPAYMENT "repayment_pg"
# define TXN_NAME "txn_title"
# define REPAY_TABLE "repay_table"
# define AMOUNT_GNE "amount_gne"
# define AMOUNT_ENTRY "amount_ent"
# define REMAINDER_OPT "remain_opt"
# define FREQ_CONTAINER "freq_frame"
#define PG_PAYMENT "payment_pg"
# define PAY_TXN_TITLE "pay_txn_title"
# define PAY_AMT_GNE "pay_amt_gne"
# define PAY_AMT_ENTRY "pay_amt_ent"
# define PAY_TABLE "pay_table"
# define PAY_TXN_PART_RB "pay_txn_part_rb"
# define PAY_UNIQ_FREQ_RB "pay_uniq_freq_rb"
# define PAY_FREQ_CONTAINER "pay_freq_align"
#define PG_FINISH "finish_pg"
#define OPT_VBOX_SPACING 2
/**
* TODO/fixme:
* . param account selection should fill in orig/cur principal amounts from
* the books.
* . initialize type freq to monthly.
* . if LoanType <- !FIXED
* . Frequency <- sensitive
**/
/**
* The data relating to a single "repayment option" -- a potential
* [sub-]transaction in the repayment.
**/
typedef struct RepayOptData_ {
gboolean enabled;
char *name; // { "insurance", "pmi", "taxes", ... }
char *txnMemo;
float amount;
gboolean throughEscrowP;
Account *to;
Account *from; // If NULL { If throughEscrowP, then through escrowP;
// else: undefined.
FreqSpec *fs; // If NULL, part of repayment; otherwise: defined here.
GDate *startDate;
} RepayOptData;
/**
* The default repayment options data.
**/
typedef struct RepayOptDataDefault_ {
char *name;
char *defaultTxnMemo;
gboolean escrowDefault;
} RepayOptDataDefault;
static RepayOptDataDefault REPAY_DEFAULTS[] = {
/* { name, default txn memo, throughEscrowP } */
{ "Taxes", "Tax Payment", FALSE },
{ "Insurance", "Insurance Payment", FALSE },
{ "PMI", "PMI Payment", FALSE },
{ "Other Expense", "Miscellaneous Payment", FALSE },
{ NULL }
};
/**
* The UI-side storage of the repayment options.
**/
typedef struct RepayOptUI_ {
GtkCheckButton *optCb;
GtkCheckButton *escrowCb;
RepayOptData *optData;
} RepayOptUIData;
typedef enum {
FIXED = 0,
VARIABLE,
VARIABLE_5_1 = VARIABLE,
VARIABLE_7_1,
VARIABLE_10_1,
/* ... FIXME */
} LoanType;
typedef enum {
MONTHS = 0,
YEARS
} PeriodSize;
/**
* Data about a loan repayment.
**/
typedef struct LoanData_ {
Account *primaryAcct;
float principal;
float principalLeft;
float interestRate;
LoanType type;
FreqSpec *loanFreq;
GDate *startDate;
GDate *varStartDate;
int numPer;
int numPerRemain;
PeriodSize perSize;
int repayOptCount;
RepayOptData **repayOpts;
} LoanData;
/**
* The UI-side storage of the loan druid data.
**/
typedef struct LoanDruidData_ {
GladeXML *gxml;
GtkWidget *dialog;
GnomeDruid *druid;
LoanData ld;
/* The UI-side storage of repayment data; this is 1:1 with the array
* in LoanData */
RepayOptUIData **repayOptsUI;
/* Current index of the payment opt for multiplexing the 'payment'
* page. */
int currentIdx;
/* widgets */
/* prm = params */
GtkTable *prmTable;
GnomeNumberEntry *prmOrigPrincGNE;
GtkEntry *prmOrigPrincEntry;
GnomeNumberEntry *prmCurPrincGNE;
GtkEntry *prmCurPrincEntry;
GtkSpinButton *prmIrateSpin;
GtkOptionMenu *prmType;
GtkFrame *prmVarFrame;
GNCFrequency *prmVarGncFreq;
GnomeDateEdit *prmStartDateGDE;
GtkSpinButton *prmLengthSpin;
GtkOptionMenu *prmLengthType;
GtkSpinButton *prmRemainSpin;
/* opt = options */
GtkVBox *optVBox;
GtkCheckButton *optEscrowOpt;
GtkHBox *optEscrowHBox;
/* rep = repayment */
GtkEntry *repTxnName;
GtkTable *repTable;
GnomeNumberEntry *repAmtGNE;
GtkEntry *repAmtEntry;
GtkOptionMenu *repRemainderOpt;
GtkFrame *repFreqFrame;
GNCFrequency *repGncFreq;
/* pay = payment[s] */
GtkEntry *payTxnName;
GnomeNumberEntry *payAmtGNE;
GtkEntry *payAmtEntry;
GtkTable *payTable;
GtkRadioButton *payTxnFreqPartRb;
GtkRadioButton *payTxnFreqUniqRb;
GtkAlignment *payFreqAlign;
GNCFrequency *payGncFreq;
} LoanDruidData;
static void gnc_loan_druid_data_init( LoanDruidData *ldd );
static void gnc_loan_druid_get_widgets( LoanDruidData *ldd );
static void ld_close_handler( LoanDruidData *ldd );
static void ld_destroy( GtkObject *o, gpointer ud );
static void ld_cancel_check( GnomeDruid *gd, LoanDruidData *ldd );
static void ld_opt_toggled( GtkToggleButton *tb, gpointer ud );
static void ld_opt_consistency( GtkToggleButton *tb, gpointer ud );
static void ld_escrow_tog( GtkToggleButton *tb, gpointer ud );
static void ld_pay_freq_toggle( GtkToggleButton *tb, gpointer ud );
static gboolean ld_info_save( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static void ld_info_prep( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static gboolean ld_opts_next( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static void ld_opts_prep( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static gboolean ld_rep_next ( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static void ld_rep_prep ( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static gboolean ld_pay_next ( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static void ld_pay_prep ( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static gboolean ld_pay_back ( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
static gboolean ld_fin_back ( GnomeDruidPage *gdp, gpointer arg1, gpointer ud );
struct LoanDruidData_*
gnc_ui_sx_loan_druid_create()
{
static struct {
char *pageName;
gboolean (*nextFn)();
void (*prepFn)();
gboolean (*backFn)();
void (*finishFn)();
/* cancel is handled by the druid itself. */
} DRUID_HANDLERS[] = {
{ PG_INFO, ld_info_save, ld_info_prep, ld_info_save, NULL },
{ PG_OPTS, ld_opts_next, ld_opts_prep, NULL, NULL },
{ PG_REPAYMENT, ld_rep_next, ld_rep_prep },
{ PG_PAYMENT, ld_pay_next, ld_pay_prep, ld_pay_back, NULL },
{ PG_FINISH, NULL, NULL, ld_fin_back, NULL },
{ NULL }
};
int i;
LoanDruidData *ldd;
ldd = g_new0( LoanDruidData, 1 );
gnc_loan_druid_data_init( ldd );
ldd->gxml = gnc_glade_xml_new( SX_GLADE_FILE, LOAN_DRUID_WIN_GLADE_NAME );
ldd->dialog = glade_xml_get_widget( ldd->gxml, LOAN_DRUID_WIN_GLADE_NAME );
ldd->druid = GNOME_DRUID(glade_xml_get_widget( ldd->gxml,
LOAN_DRUID_GLADE_NAME ));
/* get pointers to the various widgets */
gnc_loan_druid_get_widgets( ldd );
/* FIXME: non-gladeable widget setup */
{
{
GtkAdjustment *a;
/* 8.0 [%], range of 0.05..100.0 with ticks at 0.05[%]. */
a = GTK_ADJUSTMENT(gtk_adjustment_new( 8.0, 0.05,
100.0, 0.05,
1.0, 1.0 ));
gtk_spin_button_set_adjustment( ldd->prmIrateSpin, a );
gtk_spin_button_set_value( ldd->prmIrateSpin, 8.00 );
gtk_spin_button_set_snap_to_ticks( ldd->prmIrateSpin,
TRUE );
a = GTK_ADJUSTMENT(gtk_adjustment_new( 360, 1,
9999, 1,
12, 12 ));
gtk_spin_button_set_adjustment( ldd->prmLengthSpin, a );
a = GTK_ADJUSTMENT(gtk_adjustment_new( 360, 1,
9999, 1,
12, 12 ));
gtk_spin_button_set_adjustment( ldd->prmRemainSpin, a );
}
gnc_option_menu_init( GTK_WIDGET(ldd->prmType) );
gnc_option_menu_init( GTK_WIDGET(ldd->prmLengthType) );
gnc_option_menu_init( GTK_WIDGET(ldd->repRemainderOpt) );
/* PARAM_TABLE(0,1) += account sel */
/* OPT_ESCROW_CONTAINER += account sel */
{
}
/* OPT_CONTAINER += option options. */
/* . Each RepayOpt gets an entry in the optContainer.
* . Each "entry" is a 2-line vbox containing:
* . The checkbox for the option itself
* . an alignment-contained sub-checkbox for "through the
* escrow account".
* . Hook up each to bit-twiddling the appropriate line.
*/
/* FIXME : too deep, factor out. */
{
RepayOptUIData *rouid;
GtkVBox *vb;
GtkAlignment *optAlign, *subOptAlign;
GString *str;
str = g_string_sized_new( 32 );
for ( i=0; i<ldd->ld.repayOptCount; i++ ) {
rouid = ldd->repayOptsUI[i];
vb = GTK_VBOX(gtk_vbox_new( FALSE, OPT_VBOX_SPACING ));
/* Add payment checkbox. */
g_string_sprintf( str, "... pay \"%s\"?", rouid->optData->name );
rouid->optCb =
GTK_CHECK_BUTTON(gtk_check_button_new_with_label( str->str ));
gtk_box_pack_start( GTK_BOX(vb), GTK_WIDGET(rouid->optCb),
FALSE, FALSE, 2 );
rouid->escrowCb =
GTK_CHECK_BUTTON(gtk_check_button_new_with_label( _("via Escrow account?") ));
gtk_widget_set_sensitive( GTK_WIDGET(rouid->escrowCb), FALSE );
subOptAlign = GTK_ALIGNMENT(gtk_alignment_new( 0.5, 0.5, 0.75, 1.0 ));
gtk_container_add( GTK_CONTAINER(subOptAlign),
GTK_WIDGET(rouid->escrowCb) );
gtk_box_pack_start( GTK_BOX(vb), GTK_WIDGET(subOptAlign),
FALSE, FALSE, 2 );
gtk_signal_connect( GTK_OBJECT( rouid->optCb ),
"toggled",
GTK_SIGNAL_FUNC(ld_opt_toggled),
(gpointer)rouid );
gtk_signal_connect( GTK_OBJECT( rouid->optCb ),
"toggled",
GTK_SIGNAL_FUNC(ld_opt_consistency),
(gpointer)rouid->escrowCb );
gtk_signal_connect( GTK_OBJECT( rouid->escrowCb ),
"toggled",
GTK_SIGNAL_FUNC(ld_escrow_tog),
(gpointer)rouid );
optAlign = GTK_ALIGNMENT(gtk_alignment_new( 0.5, 0.5, 0.75, 1.0 ));
gtk_container_add( GTK_CONTAINER(optAlign),
GTK_WIDGET(vb) );
gtk_box_pack_start( GTK_BOX(ldd->optVBox), GTK_WIDGET(optAlign),
FALSE, FALSE, 2 );
gtk_widget_show_all( GTK_WIDGET(optAlign) );
}
g_string_free( str, TRUE );
}
gtk_signal_connect( GTK_OBJECT(ldd->payTxnFreqPartRb), "toggled",
GTK_SIGNAL_FUNC(ld_pay_freq_toggle), (gpointer)ldd );
gtk_signal_connect( GTK_OBJECT(ldd->payTxnFreqUniqRb), "toggled",
GTK_SIGNAL_FUNC(ld_pay_freq_toggle), (gpointer)ldd );
ldd->prmVarGncFreq =
GNC_FREQUENCY(gnc_frequency_new( NULL, NULL ));
gtk_container_add( GTK_CONTAINER(ldd->prmVarFrame),
GTK_WIDGET(ldd->prmVarGncFreq) );
ldd->repGncFreq =
GNC_FREQUENCY(gnc_frequency_new( NULL, NULL ));
gtk_container_add( GTK_CONTAINER(ldd->repFreqFrame),
GTK_WIDGET(ldd->repGncFreq) );
ldd->payGncFreq =
GNC_FREQUENCY(gnc_frequency_new( NULL, NULL ));
gtk_container_add( GTK_CONTAINER(ldd->payFreqAlign),
GTK_WIDGET(ldd->payGncFreq) );
/* FIXME: Repayment/Payment[s] pages += account sel */
}
gnc_register_gui_component( DIALOG_LOAN_DRUID_CM_CLASS,
NULL, /* no refresh handler */
ld_close_handler,
ldd );
gtk_signal_connect( GTK_OBJECT(ldd->dialog), "destroy",
GTK_SIGNAL_FUNC(ld_destroy),
ldd );
/* setup page-transition handlers */
/* setup druid-global handler for cancel */
gtk_signal_connect( GTK_OBJECT(ldd->druid), "cancel",
GTK_SIGNAL_FUNC(ld_cancel_check),
(gpointer)ldd );
/* FIXME: this is substantially similar to the code in
* dialog-sxsincelast.c ... it should probably be factored out
* somewhere. */
for ( i=0; DRUID_HANDLERS[i].pageName != NULL; i++ ) {
GtkWidget *pg;
pg = glade_xml_get_widget( ldd->gxml,
DRUID_HANDLERS[i].pageName );
if ( DRUID_HANDLERS[i].prepFn ) {
gtk_signal_connect( GTK_OBJECT(pg), "prepare",
GTK_SIGNAL_FUNC(DRUID_HANDLERS[i].
prepFn),
ldd);
}
if ( DRUID_HANDLERS[i].backFn ) {
gtk_signal_connect( GTK_OBJECT(pg), "back",
GTK_SIGNAL_FUNC(DRUID_HANDLERS[i].
backFn),
ldd);
}
if ( DRUID_HANDLERS[i].nextFn ) {
gtk_signal_connect( GTK_OBJECT(pg), "next",
GTK_SIGNAL_FUNC(DRUID_HANDLERS[i].
nextFn),
ldd);
}
if ( DRUID_HANDLERS[i].finishFn ) {
gtk_signal_connect( GTK_OBJECT(pg), "finish",
GTK_SIGNAL_FUNC(DRUID_HANDLERS[i].
finishFn),
ldd);
}
}
gtk_widget_show_all( ldd->dialog );
return ldd;
}
static
void
gnc_loan_druid_data_init( LoanDruidData *ldd )
{
int i, optCount;
RepayOptData *optData;
/* get the count of repayment defaults. */
for ( optCount=0; REPAY_DEFAULTS[optCount].name != NULL; optCount++ )
;
ldd->currentIdx = -1;
ldd->ld.startDate = g_date_new();
ldd->ld.varStartDate = g_date_new();
g_date_set_time( ldd->ld.startDate, time(NULL) );
ldd->ld.loanFreq = xaccFreqSpecMalloc( gnc_get_current_book() );
ldd->ld.repayOptCount = optCount;
ldd->ld.repayOpts = g_new0( RepayOptData*, optCount );
/* copy all the default lines into the LDD */
ldd->repayOptsUI = g_new0( RepayOptUIData*, optCount );
for ( i=0; i < optCount; i++ ) {
g_assert( REPAY_DEFAULTS[i].name != NULL );
ldd->repayOptsUI[i] = g_new0( RepayOptUIData, 1 );
optData = ldd->ld.repayOpts[i]
= ldd->repayOptsUI[i]->optData
= g_new0( RepayOptData, 1 );
optData->enabled = FALSE;
optData->name = g_strdup( REPAY_DEFAULTS[i].name );
optData->txnMemo = g_strdup( REPAY_DEFAULTS[i].
defaultTxnMemo );
optData->amount = 0.0;
optData->throughEscrowP = REPAY_DEFAULTS[i].escrowDefault;
optData->fs = NULL;
optData->startDate = NULL;
}
}
static
void
gnc_loan_druid_get_widgets( LoanDruidData *ldd )
{
g_assert( ldd != NULL );
g_assert( ldd->gxml != NULL );
/* Get all widgets */
#define GET_CASTED_WIDGET( cast, name ) \
(cast( glade_xml_get_widget( ldd->gxml, name ) ))
/* prm = params */
ldd->prmTable =
GET_CASTED_WIDGET( GTK_TABLE, PARAM_TABLE );
ldd->prmOrigPrincGNE =
GET_CASTED_WIDGET( GNOME_NUMBER_ENTRY, ORIG_PRINC_GNE );
ldd->prmOrigPrincEntry =
GET_CASTED_WIDGET( GTK_ENTRY, ORIG_PRINC_ENTRY );
ldd->prmCurPrincGNE =
GET_CASTED_WIDGET( GNOME_NUMBER_ENTRY, CUR_PRINC_GNE );
ldd->prmCurPrincEntry =
GET_CASTED_WIDGET( GTK_ENTRY, CUR_PRINC_ENTRY );
ldd->prmIrateSpin =
GET_CASTED_WIDGET( GTK_SPIN_BUTTON, IRATE_SPIN );
ldd->prmType =
GET_CASTED_WIDGET( GTK_OPTION_MENU, TYPE_OPT );
ldd->prmVarFrame =
GET_CASTED_WIDGET( GTK_FRAME, VAR_CONTAINER );
ldd->prmStartDateGDE =
GET_CASTED_WIDGET( GNOME_DATE_EDIT, START_DATE );
ldd->prmLengthSpin =
GET_CASTED_WIDGET( GTK_SPIN_BUTTON, LENGTH_SPIN );
ldd->prmLengthType =
GET_CASTED_WIDGET( GTK_OPTION_MENU, LENGTH_OPT );
ldd->prmRemainSpin =
GET_CASTED_WIDGET( GTK_SPIN_BUTTON, REMAIN_SPIN );
/* opt = options */
ldd->optVBox =
GET_CASTED_WIDGET( GTK_VBOX, OPT_CONTAINER );
ldd->optEscrowOpt =
GET_CASTED_WIDGET( GTK_CHECK_BUTTON, OPT_ESCROW );
ldd->optEscrowHBox =
GET_CASTED_WIDGET( GTK_HBOX, OPT_ESCROW_CONTAINER );
/* rep = repayment */
ldd->repTxnName =
GET_CASTED_WIDGET( GTK_ENTRY, TXN_NAME );
ldd->repTable =
GET_CASTED_WIDGET( GTK_TABLE, REPAY_TABLE );
ldd->repAmtGNE =
GET_CASTED_WIDGET( GNOME_NUMBER_ENTRY, AMOUNT_GNE );
ldd->repAmtEntry =
GET_CASTED_WIDGET( GTK_ENTRY, AMOUNT_ENTRY );
ldd->repRemainderOpt =
GET_CASTED_WIDGET( GTK_OPTION_MENU, REMAINDER_OPT );
ldd->repFreqFrame =
GET_CASTED_WIDGET( GTK_FRAME, FREQ_CONTAINER );
/* pay = payment[s] */
ldd->payTxnName =
GET_CASTED_WIDGET( GTK_ENTRY, PAY_TXN_TITLE );
ldd->payAmtGNE =
GET_CASTED_WIDGET( GNOME_NUMBER_ENTRY, PAY_AMT_GNE );
ldd->payAmtEntry =
GET_CASTED_WIDGET( GTK_ENTRY, PAY_AMT_ENTRY );
ldd->payTable =
GET_CASTED_WIDGET( GTK_TABLE, PAY_TABLE );
ldd->payTxnFreqPartRb =
GET_CASTED_WIDGET( GTK_RADIO_BUTTON, PAY_TXN_PART_RB );
ldd->payTxnFreqUniqRb =
GET_CASTED_WIDGET( GTK_RADIO_BUTTON, PAY_UNIQ_FREQ_RB );
ldd->payFreqAlign =
GET_CASTED_WIDGET( GTK_ALIGNMENT, PAY_FREQ_CONTAINER );
}
static
void
ld_close_handler( LoanDruidData *ldd )
{
gtk_widget_hide( ldd->dialog );
gtk_widget_destroy( ldd->dialog );
}
static
void
ld_destroy( GtkObject *o, gpointer ud )
{
LoanDruidData *ldd;
ldd = (LoanDruidData*)ud;
g_assert( ldd );
gnc_unregister_gui_component_by_data
(DIALOG_LOAN_DRUID_CM_CLASS, ldd);
/* FIXME: free alloc'd mem; cleanup */
g_free( ldd );
}
static
void
ld_cancel_check( GnomeDruid *gd, LoanDruidData *ldd )
{
const char *cancelMsg = _( "Are you sure you want to close "
"the Mortgage/Loan Setup Druid?" );
if ( gnc_verify_dialog_parented( ldd->dialog, FALSE, cancelMsg ) ) {
gnc_close_gui_component_by_data( DIALOG_LOAN_DRUID_CM_CLASS,
ldd );
}
}
static
void
ld_opt_toggled( GtkToggleButton *tb, gpointer ud )
{
RepayOptUIData *rouid;
rouid = (RepayOptUIData*)ud;
rouid->optData->enabled = gtk_toggle_button_get_active(tb);
}
static
void
ld_opt_consistency( GtkToggleButton *tb, gpointer ud )
{
GtkToggleButton *escrowCb;
escrowCb = GTK_TOGGLE_BUTTON(ud);
/* make sure the escrow option is only selected if we're active. */
gtk_toggle_button_set_state( escrowCb,
gtk_toggle_button_get_active(escrowCb)
& gtk_toggle_button_get_active(tb) );
/* make sure the escrow option is only sensitive if we're active. */
gtk_widget_set_sensitive( GTK_WIDGET(escrowCb),
gtk_toggle_button_get_active(tb) );
}
static
void
ld_escrow_tog( GtkToggleButton *tb, gpointer ud )
{
RepayOptUIData *rouid;
rouid = (RepayOptUIData*)ud;
rouid->optData->throughEscrowP = gtk_toggle_button_get_active( tb );
}
static
gboolean
ld_info_save( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
LoanDruidData *ldd;
float amt;
gchar *txt;
ldd = (LoanDruidData*)ud;
/* FIXME: account */
txt = gtk_editable_get_chars( GTK_EDITABLE(ldd->prmOrigPrincEntry),
0, -1 );
amt = -1.0;
amt = (float)strtod( txt, NULL );
if ( amt < 0 ) {
gnc_error_dialog( _("The original principal must "
"be a valid number.") );
return TRUE;
}
ldd->ld.principal = amt;
g_free( txt );
txt = gtk_editable_get_chars( GTK_EDITABLE(ldd->prmCurPrincEntry),
0, -1 );
amt = -1.0;
amt = (float)strtod( txt, NULL );
if ( amt < 0 ) {
gnc_error_dialog( _("The current principal must "
"be a valid number.") );
return TRUE;
}
ldd->ld.principalLeft = amt;
ldd->ld.interestRate =
gtk_spin_button_get_value_as_float( ldd->prmIrateSpin );
ldd->ld.type = gnc_option_menu_get_active( GTK_WIDGET(ldd->prmType) );
if ( ldd->ld.type != FIXED ) {
gnc_frequency_save_state( ldd->prmVarGncFreq,
ldd->ld.loanFreq,
ldd->ld.varStartDate );
}
/* start date */
{
time_t tmpTT;
struct tm *tmpTm;
tmpTT = gnome_date_edit_get_date( ldd->prmStartDateGDE );
tmpTm = localtime( &tmpTT );
g_date_set_dmy( ldd->ld.startDate,
1, (tmpTm->tm_mon+1), tmpTm->tm_mday );
}
/* len / periods */
{
ldd->ld.perSize =
(gnc_option_menu_get_active( GTK_WIDGET(ldd->prmLengthType) )
== MONTHS) ? MONTHS : YEARS;
ldd->ld.numPer =
gtk_spin_button_get_value_as_int( ldd->prmLengthSpin );
ldd->ld.numPerRemain =
gtk_spin_button_get_value_as_int( ldd->prmRemainSpin );
}
return FALSE;
}
static
void
ld_info_prep( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
LoanDruidData *ldd;
GString *str;
ldd = (LoanDruidData*)ud;
str = g_string_sized_new( 16 );
/* FIXME: account. */
g_string_sprintf( str, "%0.2f", ldd->ld.principal );
gtk_entry_set_text( ldd->prmOrigPrincEntry, str->str );
g_string_sprintf( str, "%0.2f", ldd->ld.principalLeft );
gtk_entry_set_text( ldd->prmCurPrincEntry, str->str );
gtk_spin_button_set_value( ldd->prmIrateSpin, ldd->ld.interestRate );
gtk_option_menu_set_history( ldd->prmType, ldd->ld.type );
if ( ldd->ld.type != FIXED ) {
gnc_frequency_setup( ldd->prmVarGncFreq,
ldd->ld.loanFreq,
ldd->ld.varStartDate );
}
/* start date */
{
struct tm *tmpTm;
tmpTm = g_new0( struct tm, 1 );
g_date_to_struct_tm( ldd->ld.startDate, tmpTm );
gnome_date_edit_set_time( ldd->prmStartDateGDE,
mktime(tmpTm) );
g_free( tmpTm );
}
/* length: total and remaining */
{
// prmLengthSpin
gtk_spin_button_set_value( ldd->prmLengthSpin, ldd->ld.numPer );
// prmLengthType
gtk_option_menu_set_history( ldd->prmLengthType, ldd->ld.perSize );
// prmRemainSpin
gtk_spin_button_set_value( ldd->prmRemainSpin, ldd->ld.numPerRemain );
}
g_string_free( str, TRUE );
}
static
gboolean
ld_opts_next( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
/* FIXME: do anything here? */
return FALSE;
}
static
void
ld_opts_prep( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
/* FIXME: setup the options */
}
static
gboolean
ld_rep_next( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
LoanDruidData *ldd;
ldd = (LoanDruidData*)ud;
if ( (ldd->currentIdx < 0)
|| (ldd->currentIdx >= ldd->ld.repayOptCount)
|| !ldd->ld.repayOpts[ldd->currentIdx]->enabled ) {
int i;
for ( i=0;
(i < ldd->ld.repayOptCount)
&& !ldd->ld.repayOpts[i]->enabled;
i++ )
;
if ( i == ldd->ld.repayOptCount ) {
/* transition to final page. */
GtkWidget *pg;
pg = glade_xml_get_widget( ldd->gxml, PG_FINISH );
gnome_druid_set_page( ldd->druid, GNOME_DRUID_PAGE(pg) );
return TRUE;
}
ldd->currentIdx = i;
}
return FALSE;
}
static
void
ld_rep_prep( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
/* do anything? */
}
static
void
ld_pay_prep( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
LoanDruidData *ldd;
RepayOptData *rod;
GString *str;
gboolean uniq;
ldd = (LoanDruidData*)ud;
g_assert( ldd->currentIdx >= 0 );
g_assert( ldd->currentIdx <= ldd->ld.repayOptCount );
rod = ldd->ld.repayOpts[ldd->currentIdx];
str = g_string_sized_new( 32 );
g_string_sprintf( str, "Payment: \"%s\"", rod->name );
gnome_druid_page_standard_set_title( GNOME_DRUID_PAGE_STANDARD(gdp),
str->str );
/* FIXME: copy in the relevant data from the currently-indexed
* option. */
gtk_entry_set_text( ldd->payTxnName, rod->txnMemo );
g_string_sprintf( str, "%0.2f", rod->amount );
gtk_entry_set_text( ldd->payAmtEntry, str->str );
uniq = (rod->fs != NULL);
gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(ldd->payTxnFreqPartRb),
!uniq );
gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(ldd->payTxnFreqUniqRb),
uniq );
gtk_widget_set_sensitive( GTK_WIDGET(ldd->payFreqAlign),
uniq );
if ( uniq ) {
gnc_frequency_setup( ldd->payGncFreq,
rod->fs, rod->startDate );
}
g_string_free( str, TRUE );
}
static
void
ld_pay_save_current( LoanDruidData *ldd )
{
gchar *tmpStr;
RepayOptData *rod;
g_assert( ldd->currentIdx >= 0 );
g_assert( ldd->currentIdx <= ldd->ld.repayOptCount );
rod = ldd->ld.repayOpts[ ldd->currentIdx ];
tmpStr = gtk_editable_get_chars( GTK_EDITABLE(ldd->payTxnName),
0, -1 );
if ( rod->txnMemo != NULL ) {
g_free( rod->txnMemo );
}
rod->txnMemo = tmpStr;
tmpStr = NULL;
tmpStr = gtk_editable_get_chars( GTK_EDITABLE(ldd->payAmtEntry),
0, -1 );
rod->amount = (float)strtod( tmpStr, NULL );
g_free( tmpStr );
/* if ( rb toggled )
* ensure freqspec/startdate setup
* save
* else
* if (freqspec setup)
* remove freqspec/startdate
*/
/* neither of these should happen. */
g_assert( ! (rod->fs && !rod->startDate) );
g_assert( ! (!rod->fs && rod->startDate) );
if ( gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ldd->payTxnFreqUniqRb)) ) {
if ( rod->fs == NULL ) {
rod->fs = xaccFreqSpecMalloc( gnc_get_current_book() );
}
if ( rod->startDate == NULL ) {
rod->startDate = g_date_new();
}
gnc_frequency_save_state( ldd->payGncFreq,
rod->fs, rod->startDate );
} else {
if ( rod->fs ) {
xaccFreqSpecFree( rod->fs );
rod->fs = NULL;
}
if ( rod->startDate ) {
g_date_free( rod->startDate );
rod->startDate = NULL;
}
}
}
static
gboolean
ld_pay_next( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
int i;
LoanDruidData *ldd;
ldd = (LoanDruidData*)ud;
/* save current data */
ld_pay_save_current( ldd );
/* Go through opts list and select next enabled option. */
for ( i=(++ldd->currentIdx);
(i < ldd->ld.repayOptCount)
&& !ldd->ld.repayOpts[i]->enabled; i++ )
;
if ( i != ldd->ld.repayOptCount ) {
ldd->currentIdx = i;
ld_pay_prep( gdp, arg1, ud );
return TRUE;
}
return FALSE;
}
static
gboolean
ld_pay_back( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
int i;
LoanDruidData *ldd;
ldd = (LoanDruidData*)ud;
/* save current data */
ld_pay_save_current( ldd );
for ( i=(--ldd->currentIdx);
(i > -1) && !ldd->ld.repayOpts[i]->enabled;
i-- ) {
}
if ( i != -1 ) {
ldd->currentIdx = i;
ld_pay_prep( gdp, arg1, ud );
return TRUE;
}
return FALSE;
}
static
gboolean
ld_fin_back ( GnomeDruidPage *gdp, gpointer arg1, gpointer ud )
{
LoanDruidData *ldd;
ldd = (LoanDruidData*)ud;
if ( (ldd->currentIdx < 0)
|| (ldd->currentIdx >= ldd->ld.repayOptCount)
|| !ldd->ld.repayOpts[ldd->currentIdx]->enabled ) {
int i;
for ( i=ldd->ld.repayOptCount-1;
(i > -1)
&& !ldd->ld.repayOpts[i]->enabled;
i-- )
;
if ( i == -1 ) {
/* transition to Repayment page. */
GtkWidget *pg;
pg = glade_xml_get_widget( ldd->gxml, PG_REPAYMENT );
gnome_druid_set_page( ldd->druid, GNOME_DRUID_PAGE(pg) );
return TRUE;
}
ldd->currentIdx = i;
}
return FALSE;
}
static
void
ld_pay_freq_toggle( GtkToggleButton *tb, gpointer ud )
{
LoanDruidData *ldd;
gboolean uniq;
ldd = (LoanDruidData*)ud;
g_assert( ldd->currentIdx >= 0 );
g_assert( ldd->currentIdx <= ldd->ld.repayOptCount );
uniq = gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON(ldd->payTxnFreqUniqRb) );
gtk_widget_set_sensitive( GTK_WIDGET(ldd->payFreqAlign), uniq );
}

24
src/gnome/druid-loan.h Normal file
View File

@ -0,0 +1,24 @@
/********************************************************************\
* druid-loan.h : A Gnome Druid for setting up loan-repayment *
* scheduled transactions. *
* Copyright (C) 2002 Joshua Sled <jsled@asynchronous.org> *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 59 Temple Place - Suite 330 Fax: +1-617-542-2652 *
* Boston, MA 02111-1307, USA gnu@gnu.org *
\********************************************************************/
struct LoanDruidData_* gnc_ui_sx_loan_druid_create();

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,7 @@
#include "dialog-totd.h"
#include "dialog-transfer.h"
#include "dialog-utils.h"
#include "druid-loan.h"
#include "gfec.h"
#include "global-options.h"
#include "gnc-engine.h"
@ -723,6 +724,13 @@ gnc_main_window_sched_xaction_slr_cb (GtkWidget *widget, gpointer data) {
}
}
static
void
gnc_main_window_sx_loan_druid_cb( GtkWidget *widget, gpointer data)
{
gnc_ui_sx_loan_druid_create();
}
void
gnc_main_window_about_cb (GtkWidget *widget, gpointer data)
{
@ -921,6 +929,14 @@ gnc_main_window_create_menus(GNCMDIInfo * maininfo)
GNOME_APP_PIXMAP_NONE, NULL,
0, 0, NULL
},
GNOMEUIINFO_SEPARATOR,
{ GNOME_APP_UI_ITEM,
N_( "Mortgage/Loan Repayment Setup" ),
N_( "Setup scheduled transactions for repayment of a loan" ),
gnc_main_window_sx_loan_druid_cb, NULL, NULL,
GNOME_APP_PIXMAP_NONE, NULL,
0, 0, NULL
},
GNOMEUIINFO_END
};

View File

@ -4,12 +4,13 @@ SUBDIRS = gnumeric printing
gncscmdir = ${GNC_SCM_INSTALL_DIR}
gncscmmoddir = ${GNC_SHAREDIR}/guile-modules/gnucash
gncscmmod_DATA = process.scm main.scm price-quotes.scm
gncscmmod_DATA = process.scm main.scm price-quotes.scm fin.scm
gnc_regular_scm_files = \
build-config.scm \
command-line.scm \
doc.scm \
fin.scm \
help-topics-index.scm \
main-window.scm \
path.scm \

63
src/scm/fin.scm Normal file
View File

@ -0,0 +1,63 @@
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2 of
;; the License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, contact:
;;
;; Free Software Foundation Voice: +1-617-542-5942
;; 59 Temple Place - Suite 330 Fax: +1-617-542-2652
;; Boston, MA 02111-1307, USA gnu@gnu.org
;; Copyright 2002 Joshua Sled <jsled@asynchronous.org>
;; pretty literal copies of similar code from gnumeric-1.0.8
(define (gnc:ipmt rate nper per pv fv type)
(* rate
(- 0 (calc-principal pv
(calc-pmt rate nper pv fv type)
rate (- per 1))
)
)
)
(define (gnc:ppmt rate nper per pv fv type)
(let ((pmt (calc-pmt rate nper pv fv type)))
(let ((ipmt (* rate
(- 0 (calc-principal pv pmt rate (- per 1))))))
(- pmt ipmt)))
)
(define (gnc:pmt rate nper pv fv type)
(calc-pmt rate nper pv fv type))
(define (calc-pmt rate nper pv fv type)
(let ((pvif (calc-pvif rate nper))
(fvifa (calc-fvifa rate nper)))
(/ (- (* (- 0 pv) pvif) fv)
(* fvifa
(+ 1.0
(* rate type)))))
)
(define (calc-pvif rate nper)
(expt (+ 1 rate) nper)
)
(define (calc-fvifa rate nper)
(/ (- (expt (+ 1 rate) nper) 1) rate)
)
(define (calc-principal pv pmt rate per)
(+ (* pv (expt (+ 1.0 rate) per))
(* pmt (/ (- (expt (+ 1 rate) per)
1)
rate)))
)

View File

@ -394,6 +394,8 @@ string and 'directories' must be a list of strings."
(load-from-path "main-window.scm") ;; depends on app-utils (N_, etc.)...
(load-from-path "tip-of-the-day.scm") ;; depends on app-utils (config-var...)
(load-from-path "printing/print-check.scm") ;; depends on simple-obj...
;; +jsled - 2002.07.08
(load-from-path "fin.scm")
(gnc:initialize-tip-of-the-day)