diff --git a/ChangeLog b/ChangeLog index 1f2f8eb8d3..3155db07b7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,19 @@ +2002-06-25 Derek Atkins + + * gncOwner.[ch]: Provide functions to store/lookup an owner in a + Lot's kvp_frame. + + * gncInvoice.c: add implemention of GetPostedLot(); when posting + to an Account, look for any pre-payment Lots and post to that. If + the Invoice total < pre-payment, then autmatically forward the + payments to a future Lot. If no pre-payment Lot is found, then + create a new one. + + * dialog-payment.c: force payment amounts to be positive values + (you cannot un-pay via this dialog). Apply payments to open + Invoice Lots in a FIFO manner (by Due Date). If there is anything + left over, then create a pre-payment Lot. + 2002-06-24 David Hampton * src/import-export/ofx/Makefile.am: diff --git a/src/business/business-core/gncInvoice.c b/src/business/business-core/gncInvoice.c index 22af685056..34c0e256be 100644 --- a/src/business/business-core/gncInvoice.c +++ b/src/business/business-core/gncInvoice.c @@ -366,6 +366,12 @@ gnc_commodity * gncInvoiceGetCommonCommodity (GncInvoice *invoice) return invoice->common_commodity; } +GNCLot * gncInvoiceGetPostedLot (GncInvoice *invoice) +{ + if (!invoice) return NULL; + return invoice->posted_lot; +} + Transaction * gncInvoiceGetPostedTxn (GncInvoice *invoice) { if (!invoice) return NULL; @@ -475,12 +481,41 @@ GncInvoice * gncInvoiceGetInvoiceFromTxn (Transaction *txn) guid, _GNC_MOD_NAME); } +struct lotmatch { + GncOwner *owner; + gboolean reverse; +}; + +static gboolean +gnc_lot_match_owner_payment (GNCLot *lot, gpointer user_data) +{ + struct lotmatch *lm = user_data; + GncOwner owner_def, *owner; + gnc_numeric balance = gnc_lot_get_balance (lot); + + /* Is this a payment lot */ + if (gnc_numeric_positive_p (lm->reverse ? balance : + gnc_numeric_neg (balance))) + return FALSE; + + /* Is there an invoice attached? */ + if (gncInvoiceGetInvoiceFromLot (lot)) + return FALSE; + + /* Is it ours? */ + if (!gncOwnerGetOwnerFromLot (lot, &owner_def)) + return FALSE; + owner = gncOwnerGetEndOwner (&owner_def); + + return gncOwnerEqual (owner, lm->owner); +} + Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc, Timespec *post_date, Timespec *due_date, const char * memo) { Transaction *txn; - GNCLot *lot; + GNCLot *lot = NULL; GList *iter; GList *splitinfo = NULL; gnc_numeric total; @@ -497,8 +532,25 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc, /* Figure out if we need to "reverse" the numbers. */ reverse = (gncInvoiceGetOwnerType (invoice) == GNC_OWNER_CUSTOMER); - /* Create a new lot for this invoice */ - lot = gnc_lot_new (invoice->book); + /* Find an existing payment-lot for this owner */ + { + LotList *lot_list; + struct lotmatch lm; + + lm.reverse = reverse; + lm.owner = gncOwnerGetEndOwner (gncInvoiceGetOwner (invoice)); + + lot_list = xaccAccountFindOpenLots (acc, gnc_lot_match_owner_payment, + &lm, NULL); + if (lot_list) + lot = lot_list->data; + + g_list_free (lot_list); + } + + /* Create a new lot for this invoice, if we need to do so */ + if (!lot) + lot = gnc_lot_new (invoice->book); /* Create a new transaction */ txn = xaccMallocTransaction (invoice->book); @@ -583,9 +635,6 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc, { Split *split = xaccMallocSplit (invoice->book); - /* add this split to the lot */ - gnc_lot_add_split (lot, split); - /* Set action/memo */ xaccSplitSetMemo (split, memo); xaccSplitSetAction (split, gncInvoiceGetType (invoice)); @@ -596,6 +645,9 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc, xaccAccountInsertSplit (acc, split); xaccAccountCommitEdit (acc); xaccTransAppendSplit (txn, split); + + /* add this split to the lot */ + gnc_lot_add_split (lot, split); } /* Now attach this invoice to the txn, lot, and account */ @@ -607,6 +659,59 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc, gncAccountValueDestroy (splitinfo); + /* check the lot -- if we still look like a payment lot, then that + * means we need to create a balancing split and create a new payment + * lot for the next invoice + */ + total = gnc_lot_get_balance (lot); + if (!reverse) + total = gnc_numeric_neg (total); + + if (gnc_numeric_negative_p (total)) { + Transaction *t2; + GNCLot *lot2; + Split *split; + char *memo2 = _("Automatic Payment Forward"); + + t2 = xaccMallocTransaction (invoice->book); + lot2 = gnc_lot_new (invoice->book); + gncOwnerAttachToLot (gncOwnerGetEndOwner (gncInvoiceGetOwner (invoice)), + lot2); + + xaccTransBeginEdit (t2); + xaccAccountBeginEdit (acc); + + /* Set Transaction Description (Owner Name), Currency */ + xaccTransSetDescription (t2, name); + xaccTransSetCurrency (t2, invoice->common_commodity); + + /* Entered and Posted at date */ + if (post_date) { + xaccTransSetDateEnteredTS (t2, post_date); + xaccTransSetDatePostedTS (t2, post_date); + } + + /* Balance out this lot */ + split = xaccMallocSplit (invoice->book); + xaccSplitSetMemo (split, memo2); + xaccSplitSetBaseValue (split, gnc_numeric_neg (total), + invoice->common_commodity); + xaccAccountInsertSplit (acc, split); + xaccTransAppendSplit (t2, split); + gnc_lot_add_split (lot, split); + + /* And apply the pre-payment to a new lot */ + split = xaccMallocSplit (invoice->book); + xaccSplitSetMemo (split, memo2); + xaccSplitSetBaseValue (split, total, invoice->common_commodity); + xaccAccountInsertSplit (acc, split); + xaccTransAppendSplit (t2, split); + gnc_lot_add_split (lot2, split); + + xaccTransCommitEdit (t2); + xaccAccountCommitEdit (acc); + } + return txn; } diff --git a/src/business/business-core/gncOwner.c b/src/business/business-core/gncOwner.c index 3642220e62..60631ba0ac 100644 --- a/src/business/business-core/gncOwner.c +++ b/src/business/business-core/gncOwner.c @@ -16,6 +16,10 @@ #define _GNC_MOD_NAME GNC_OWNER_MODULE_NAME +#define GNC_OWNER_ID "gncOwner" +#define GNC_OWNER_TYPE "owner-type" +#define GNC_OWNER_GUID "owner-guid" + GncOwner * gncOwnerCreate (void) { GncOwner *o = g_new0 (GncOwner, 1); @@ -203,6 +207,68 @@ const GUID * gncOwnerGetEndGUID (GncOwner *owner) return gncOwnerGetGUID (owner); } +void gncOwnerAttachToLot (GncOwner *owner, GNCLot *lot) +{ + kvp_frame *kvp; + kvp_value *value; + + if (!owner || !lot) + return; + + kvp = gnc_lot_get_slots (lot); + + value = kvp_value_new_gint64 (gncOwnerGetType (owner)); + kvp_frame_set_slot_path (kvp, value, GNC_OWNER_ID, GNC_OWNER_TYPE, NULL); + kvp_value_delete (value); + + value = kvp_value_new_guid (gncOwnerGetGUID (owner)); + kvp_frame_set_slot_path (kvp, value, GNC_OWNER_ID, GNC_OWNER_GUID, NULL); + kvp_value_delete (value); + +} + +gboolean gncOwnerGetOwnerFromLot (GNCLot *lot, GncOwner *owner) +{ + kvp_frame *kvp; + kvp_value *value; + GUID *guid; + GNCBook *book; + GncOwnerType type; + + if (!lot) return FALSE; + + book = gnc_lot_get_book (lot); + kvp = gnc_lot_get_slots (lot); + + value = kvp_frame_get_slot_path (kvp, GNC_OWNER_ID, GNC_OWNER_TYPE, NULL); + if (!value) return FALSE; + + type = kvp_value_get_gint64 (value); + + value = kvp_frame_get_slot_path (kvp, GNC_OWNER_ID, GNC_OWNER_GUID, NULL); + if (!value) return FALSE; + + guid = kvp_value_get_guid (value); + if (!guid) + return FALSE; + + switch (type) { + case GNC_OWNER_CUSTOMER: + gncOwnerInitCustomer (owner, gncCustomerLookup (book, guid)); + break; + case GNC_OWNER_VENDOR: + gncOwnerInitVendor (owner, gncVendorLookup (book, guid)); + break; + case GNC_OWNER_JOB: + gncOwnerInitJob (owner, gncJobLookup (book, guid)); + break; + default: + return FALSE; + } + + return (owner->owner.undefined != NULL); +} + gboolean gncOwnerRegister (void) { static QueryObjectDef params[] = { diff --git a/src/business/business-core/gncOwner.h b/src/business/business-core/gncOwner.h index 4e1799fbb0..b4b719d69e 100644 --- a/src/business/business-core/gncOwner.h +++ b/src/business/business-core/gncOwner.h @@ -14,6 +14,7 @@ typedef struct gnc_owner_s GncOwner; #include "gncCustomer.h" #include "gncJob.h" #include "gncVendor.h" +#include "gnc-lot.h" typedef enum { GNC_OWNER_NONE, @@ -61,6 +62,14 @@ const GUID * gncOwnerGetGUID (GncOwner *owner); GncOwner * gncOwnerGetEndOwner (GncOwner *owner); const GUID * gncOwnerGetEndGUID (GncOwner *owner); +/* attach an owner to a lot */ +void gncOwnerAttachToLot (GncOwner *owner, GNCLot *lot); + +/* Get the owner from the lot. If an owner is found in the lot, + * fill in "owner" and return TRUE. Otherwise return FALSE. + */ +gboolean gncOwnerGetOwnerFromLot (GNCLot *lot, GncOwner *owner); + #define OWNER_TYPE "type" #define OWNER_CUSTOMER "customer" #define OWNER_JOB "job" diff --git a/src/business/business-gnome/dialog-payment.c b/src/business/business-gnome/dialog-payment.c index ce792f82a1..b5b526ce09 100644 --- a/src/business/business-gnome/dialog-payment.c +++ b/src/business/business-gnome/dialog-payment.c @@ -22,6 +22,8 @@ #include "Account.h" #include "gnc-numeric.h" +#include "gncInvoice.h" + #include "dialog-payment.h" #include "business-utils.h" @@ -69,6 +71,41 @@ gnc_payment_set_owner (PaymentWindow *pw, GncOwner *owner) gnc_owner_set_owner (pw->owner_choice, owner); } +static gboolean +gnc_lot_match_invoice_owner (GNCLot *lot, gpointer user_data) +{ + GncOwner owner_def, *owner, *this_owner = user_data; + GncInvoice *invoice; + + /* If this lot is not for this owner, then ignore it */ + invoice = gncInvoiceGetInvoiceFromLot (lot); + if (invoice) { + owner = gncInvoiceGetOwner (invoice); + owner = gncOwnerGetEndOwner (owner); + } else { + if (!gncOwnerGetOwnerFromLot (lot, &owner_def)) + return FALSE; + owner = gncOwnerGetEndOwner (&owner_def); + } + + return gncOwnerEqual (owner, this_owner); +} + +static gint +gnc_lot_sort_func (GNCLot *a, GNCLot *b) +{ + GncInvoice *ia, *ib; + Timespec da, db; + + ia = gncInvoiceGetInvoiceFromLot (a); + ib = gncInvoiceGetInvoiceFromLot (b); + + da = gncInvoiceGetDateDue (ia); + db = gncInvoiceGetDateDue (ib); + + return timespec_cmp (&da, &db); +} + static void gnc_payment_ok_cb (GtkWidget *widget, gpointer data) { @@ -82,9 +119,9 @@ gnc_payment_ok_cb (GtkWidget *widget, gpointer data) /* Verify the amount is non-zero */ amount = gnc_amount_edit_get_amount (GNC_AMOUNT_EDIT (pw->amount_edit)); - if (gnc_numeric_check (amount) || gnc_numeric_zero_p (amount)) { + if (gnc_numeric_check (amount) || !gnc_numeric_positive_p (amount)) { text = _("You must enter the amount of the payment. " - "Payments may not be zero."); + "The payment amount must be greater than zero."); gnc_error_dialog_parented (GTK_WINDOW (pw->dialog), text); return; } @@ -126,12 +163,16 @@ gnc_payment_ok_cb (GtkWidget *widget, gpointer data) } /* Ok, now post the damn thing */ + gnc_suspend_gui_refresh (); { Transaction *txn; Split *split; + GList *lot_list, *fifo = NULL; + GNCLot *lot, *prepay_lot = NULL; char *memo, *num; const char *name; gnc_commodity *commodity; + gnc_numeric split_amt; Timespec date; gboolean reverse; @@ -164,25 +205,97 @@ gnc_payment_ok_cb (GtkWidget *widget, gpointer data) xaccAccountCommitEdit (acc); xaccTransAppendSplit (txn, split); - /* The split for the posting account */ - split = xaccMallocSplit (pw->book); - xaccSplitSetMemo (split, memo); - xaccSplitSetAction (split, _("Payment")); - xaccSplitSetBaseValue (split, reverse ? gnc_numeric_neg (amount) : - amount, commodity); - xaccAccountBeginEdit (post); - xaccAccountInsertSplit (post, split); - xaccAccountCommitEdit (post); - xaccTransAppendSplit (txn, split); - - /* - * Attach the OWNER to the transaction? Or, better yet, attach to - * the proper lots from this owner in this post account + /* Now, find all "open" lots in the posting account for this + * company and apply the payment on a FIFO basis. Create + * a new split for each open lot until the payment is gone. */ - - /* Commit it */ + + fifo = xaccAccountFindOpenLots (post, gnc_lot_match_invoice_owner, + &pw->owner, + (GCompareFunc)gnc_lot_sort_func); + + xaccAccountBeginEdit (post); + + /* Now iterate over the fifo until the payment is fully applied + * (or all the lots are paid) + */ + for (lot_list = fifo; lot_list; lot_list = lot_list->next) { + gnc_numeric balance; + + lot = lot_list->data; + balance = gnc_lot_get_balance (lot); + + if (!reverse) + balance = gnc_numeric_neg (balance); + + /* If the balance is "negative" then skip this lot. + * (just save the pre-payment lot for later) + */ + if (gnc_numeric_negative_p (balance)) { + if (prepay_lot) { + g_warning ("Multiple pre-payment lots are found. Skipping."); + } else { + prepay_lot = lot; + } + continue; + } + + /* + * If the amount <= the balance; we're done -- apply the amount. + * Otherwise, apply the balance, subtract that from the amount, + * and move on to the next one. + */ + if (gnc_numeric_compare (amount, balance) <= 0) { + /* amount <= balance */ + split_amt = amount; + } else { + /* amount > balance */ + split_amt = balance; + } + + /* reduce the amount by split_amt */ + amount = gnc_numeric_sub (amount, split_amt, GNC_DENOM_AUTO, + GNC_DENOM_LCD); + + /* Create the split for this lot in the post account */ + split = xaccMallocSplit (pw->book); + xaccSplitSetMemo (split, memo); + xaccSplitSetAction (split, _("Payment")); + xaccSplitSetBaseValue (split, reverse ? gnc_numeric_neg (split_amt) : + split_amt, commodity); + xaccAccountInsertSplit (post, split); + xaccTransAppendSplit (txn, split); + gnc_lot_add_split (lot, split); + + if (gnc_numeric_zero_p (amount)) + break; + } + + g_list_free (fifo); + + /* If there is still money left here, then create a pre-payment lot */ + if (gnc_numeric_positive_p (amount)) { + if (prepay_lot == NULL) { + prepay_lot = gnc_lot_new (pw->book); + gncOwnerAttachToLot (&pw->owner, prepay_lot); + } + + split = xaccMallocSplit (pw->book); + xaccSplitSetMemo (split, memo); + xaccSplitSetAction (split, _("Payment")); + xaccSplitSetBaseValue (split, reverse ? gnc_numeric_neg (amount) : + amount, commodity); + xaccAccountInsertSplit (post, split); + xaccTransAppendSplit (txn, split); + gnc_lot_add_split (prepay_lot, split); + } + + xaccAccountCommitEdit (post); + + /* Commit this new transaction */ xaccTransCommitEdit (txn); } + gnc_resume_gui_refresh (); gnc_ui_payment_window_destroy (pw); }