Bug #608436: Add auto-clear feature

Patch by Cristian KLEIN:

= Usage scenario =
John keeps his personal accounting in GnuCash and writes all credit card
expenses therein. On weekends, we checks his Internet Banking and sees that
some transactions have been recorded. He would like to clear those transactions
in GnuCash, but it is tiresome to go through each Internet Banking transaction
and do manual comparison with GnuCash records, especially since Internet
Banking transactions might be in a different order than in GnuCash.

John would prefer having an "auto-clear" feature. Given the "current amount"
from the Internet Banking, he would like GnuCash to clear the transactions for
him, if and only if, there is a unique combination of transactions that could
achieve this. If there is no solution, or if the solution is not unique,
GnuCash should warn him and John must manually clear the transactions.

git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/trunk@18610 57a11ea4-9604-0410-9ed3-97b8803252fd
This commit is contained in:
Christian Stimming 2010-02-03 21:28:48 +00:00
parent 7069d980f9
commit d0215e6be8
8 changed files with 544 additions and 2 deletions

View File

@ -55,7 +55,8 @@ libgnc_gnome_la_SOURCES = \
lot-viewer.c \
reconcile-list.c \
top-level.c \
window-reconcile.c
window-reconcile.c \
window-autoclear.c
gnomeappdir = ${datadir}/applications
@ -92,7 +93,8 @@ noinst_HEADERS = \
lot-viewer.h \
reconcile-list.h \
top-level.h \
window-reconcile.h
window-reconcile.h \
window-autoclear.h
if BUILDING_FROM_SVN
swig-gnome.c: gnome.i dialog-progress.h ${top_srcdir}/src/base-typemaps.i

View File

@ -2,6 +2,7 @@ gladedir = $(GNC_GLADE_DIR)
glade_DATA = \
account.glade \
acctperiod.glade \
autoclear.glade \
budget.glade \
chart-export.glade \
commodities.glade \

View File

@ -0,0 +1,135 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkDialog" id="Auto-clear Start Dialog">
<property name="visible">True</property>
<property name="resizable">False</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<widget class="GtkVBox" id="dialog-vbox6">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="spacing">6</property>
<child>
<widget class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;b&gt;Auto-Clear Information&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkTable" id="table1">
<property name="visible">True</property>
<property name="border_width">10</property>
<property name="n_rows">2</property>
<property name="n_columns">2</property>
<property name="column_spacing">4</property>
<property name="row_spacing">6</property>
<child>
<widget class="GtkLabel" id="end_label">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">_Ending Balance:</property>
<property name="use_underline">True</property>
</widget>
</child>
<child>
<widget class="GtkHBox" id="end_value_box">
<property name="visible">True</property>
<child>
<placeholder/>
</child>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="status_label">
<property name="visible">True</property>
<property name="label" translatable="yes"></property>
</widget>
<packing>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
</widget>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">5</property>
<property name="position">2</property>
</packing>
</child>
<child internal-child="action_area">
<widget class="GtkHButtonBox" id="dialog-action_area6">
<property name="visible">True</property>
<property name="layout_style">end</property>
<child>
<widget class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<property name="response_id">-6</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="ok_button">
<property name="label">gtk-ok</property>
<property name="response_id">-5</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

View File

@ -62,6 +62,7 @@
#include "gnc-ui-util.h"
#include "lot-viewer.h"
#include "window-reconcile.h"
#include "window-autoclear.h"
#include "window-main-summarybar.h"
/* This static indicates the debugging module that this .o belongs to. */
@ -130,6 +131,7 @@ static void gnc_plugin_page_account_tree_cmd_delete_account (GtkAction *action,
static void gnc_plugin_page_account_tree_cmd_renumber_accounts (GtkAction *action, GncPluginPageAccountTree *page);
static void gnc_plugin_page_account_tree_cmd_view_filter_by (GtkAction *action, GncPluginPageAccountTree *plugin_page);
static void gnc_plugin_page_account_tree_cmd_reconcile (GtkAction *action, GncPluginPageAccountTree *page);
static void gnc_plugin_page_account_tree_cmd_autoclear (GtkAction *action, GncPluginPageAccountTree *page);
static void gnc_plugin_page_account_tree_cmd_transfer (GtkAction *action, GncPluginPageAccountTree *page);
static void gnc_plugin_page_account_tree_cmd_stock_split (GtkAction *action, GncPluginPageAccountTree *page);
static void gnc_plugin_page_account_tree_cmd_lots (GtkAction *action, GncPluginPageAccountTree *page);
@ -178,6 +180,9 @@ static GtkActionEntry gnc_plugin_page_account_tree_actions [] = {
{ "ActionsReconcileAction", NULL, N_("_Reconcile..."), NULL,
N_("Reconcile the selected account"),
G_CALLBACK (gnc_plugin_page_account_tree_cmd_reconcile) },
{ "ActionsAutoClearAction", NULL, N_("_Auto-clear..."), NULL,
N_("Automatically clear individual transactions, given a cleared amount"),
G_CALLBACK (gnc_plugin_page_account_tree_cmd_autoclear) },
{ "ActionsTransferAction", NULL, N_("_Transfer..."), "<control>t",
N_("Transfer funds from one account to another"),
G_CALLBACK (gnc_plugin_page_account_tree_cmd_transfer) },
@ -210,6 +215,7 @@ static const gchar *actions_requiring_account[] = {
"EditEditAccountAction",
"EditDeleteAccountAction",
"ActionsReconcileAction",
"ActionsAutoClearAction",
"ActionsLotsAction",
NULL
};
@ -1157,6 +1163,22 @@ gnc_plugin_page_account_tree_cmd_reconcile (GtkAction *action,
gnc_ui_reconcile_window_raise (recnData);
}
static void
gnc_plugin_page_account_tree_cmd_autoclear (GtkAction *action,
GncPluginPageAccountTree *page)
{
GtkWidget *window;
Account *account;
AutoClearWindow *autoClearData;
account = gnc_plugin_page_account_tree_get_current_account (page);
g_return_if_fail (account != NULL);
window = GNC_PLUGIN_PAGE (page)->window;
autoClearData = autoClearWindow (window, account);
gnc_ui_autoclear_window_raise (autoClearData);
}
static void
gnc_plugin_page_account_tree_cmd_transfer (GtkAction *action,
GncPluginPageAccountTree *page)

View File

@ -73,6 +73,7 @@
#include "Scrub.h"
#include "QueryNew.h"
#include "window-reconcile.h"
#include "window-autoclear.h"
#include "window-report.h"
/* This static indicates the debugging module that this .o belongs to. */
@ -133,6 +134,7 @@ static void gnc_plugin_page_register_cmd_style_changed (GtkAction *action, GtkRa
static void gnc_plugin_page_register_cmd_style_double_line (GtkToggleAction *action, GncPluginPageRegister *plugin_page);
static void gnc_plugin_page_register_cmd_reconcile (GtkAction *action, GncPluginPageRegister *plugin_page);
static void gnc_plugin_page_register_cmd_autoclear (GtkAction *action, GncPluginPageRegister *plugin_page);
static void gnc_plugin_page_register_cmd_transfer (GtkAction *action, GncPluginPageRegister *plugin_page);
static void gnc_plugin_page_register_cmd_stock_split (GtkAction *action, GncPluginPageRegister *plugin_page);
static void gnc_plugin_page_register_cmd_lots (GtkAction *action, GncPluginPageRegister *plugin_page);
@ -241,6 +243,9 @@ static GtkActionEntry gnc_plugin_page_register_actions [] =
{ "ActionsReconcileAction", GTK_STOCK_INDEX, N_("_Reconcile..."), NULL,
N_("Reconcile the selected account"),
G_CALLBACK (gnc_plugin_page_register_cmd_reconcile) },
{ "ActionsAutoClearAction", GTK_STOCK_INDEX, N_("_Auto-clear..."), NULL,
N_("Automatically clear individual transactions, so as to reach a certain cleared amount"),
G_CALLBACK (gnc_plugin_page_register_cmd_autoclear) },
{ "ActionsStockSplitAction", NULL, N_("Stoc_k Split..."), NULL,
N_("Record a stock split or a stock merger"),
G_CALLBACK (gnc_plugin_page_register_cmd_stock_split) },
@ -315,6 +320,7 @@ static const gchar *important_actions[] = {
static const gchar *actions_requiring_account[] = {
"EditEditAccountAction",
"ActionsReconcileAction",
"ActionsAutoClearAction",
"ActionsLotsAction",
NULL
};
@ -340,6 +346,7 @@ static action_toolbar_labels toolbar_labels[] = {
{ "ScheduleTransactionAction", N_("Schedule") },
{ "BlankTransactionAction", N_("Blank") },
{ "ActionsReconcileAction", N_("Reconcile") },
{ "ActionsAutoClearAction", N_("Auto-clear") },
{ NULL, NULL },
};
@ -2435,6 +2442,26 @@ gnc_plugin_page_register_cmd_reconcile (GtkAction *action,
LEAVE(" ");
}
static void
gnc_plugin_page_register_cmd_autoclear (GtkAction *action,
GncPluginPageRegister *page)
{
Account *account;
GtkWindow *window;
AutoClearWindow * autoClearData;
ENTER("(action %p, plugin_page %p)", action, page);
g_return_if_fail(GNC_IS_PLUGIN_PAGE_REGISTER(page));
account = gnc_plugin_page_register_get_account (page);
window = gnc_window_get_gtk_window(GNC_WINDOW(GNC_PLUGIN_PAGE (page)->window));
autoClearData = autoClearWindow (GTK_WIDGET(window), account);
gnc_ui_autoclear_window_raise (autoClearData);
LEAVE(" ");
}
static void
gnc_plugin_page_register_cmd_stock_split (GtkAction *action,
GncPluginPageRegister *page)

View File

@ -26,6 +26,7 @@
<separator name="ActionsSep1"/>
<menuitem name="ActionsTransfer" action="ActionsTransferAction"/>
<menuitem name="ActionsReconcile" action="ActionsReconcileAction"/>
<menuitem name="ActionsAutoClear" action="ActionsAutoClearAction"/>
<menuitem name="ActionsStockSplit" action="ActionsStockSplitAction"/>
<menuitem name="ActionLots" action="ActionsLotsAction"/>
<separator name="ActionsSep2"/>
@ -45,6 +46,7 @@
<menuitem name="AccountEditAccount" action="EditEditAccountAction"/>
<separator name="AccountSep1"/>
<menuitem name="AccountReconcile" action="ActionsReconcileAction"/>
<menuitem name="AccountAutoClear" action="ActionsAutoClearAction"/>
<menuitem name="AccountTransfer" action="ActionsTransferAction"/>
<menuitem name="AccountStockSplit" action="ActionsStockSplitAction"/>
<menuitem name="AccountLots" action="ActionsLotsAction"/>

View File

@ -0,0 +1,322 @@
/********************************************************************\
* window-autoclear.c -- the autoclear window *
* Copyright (C) 2010 Cristian KLEIN *
* *
* 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 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\********************************************************************/
#include "config.h"
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include "glib-compat.h"
#include "Scrub.h"
#include "Scrub3.h"
#include "dialog-account.h"
#include "dialog-transfer.h"
#include "dialog-utils.h"
#include "gnc-amount-edit.h"
#include "gnc-component-manager.h"
#include "gnc-date-edit.h"
#include "gnc-event.h"
#include "gnc-gconf-utils.h"
#include "gnc-gnome-utils.h"
#include "gnc-main-window.h"
#include "gnc-plugin-page-register.h"
#include "gnc-ui.h"
#include "guile-util.h"
#include "window-autoclear.h"
#define WINDOW_AUTOCLEAR_CM_CLASS "window-autoclear"
/** STRUCTS *********************************************************/
struct _AutoClearWindow
{
Account *account; /* The account that we are auto-clearing */
gint component_id; /* id of component */
GtkWidget *window; /* The auto-clear window */
GNCAmountEdit *end_value;/* The ending value */
GtkWidget *ok_button;
GtkWidget *cancel_button;
GtkLabel *status_label;
};
/********************************************************************\
* gnc_ui_autoclear_window_raise *
* shows and raises an auto-clear window *
* *
* Args: autoClearData - the auto-clear window structure *
\********************************************************************/
void
gnc_ui_autoclear_window_raise(AutoClearWindow * autoClearData)
{
if (autoClearData == NULL)
return;
if (autoClearData->window == NULL)
return;
gtk_window_present(GTK_WINDOW(autoClearData->window));
}
static char *
gnc_autoclear_make_window_name(Account *account)
{
char *fullname;
char *title;
fullname = gnc_account_get_full_name(account);
title = g_strconcat(fullname, " - ", _("Auto-clear"), NULL);
g_free(fullname);
return title;
}
static gboolean
ght_gnc_numeric_equal(gconstpointer v1, gconstpointer v2)
{
gnc_numeric n1 = *(gnc_numeric *)v1, n2 = *(gnc_numeric *)v2;
return gnc_numeric_equal(n1, n2);
}
static guint
ght_gnc_numeric_hash(gconstpointer v1)
{
gnc_numeric n1 = *(gnc_numeric *)v1;
gdouble d1 = gnc_numeric_to_double(n1);
return g_double_hash(&d1);
}
static void
gnc_autoclear_window_ok_cb (GtkWidget *widget,
AutoClearWindow *data)
{
GList *node, *nc_list = 0, *toclear_list = 0;
gnc_numeric toclear_value;
GHashTable *sack;
gtk_label_set_text(data->status_label, "Searching for splits to clear ...");
/* Value we have to reach */
toclear_value = gnc_amount_edit_get_amount(data->end_value);
toclear_value = gnc_numeric_convert(toclear_value, xaccAccountGetCommoditySCU(data->account), GNC_RND_NEVER);
/* Extract which splits are not cleared and compute the amount we have to clear */
for (node = xaccAccountGetSplitList(data->account); node; node = node->next)
{
Split *split = (Split *)node->data;
char recn;
gnc_numeric value;
recn = xaccSplitGetReconcile (split);
value = xaccSplitGetAmount (split);
if (recn == NREC)
nc_list = g_list_append(nc_list, split);
else
toclear_value = gnc_numeric_sub_fixed(toclear_value, value);
}
/* Pretty print information */
printf("Amount to clear: %s\n", gnc_numeric_to_string(toclear_value));
printf("Available splits:\n");
for (node = nc_list; node; node = node->next)
{
Split *split = (Split *)node->data;
gnc_numeric value = xaccSplitGetAmount (split);
printf(" %s\n", gnc_numeric_to_string(value));
}
/* Run knapsack */
/* Entries in the hash table are:
* - key = amount to which we know how to clear (freed by GHashTable)
* - value = last split we used to clear this amount (not managed by GHashTable)
*/
printf("Knapsacking ...\n");
sack = g_hash_table_new_full (ght_gnc_numeric_hash, ght_gnc_numeric_equal, g_free, NULL);
for (node = nc_list; node; node = node->next)
{
Split *split = (Split *)node->data;
gnc_numeric split_value = xaccSplitGetAmount(split);
GHashTableIter iter;
gnc_numeric *key;
GList *reachable_list = 0, *node;
printf(" Split value: %s\n", gnc_numeric_to_string(split_value));
/* For each value in the sack */
g_hash_table_iter_init (&iter, sack);
while (g_hash_table_iter_next (&iter, (gpointer *)&key, NULL))
{
/* Compute a new reachable value */
gnc_numeric reachable_value = gnc_numeric_add_fixed(*key, split_value);
reachable_list = g_list_append(reachable_list, g_memdup(&reachable_value, sizeof(gnc_numeric)));
printf(" Sack: found %s, added %s\n", gnc_numeric_to_string(*key), gnc_numeric_to_string(reachable_value));
}
/* Add the value of the split itself to the reachable_list */
reachable_list = g_list_append(reachable_list, g_memdup(&split_value, sizeof(gnc_numeric)));
/* Add everything to the sack, looking out for duplicates */
for (node = reachable_list; node; node = node->next)
{
gnc_numeric *reachable_value = node->data;
Split *toinsert_split = split;
printf(" Reachable value: %s ", gnc_numeric_to_string(*reachable_value));
/* Check if it already exists */
if (g_hash_table_lookup_extended(sack, reachable_value, NULL, NULL))
{
/* If yes, we are in trouble, we reached an amount using two solutions */
toinsert_split = NULL;
printf("dup");
}
g_hash_table_insert (sack, reachable_value, toinsert_split);
printf("\n");
}
g_list_free(reachable_list);
}
/* Check solution */
printf("Rebuilding solution ...\n");
while (!gnc_numeric_zero_p(toclear_value))
{
Split *split;
printf(" Left to clear: %s\n", gnc_numeric_to_string(toclear_value));
if (g_hash_table_lookup_extended(sack, &toclear_value, NULL, (gpointer *)&split))
{
if (split != NULL)
{
toclear_list = g_list_prepend(toclear_list, split);
toclear_value = gnc_numeric_sub_fixed(toclear_value, xaccSplitGetAmount(split));
printf(" Cleared: %s -> %s\n", gnc_numeric_to_string(xaccSplitGetAmount(split)), gnc_numeric_to_string(toclear_value));
}
else
{
/* We couldn't reconstruct the solution */
printf(" Solution not unique.\n");
gtk_label_set_text(data->status_label, "Cannot uniquely clear splits. Found multiple possibilities.");
return;
}
}
else
{
printf(" No solution found.\n");
gtk_label_set_text(data->status_label, "The selected amount cannot be cleared.");
return;
}
}
g_hash_table_destroy (sack);
/* Show solution */
printf("Clearing splits:\n");
for (node = toclear_list; node; node = node->next)
{
Split *split = node->data;
char recn;
gnc_numeric value;
recn = xaccSplitGetReconcile (split);
value = xaccSplitGetAmount (split);
printf(" %c %s\n", recn, gnc_numeric_to_string(value));
xaccSplitSetReconcile (split, CREC);
}
if (toclear_list == 0)
printf(" None\n");
/* Free lists */
g_list_free(nc_list);
g_list_free(toclear_list);
/* Close window */
gtk_widget_destroy(data->window);
g_free(data);
}
static void
gnc_autoclear_window_cancel_cb (GtkWidget *widget,
AutoClearWindow *data)
{
/* Close window */
gtk_widget_destroy(data->window);
g_free(data);
}
/********************************************************************\
* autoClearWindow *
* opens up the window to auto-clear an account *
* *
* Args: parent - the parent of this window *
* account - the account to auto-clear *
* Return: autoClearData - the instance of this AutoClearWindow *
\********************************************************************/
AutoClearWindow *
autoClearWindow (GtkWidget *parent, Account *account)
{
GtkWidget *dialog, *box, *label, *end_value;
GladeXML *xml;
AutoClearWindow *data;
char *title;
data = g_new0 (AutoClearWindow, 1);
data->account = account;
/* Create the dialog box */
xml = gnc_glade_xml_new ("autoclear.glade", "Auto-clear Start Dialog");
dialog = glade_xml_get_widget (xml, "Auto-clear Start Dialog");
title = gnc_autoclear_make_window_name (account);
gtk_window_set_title(GTK_WINDOW(dialog), title);
g_free (title);
/* Add amount edit box */
end_value = gnc_amount_edit_new();
data->end_value = GNC_AMOUNT_EDIT(end_value);
box = glade_xml_get_widget(xml, "end_value_box");
gtk_box_pack_start(GTK_BOX(box), end_value, TRUE, TRUE, 0);
label = glade_xml_get_widget(xml, "end_label");
gtk_label_set_mnemonic_widget(GTK_LABEL(label), end_value);
gtk_widget_grab_focus(GTK_WIDGET(end_value));
data->window = GTK_WIDGET(dialog);
if (parent != NULL)
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent));
data->ok_button = glade_xml_get_widget(xml, "ok_button");
data->cancel_button = glade_xml_get_widget(xml, "cancel_button");
data->status_label = GTK_LABEL(glade_xml_get_widget(xml, "status_label"));
g_signal_connect(data->ok_button, "clicked",
G_CALLBACK(gnc_autoclear_window_ok_cb), data);
g_signal_connect(data->end_value, "activate",
G_CALLBACK(gnc_autoclear_window_ok_cb), data);
g_signal_connect(data->cancel_button, "clicked",
G_CALLBACK(gnc_autoclear_window_cancel_cb), data);
return data;
}

View File

@ -0,0 +1,31 @@
/********************************************************************\
* window-autoclear.h -- the autoclear window *
* Copyright (C) 2010 Cristian KLEIN *
* *
* 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 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\********************************************************************/
#ifndef WINDOW_AUTOCLEAR_H
#define WINDOW_AUTOCLEAR_H
typedef struct _AutoClearWindow AutoClearWindow;
AutoClearWindow *autoClearWindow (gncUIWidget parent, Account *account);
void gnc_ui_autoclear_window_raise(AutoClearWindow * autoClearData);
#endif