diff --git a/gnucash/gnome/CMakeLists.txt b/gnucash/gnome/CMakeLists.txt index f5e2481a95..d3c22917f9 100644 --- a/gnucash/gnome/CMakeLists.txt +++ b/gnucash/gnome/CMakeLists.txt @@ -4,6 +4,7 @@ set (gnc_gnome_noinst_HEADERS assistant-hierarchy.h assistant-loan.h assistant-stock-split.h + assistant-stock-transaction.h business-options-gnome.h business-urls.h business-gnome-utils.h @@ -72,6 +73,7 @@ set (gnc_gnome_SOURCES assistant-hierarchy.c assistant-loan.cpp assistant-stock-split.c + assistant-stock-transaction.cpp business-options-gnome.c business-urls.c business-gnome-utils.c diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp new file mode 100644 index 0000000000..3fbfc8aa93 --- /dev/null +++ b/gnucash/gnome/assistant-stock-transaction.cpp @@ -0,0 +1,1225 @@ +/********************************************************************\ + * assistant-stock-transaction.cpp -- stock assistant for GnuCash * + * Copyright (C) 2022 Christopher Lam * + * * + * 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 + +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "Transaction.h" +#include "engine-helpers.h" +#include "dialog-utils.h" +#include "assistant-stock-transaction.h" +#include "gnc-account-sel.h" +#include "gnc-amount-edit.h" +#include "gnc-component-manager.h" +#include "gnc-date-edit.h" +#include "gnc-tree-view-account.h" + +static QofLogModule log_module = GNC_MOD_ASSISTANT; + +void stock_assistant_prepare (GtkAssistant *assistant, GtkWidget *page, + gpointer user_data); +void stock_assistant_finish (GtkAssistant *assistant, gpointer user_data); +void stock_assistant_cancel (GtkAssistant *gtkassistant, gpointer user_data); +} + +enum class FieldMask : unsigned; +bool operator &(FieldMask lhs, FieldMask rhs); +FieldMask operator |(FieldMask lhs, FieldMask rhs); +FieldMask operator ^(FieldMask lhs, FieldMask rhs); + +static const char* ASSISTANT_STOCK_TRANSACTION_CM_CLASS = "assistant-stock-transaction"; + +enum assistant_pages +{ + PAGE_INTRO = 0, + PAGE_TRANSACTION_TYPE, + PAGE_TRANSATION_DETAILS, + PAGE_STOCK_AMOUNT, + PAGE_STOCK_VALUE, + PAGE_CASH, + PAGE_FEES, + PAGE_DIVIDEND, + PAGE_CAPGAINS, + PAGE_FINISH +}; + +enum split_cols +{ + SPLIT_COL_ACCOUNT = 0, + SPLIT_COL_MEMO, + SPLIT_COL_DEBIT, + SPLIT_COL_CREDIT, + NUM_SPLIT_COLS +}; + +/** structures *********************************************************/ + +enum class FieldMask : unsigned +{ + DISABLED = 0, + ENABLED_DEBIT, + ENABLED_CREDIT, + ALLOW_ZERO = 4, + ALLOW_NEGATIVE = 8 +}; + +FieldMask operator |(FieldMask lhs, FieldMask rhs) +{ + return static_cast (static_cast(lhs) | + static_cast(rhs)); +}; + +bool operator &(FieldMask lhs, FieldMask rhs) +{ + return (static_cast(lhs) & static_cast(rhs)); +}; + +FieldMask operator ^(FieldMask lhs, FieldMask rhs) +{ + return static_cast (static_cast(lhs) ^ + static_cast(rhs)); +}; + +struct TxnTypeInfo +{ + FieldMask stock_amount; + FieldMask stock_value; + FieldMask cash_value; + FieldMask fees_value; + bool fees_capitalize; + FieldMask dividend_value; + FieldMask capgains_value; + const char* friendly_name; + const char* explanation; +}; + +using StringVec = std::vector; +using TxnTypeVec = std::vector; +using AccountVec = std::vector; + +static const TxnTypeVec starting_types +{ + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::ENABLED_CREDIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing an + // Initial stock purchase + NC_("Stock Assistant: Transaction Type", "Open buy"), + N_("Initial stock purchase") + }, + { + FieldMask::ENABLED_CREDIT, // stock_amt + FieldMask::ENABLED_CREDIT, // stock_val + FieldMask::ENABLED_DEBIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing an + // initial stock short-sale + NC_("Stock Assistant: Transaction Type", "Open short"), + N_("Initial stock short-sale") + } +}; + +static const TxnTypeVec long_types +{ + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::ENABLED_CREDIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // purchase of stock. + NC_("Stock Assistant: Transaction Type", "Buy"), + N_("Buying stock.") + }, + { + FieldMask::ENABLED_CREDIT, // stock_amt + FieldMask::ENABLED_CREDIT, // stock_val + FieldMask::ENABLED_DEBIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE, // capgains_amt + // Translators: this is a stock transaction describing sale of + // stock, and recording capital gains/loss + NC_("Stock Assistant: Transaction Type", "Sell"), + N_("Selling stock, and record capital gains/loss") + }, + { + FieldMask::DISABLED, // stock_amt + FieldMask::DISABLED, // stock_val + FieldMask::ENABLED_DEBIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::ENABLED_CREDIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // dividends issued to holder + NC_("Stock Assistant: Transaction Type", "Dividend"), + N_("Company issues dividends to holder") + }, + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::ENABLED_DEBIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::ENABLED_CREDIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // dividend issued to holder, and is reinvested. Some + // dividends are distributed as cash. + NC_("Stock Assistant: Transaction Type", "Dividend reinvestment (w/ remainder)"), + N_("Company issues dividend which is reinvested. Some dividends are paid to holder") + }, + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::ENABLED_CREDIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // dividend which is wholly reinvested. + NC_("Stock Assistant: Transaction Type", "Dividend reinvestment (w/o remainder)"), + N_("Company issues dividend which is wholly reinvested.") + }, + { + FieldMask::DISABLED, // stock_amt + FieldMask::ENABLED_CREDIT, // stock_val + FieldMask::ENABLED_DEBIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing return + // of capital + NC_("Stock Assistant: Transaction Type", "Return of Capital"), + N_("Stock returns capital to holder") + }, + { + FieldMask::DISABLED, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::ENABLED_CREDIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing a + // notional distribution + NC_("Stock Assistant: Transaction Type", "Notional distribution"), + N_("Stock returns a notional distribution") + }, + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::DISABLED, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing a stock + // split + NC_("Stock Assistant: Transaction Type", "Stock split"), + N_("Stock price is fractionally reduced") + }, + { + FieldMask::ENABLED_CREDIT, // stock_amt + FieldMask::DISABLED, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing a reverse split + NC_("Stock Assistant: Transaction Type", "Reverse split"), + N_("Stock price is fractionally increased.") + }, + { + FieldMask::ENABLED_CREDIT, // stock_amt + FieldMask::ENABLED_CREDIT, // stock_val + FieldMask::ENABLED_DEBIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE, // capgains_amt + // Translators: this is a stock transaction describing a + // reverse split. Some fractional stock is returned as cash. + NC_("Stock Assistant: Transaction Type", "Reverse split w/ cash in lieu for fractionals"), + N_("Stock price is fractionally increased. Fractional remaining stock is returned as cash.") + }, +}; + +static const TxnTypeVec short_types +{ + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::ENABLED_CREDIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // shorting of stock. + NC_("Stock Assistant: Transaction Type", "Short sell"), + N_("Shorting stock.") + }, + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::ENABLED_CREDIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE, // capg_amt + // Translators: this is a stock transaction describing cover + // buying stock, and recording capital gains/loss + NC_("Stock Assistant: Transaction Type", "Cover buy"), + N_("Cover buying stock, and record capital gains/loss") + }, + { + FieldMask::DISABLED, // stock_amt + FieldMask::DISABLED, // stock_val + FieldMask::ENABLED_CREDIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::ENABLED_DEBIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // dividends retrieved from holder when shorting stock + NC_("Stock Assistant: Transaction Type", "Compensatory Dividend"), + N_("Company issues dividends to holder when shorting stock") + }, + { + FieldMask::ENABLED_CREDIT, // stock_amt + FieldMask::ENABLED_CREDIT, // stock_val + FieldMask::ENABLED_DEBIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::ENABLED_DEBIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // dividends retrieved from holder when shorting stock. Some + // dividends are recovered from holder + NC_("Stock Assistant: Transaction Type", "Dividend reinvestment (w/ remainder)"), + N_("Company issues dividends to holder when shorting stock. Some dividends are recovered from holder") + }, + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::ENABLED_DEBIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing + // dividends retrieved from holder when shorting stock, + NC_("Stock Assistant: Transaction Type", "Dividend reinvestment (w/o remainder)"), + N_("Company issues dividend when shorting stock, which are wholly recovered from holder.") + }, + { + FieldMask::DISABLED, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::ENABLED_CREDIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing return + // of capital retrieved from holder when shorting stock + NC_("Stock Assistant: Transaction Type", "Compensatory Return of Capital"), + N_("Return retrieves capital from holder when shorting stock") + }, + { + FieldMask::DISABLED, // stock_amt + FieldMask::ENABLED_CREDIT, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::ENABLED_DEBIT, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing a + // notional distribution when shorting stock + NC_("Stock Assistant: Transaction Type", "Compensatory Notional distribution"), + N_("Stock retrieves a notional distribution") + }, + { + FieldMask::ENABLED_CREDIT, // stock_amt + FieldMask::DISABLED, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing a stock + // split when shorting stock + NC_("Stock Assistant: Transaction Type", "Stock split"), + N_("Stock price is fractionally reduced when shorting stock") + }, + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::DISABLED, // stock_val + FieldMask::DISABLED, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + true, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::DISABLED, // capg_amt + // Translators: this is a stock transaction describing a + // reverse split when shorting stock. + NC_("Stock Assistant: Transaction Type", "Reverse split"), + N_("Stock price is fractionally increased when shorting stock.") + }, + { + FieldMask::ENABLED_DEBIT, // stock_amt + FieldMask::ENABLED_DEBIT, // stock_val + FieldMask::ENABLED_CREDIT, // cash_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt + false, // fees_capitalize + FieldMask::DISABLED, // dividend_amt + FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE, // capg_amt + // Translators: this is a stock transaction describing a + // reverse split when shorting stock. Fractional remaining + // stock is retrieved as cash. + NC_("Stock Assistant: Transaction Type", "Reverse split w/ cash in lieu for fractionals"), + N_("Stock price is fractionally increased when shorting stock. Fractional remaining stock is retrieved as cash.") + }, +}; + +typedef struct +{ + GtkWidget * window; + GtkWidget * assistant; + + const TxnTypeVec * txn_types; + Account * acct; + gnc_commodity * currency; + + // transaction type page + GtkWidget * transaction_type_page; + GtkWidget * transaction_type_combo; + GtkWidget * transaction_type_explanation; + const TxnTypeInfo * txn_type; + + // transaction details page + GtkWidget * transaction_details_page; + GtkWidget * date_edit; + GtkWidget * transaction_description_entry; + + // stock amount page + gnc_numeric balance_at_date; + GtkWidget * stock_amount_page; + GtkWidget * prev_amount; + GtkWidget * next_amount; + GtkWidget * stock_amount_edit; + + // stock value page + GtkWidget * stock_value_page; + GtkWidget * stock_value_edit; + GtkWidget * price_value; + GtkWidget * stock_memo_edit; + + // cash page + GtkWidget * cash_page; + GtkWidget * cash_account; + GtkWidget * cash_memo_edit; + GtkWidget * cash_value; + + // fees page + GtkWidget * fees_page; + GtkWidget * capitalize_fees_checkbox; + GtkWidget * fees_account; + GtkWidget * fees_memo_edit; + GtkWidget * fees_value; + + // dividend page + GtkWidget * dividend_page; + GtkWidget * dividend_account; + GtkWidget * dividend_memo_edit; + GtkWidget * dividend_value; + + // capgains page + GtkWidget * capgains_page; + GtkWidget * capgains_account; + GtkWidget * capgains_memo_edit; + GtkWidget * capgains_value; + + // finish page + GtkWidget * finish_page; + GtkWidget * finish_split_view; + GtkWidget * finish_summary; +} StockTransactionInfo; + + +/******* implementations ***********************************************/ +static void +stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data) +{ + auto info = static_cast(user_data); + gnc_unregister_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info); + g_free (info); +} + +static void +refresh_page_transaction_type (GtkWidget *widget, gpointer user_data) +{ + auto info = static_cast(user_data); + + auto type_idx = gtk_combo_box_get_active (GTK_COMBO_BOX (widget)); + if (type_idx < 0) // combo isn't initialized yet. + return; + + info->txn_type = &(info->txn_types->at (type_idx)); + + gtk_label_set_text (GTK_LABEL (info->transaction_type_explanation), + _(info->txn_type->explanation)); + + // set default capitalize fees setting + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (info->capitalize_fees_checkbox), + info->txn_type->fees_capitalize); +} + + +static void +refresh_page_stock_amount (GtkWidget *widget, gpointer user_data) +{ + auto info = static_cast(user_data); + + auto pinfo = gnc_commodity_print_info (xaccAccountGetCommodity (info->acct), true); + auto bal = info->balance_at_date; + gtk_label_set_text (GTK_LABEL(info->prev_amount), xaccPrintAmount (bal, pinfo)); + + gnc_numeric stock_amount; + + if (gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT (info->stock_amount_edit), + &stock_amount, true, nullptr)) + gtk_label_set_text (GTK_LABEL(info->next_amount), nullptr); + else + { + if (info->txn_type->stock_amount == FieldMask::ENABLED_CREDIT) + stock_amount = gnc_numeric_neg (stock_amount); + bal = gnc_numeric_add_fixed (bal, stock_amount); + + gtk_label_set_text (GTK_LABEL(info->next_amount), + xaccPrintAmount (bal, pinfo)); + } +} + + +static void +refresh_page_stock_value (GtkWidget *widget, gpointer user_data) +{ + auto info = static_cast(user_data); + gnc_numeric amount, value; + + if (info->txn_type->stock_amount == FieldMask::DISABLED || + info->txn_type->stock_value == FieldMask::DISABLED || + gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT (info->stock_amount_edit), &amount, true, nullptr) || + gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT (info->stock_value_edit), &value, true, nullptr) || + gnc_numeric_zero_p (value)) + { + // Translators: StockAssistant: N/A denotes stock price is not computable + const char* na_label = N_("N/A"); + gtk_label_set_text (GTK_LABEL (info->price_value), _(na_label)); + return; + } + + auto price = gnc_numeric_div (value, amount, GNC_DENOM_AUTO, GNC_HOW_RND_ROUND); + auto pinfo = gnc_commodity_print_info (info->currency, true); + gtk_label_set_text (GTK_LABEL (info->price_value), xaccPrintAmount (price, pinfo)); +} + +static void +refresh_page_cash (GtkWidget *widget, gpointer user_data) +{ + return; +} + +static void +refresh_page_fees (GtkWidget *widget, gpointer user_data) +{ + auto info = static_cast(user_data); + auto capitalize_fees = gtk_toggle_button_get_active + (GTK_TOGGLE_BUTTON (info->capitalize_fees_checkbox)); + gtk_widget_set_sensitive (info->fees_account, !capitalize_fees); +} + +static void +refresh_page_dividend (GtkWidget *widget, gpointer user_data) +{ + return; +} + +static void +refresh_page_capgains (GtkWidget *widget, gpointer user_data) +{ + return; +} + +static void +add_error (StringVec& errors, const char* format_str, const char* arg) +{ + gchar *buf = g_strdup_printf (_(format_str), + g_dpgettext2 (nullptr, "Stock Assistant: Page name", arg)); + errors.emplace_back (buf); + g_free (buf); +} + +static void +add_error_str (StringVec& errors, const char* str) +{ + errors.emplace_back (_(str)); +} + +static void +check_page (GtkListStore *list, gnc_numeric& debit, gnc_numeric& credit, + FieldMask splitfield, Account *acct, GtkWidget *memo, GtkWidget *gae, + gnc_commodity *comm, bool ignore_account, + const char* page, StringVec& errors) +{ + if (splitfield == FieldMask::DISABLED) + return; + const char* missing_str = N_("(missing)"); + const gchar* amtstr; + gnc_numeric amount; + bool debit_side = (splitfield & FieldMask::ENABLED_DEBIT); + + if (gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT (gae), &amount, true, nullptr)) + { + if (splitfield & FieldMask::ALLOW_ZERO) + amtstr = ""; + else + { + add_error (errors, N_("Amount for %s is missing"), page); + amtstr = _(missing_str); + } + } + else + { + if (!(splitfield & FieldMask::ALLOW_NEGATIVE)) + { + if ((splitfield & FieldMask::ALLOW_ZERO) && gnc_numeric_negative_p (amount)) + add_error (errors, N_("Amount for %s must not be negative."), page); + else if (!(splitfield & FieldMask::ALLOW_ZERO) && !gnc_numeric_positive_p (amount)) + add_error (errors, N_("Amount for %s must be positive."), page); + } + if (gnc_numeric_negative_p (amount)) + { + amount = gnc_numeric_neg (amount); + debit_side = !debit_side; + } + if (debit_side) + debit = gnc_numeric_add_fixed (debit, amount); + else + credit = gnc_numeric_add_fixed (credit, amount); + amtstr = xaccPrintAmount (amount, gnc_commodity_print_info (comm, true)); + } + + auto memostr = gtk_entry_get_text (GTK_ENTRY (memo)); + const gchar *acctstr; + + if (ignore_account) + acctstr = ""; + else if (acct) + acctstr = xaccAccountGetName (acct); + else + { + add_error (errors, N_("Account for %s is missing"), page); + acctstr = _(missing_str); + } + + GtkTreeIter iter; + gtk_list_store_append (list, &iter); + gtk_list_store_set (list, &iter, + SPLIT_COL_ACCOUNT, acctstr, + SPLIT_COL_MEMO, memostr, + SPLIT_COL_DEBIT, debit_side ? amtstr : "", + SPLIT_COL_CREDIT, !debit_side ? amtstr : "", + -1); +} + +static inline Account* +gas_account (GtkWidget *gas) +{ + return gnc_account_sel_get_account (GNC_ACCOUNT_SEL (gas)); +} + +static void +refresh_page_finish (StockTransactionInfo *info) +{ + auto view = GTK_TREE_VIEW (info->finish_split_view); + auto list = GTK_LIST_STORE (gtk_tree_view_get_model(view)); + gtk_list_store_clear (list); + + gnc_numeric debit = gnc_numeric_zero (); + gnc_numeric credit = gnc_numeric_zero (); + StringVec errors; + + if (info->txn_type->stock_amount != FieldMask::DISABLED) + { + auto stock_amount = gnc_amount_edit_get_amount + (GNC_AMOUNT_EDIT(info->stock_amount_edit)); + if (info->txn_type->stock_amount & FieldMask::ENABLED_CREDIT) + stock_amount = gnc_numeric_neg (stock_amount); + auto new_bal = gnc_numeric_add_fixed (info->balance_at_date, stock_amount); + if (gnc_numeric_positive_p (info->balance_at_date) && + gnc_numeric_negative_p (new_bal)) + add_error_str (errors, N_("Cannot sell more units than owned")); + else if (gnc_numeric_negative_p (info->balance_at_date) && + gnc_numeric_positive_p (new_bal)) + add_error_str (errors, N_("Cannot cover buy more units than owed")); + } + + check_page (list, debit, credit, info->txn_type->stock_value, info->acct, + info->stock_memo_edit, info->stock_value_edit, info->currency, + false, NC_ ("Stock Assistant: Page name", "stock value"), errors); + + check_page (list, debit, credit, info->txn_type->cash_value, + gas_account (info->cash_account), info->cash_memo_edit, + info->cash_value, info->currency, false, + NC_ ("Stock Assistant: Page name", "cash"), errors); + + check_page (list, debit, credit, info->txn_type->fees_value, + gas_account (info->fees_account), info->fees_memo_edit, + info->fees_value, info->currency, + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON + (info->capitalize_fees_checkbox)), + NC_ ("Stock Assistant: Page name", "fees"), errors); + + check_page (list, debit, credit, info->txn_type->dividend_value, + gas_account (info->dividend_account), + info->dividend_memo_edit, info->dividend_value, info->currency, + false, NC_ ("Stock Assistant: Page name", "dividend"), errors); + + // the next two checks will involve the two capgains splits: + // income side and stock side. The capgains_value ^ + // (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT) will swap the debit/credit + // flags. + if (info->txn_type->capgains_value != FieldMask::DISABLED) + { + check_page (list, debit, credit, info->txn_type->capgains_value, + gas_account (info->capgains_account), + info->capgains_memo_edit, info->capgains_value, info->currency, + false, NC_ ("Stock Assistant: Page name", "capital gains"), errors); + + check_page (list, debit, credit, + info->txn_type->capgains_value ^ (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT), + info->acct, info->capgains_memo_edit, info->capgains_value, + info->currency, false, + NC_ ("Stock Assistant: Page name", "capital gains"), errors); + } + + if (!gnc_numeric_equal (debit, credit)) + add_error_str (errors, N_("Debits and credits are not balanced")); + + if (errors.empty()) + { + auto success_msg = N_("No errors found. Click Apply to create transaction."); + gtk_assistant_set_page_complete (GTK_ASSISTANT (info->window), + info->finish_page, true); + gtk_label_set_text (GTK_LABEL (info->finish_summary), _(success_msg)); + } + else + { + auto header_str = N_("The following errors must be fixed:"); + auto header = std::string { _(header_str) }; + auto fold = [](auto a, auto b) { return std::move(a) + '\n' + std::move(b); }; + auto errmsg { std::accumulate (errors.begin(), errors.end(), header, fold) }; + gtk_assistant_set_page_complete (GTK_ASSISTANT (info->window), + info->finish_page, false); + gtk_label_set_text (GTK_LABEL (info->finish_summary), errmsg.c_str()); + } +} + +void +stock_assistant_prepare (GtkAssistant *assistant, GtkWidget *page, + gpointer user_data) +{ + auto info = static_cast(user_data); + gint currentpage = gtk_assistant_get_current_page(assistant); + + switch (currentpage) + { + case PAGE_TRANSACTION_TYPE: + // initialize transaction types. + gtk_combo_box_text_remove_all (GTK_COMBO_BOX_TEXT (info->transaction_type_combo)); + for (auto& it : *(info->txn_types)) + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (info->transaction_type_combo), + g_dpgettext2 (nullptr, "Stock Assistant: Transaction Type", + it.friendly_name)); + gtk_combo_box_set_active (GTK_COMBO_BOX (info->transaction_type_combo), 0); + refresh_page_transaction_type (info->transaction_type_combo, info); + gtk_widget_grab_focus (info->transaction_type_combo); + break; + case PAGE_STOCK_AMOUNT: + info->balance_at_date = xaccAccountGetBalanceAsOfDate + (info->acct, gnc_date_edit_get_date_end (GNC_DATE_EDIT (info->date_edit))); + refresh_page_stock_amount (info->stock_amount_edit, info); + // fixme: the following doesn't work??? + gtk_widget_grab_focus (gnc_amount_edit_gtk_entry + (GNC_AMOUNT_EDIT (info->stock_amount_edit))); + break; + case PAGE_STOCK_VALUE: + refresh_page_stock_value (info->stock_value_edit, info); + // fixme: ditto + gtk_widget_grab_focus (gnc_amount_edit_gtk_entry + (GNC_AMOUNT_EDIT (info->stock_value_edit))); + break; + case PAGE_CASH: + refresh_page_cash (info->cash_value, info); + break; + case PAGE_FEES: + refresh_page_fees (info->fees_value, info); + break; + case PAGE_DIVIDEND: + refresh_page_dividend (info->fees_value, info); + break; + case PAGE_CAPGAINS: + refresh_page_capgains (info->capgains_value, info); + break; + case PAGE_FINISH: + refresh_page_finish (info); + break; + } +} + +static gint +forward_page_func (gint current_page, gpointer user_data) +{ + auto info = static_cast(user_data); + auto& txn_type = info->txn_type; + + current_page++; + if (!txn_type) + return current_page; + + if (txn_type->stock_amount == FieldMask::DISABLED && current_page == PAGE_STOCK_AMOUNT) + current_page++; + if (txn_type->stock_value == FieldMask::DISABLED && current_page == PAGE_STOCK_VALUE) + current_page++; + if (txn_type->cash_value == FieldMask::DISABLED && current_page == PAGE_CASH) + current_page++; + if (txn_type->fees_value == FieldMask::DISABLED && current_page == PAGE_FEES) + current_page++; + if (txn_type->dividend_value == FieldMask::DISABLED && current_page == PAGE_DIVIDEND) + current_page++; + if (txn_type->capgains_value == FieldMask::DISABLED && current_page == PAGE_CAPGAINS) + current_page++; + + return current_page; +} + +static void +create_split (Transaction *trans, FieldMask splitfield, + const gchar *action, Account *account, + AccountVec& account_commits, GtkWidget *memo_entry, + GtkWidget *amount, GtkWidget *value, + bool skip_if_zero) +{ + auto amount_numeric = amount ? gnc_amount_edit_get_amount (GNC_AMOUNT_EDIT (amount)) : gnc_numeric_zero (); + auto value_numeric = value ? gnc_amount_edit_get_amount (GNC_AMOUNT_EDIT (value)) : gnc_numeric_zero (); + + if (skip_if_zero && gnc_numeric_zero_p (value_numeric)) + return; + + if (splitfield & FieldMask::ENABLED_CREDIT) + { + amount_numeric = gnc_numeric_neg (amount_numeric); + value_numeric = gnc_numeric_neg (value_numeric); + } + + auto split = xaccMallocSplit (gnc_get_current_book ()); + xaccSplitSetParent (split, trans); + xaccAccountBeginEdit (account); + account_commits.emplace_back (account); + xaccSplitSetAccount (split, account); + xaccSplitSetMemo (split, gtk_entry_get_text (GTK_ENTRY (memo_entry))); + xaccSplitSetValue (split, value_numeric); + xaccSplitSetAmount (split, amount_numeric); + DEBUG ("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)", + action, xaccAccountGetName (account), + gnc_num_dbg_to_string (value_numeric), + gnc_num_dbg_to_string (amount_numeric), + gnc_num_dbg_to_string (xaccSplitGetValue (split)), + gnc_num_dbg_to_string (xaccSplitGetAmount (split))); + gnc_set_num_action (nullptr, split, + nullptr, g_dpgettext2 (nullptr, "Stock Assistant: Action field", action)); +} + +static void +add_price (GtkWidget *amount, GtkWidget *value, + gnc_commodity *commodity, gnc_commodity *currency, time64 date) +{ + auto amt = gnc_amount_edit_get_amount (GNC_AMOUNT_EDIT (amount)); + auto val = gnc_amount_edit_get_amount (GNC_AMOUNT_EDIT (value)); + auto p = gnc_numeric_div (val, amt, GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT); + auto price = gnc_price_create (gnc_get_current_book ()); + + gnc_price_begin_edit (price); + gnc_price_set_commodity (price, commodity); + gnc_price_set_currency (price, currency); + gnc_price_set_time64 (price, date); + gnc_price_set_source (price, PRICE_SOURCE_STOCK_TRANSACTION); + gnc_price_set_typestr (price, PRICE_TYPE_UNK); + gnc_price_set_value (price, p); + gnc_price_commit_edit (price); + + auto book = gnc_get_current_book (); + auto pdb = gnc_pricedb_get_db (book); + + if (!gnc_pricedb_add_price (pdb, price)) + PWARN ("error adding price"); + + gnc_price_unref (price); +} + +void +stock_assistant_finish (GtkAssistant *assistant, gpointer user_data) +{ + auto info = static_cast(user_data); + AccountVec account_commits; + auto book = gnc_get_current_book (); + + gnc_suspend_gui_refresh (); + + auto trans = xaccMallocTransaction (book); + xaccTransBeginEdit (trans); + xaccTransSetCurrency (trans, info->currency); + xaccTransSetDescription (trans, gtk_entry_get_text + (GTK_ENTRY (info->transaction_description_entry))); + + auto date = gnc_date_edit_get_date (GNC_DATE_EDIT (info->date_edit)); + xaccTransSetDatePostedSecsNormalized (trans, date); + + if (info->txn_type->stock_amount != FieldMask::DISABLED || + info->txn_type->stock_value != FieldMask::DISABLED) + create_split (trans, info->txn_type->stock_value, + NC_ ("Stock Assistant: Action field", "Stock"), info->acct, + account_commits, info->stock_memo_edit, info->stock_amount_edit, + info->stock_value_edit, false); + + if (info->txn_type->cash_value != FieldMask::DISABLED) + create_split (trans, info->txn_type->cash_value, + NC_ ("Stock Assistant: Action field", "Cash"), + gas_account (info->cash_account), + account_commits, info->cash_memo_edit, info->cash_value, + info->cash_value, false); + + if (info->txn_type->fees_value != FieldMask::DISABLED) + { + auto capitalize = gtk_toggle_button_get_active + (GTK_TOGGLE_BUTTON (info->capitalize_fees_checkbox)); + create_split (trans, info->txn_type->fees_value, + NC_ ("Stock Assistant: Action field", "Fees"), + capitalize ? info->acct : gas_account (info->fees_account), + account_commits, info->fees_memo_edit, + capitalize ? nullptr : info->fees_value, + info->fees_value, true); + } + + if (info->txn_type->dividend_value != FieldMask::DISABLED) + create_split (trans, info->txn_type->dividend_value, + NC_ ("Stock Assistant: Action field", "Dividend"), + gas_account (info->dividend_account), + account_commits, info->dividend_memo_edit, + info->dividend_value, info->dividend_value, false); + + if (info->txn_type->capgains_value != FieldMask::DISABLED) + { + create_split (trans, info->txn_type->capgains_value, + NC_ ("Stock Assistant: Action field", "Capital Gains"), + gas_account (info->capgains_account), + account_commits, info->capgains_memo_edit, + info->capgains_value, info->capgains_value, false); + + create_split (trans, + info->txn_type->capgains_value ^ (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT), + NC_ ("Stock Assistant: Action field", "Capital Gains"), + info->acct, account_commits, info->capgains_memo_edit, + nullptr, info->capgains_value, false); + } + + if (info->txn_type->stock_amount != FieldMask::DISABLED && + info->txn_type->stock_value != FieldMask::DISABLED) + add_price (info->stock_amount_edit, info->stock_value_edit, + xaccAccountGetCommodity (info->acct), info->currency, date); + + xaccTransCommitEdit (trans); + + std::for_each (account_commits.begin(), account_commits.end(), xaccAccountCommitEdit); + + gnc_resume_gui_refresh (); + + gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info); +} + + +void +stock_assistant_cancel (GtkAssistant *assistant, gpointer user_data) +{ + auto info = static_cast(user_data); + gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info); +} + +static GtkWidget* +get_widget (GtkBuilder *builder, const gchar * ID) +{ + g_return_val_if_fail (builder && ID, nullptr); + auto obj = gtk_builder_get_object (builder, ID); + if (!obj) + PWARN ("get_widget ID '%s' not found. it may be a typo?", ID); + return GTK_WIDGET (obj); +} + +static GtkWidget* +create_gas (GtkBuilder *builder, gint row, + std::vector type, gnc_commodity *currency, + const gchar *table_ID, const gchar *label_ID) +{ + auto table = get_widget (builder, table_ID); + auto label = get_widget (builder, label_ID); + auto gas = gnc_account_sel_new (); + GList *acct_list = nullptr; + for (auto& it : type) + acct_list = g_list_prepend (acct_list, (gpointer)it); + auto curr_list = g_list_prepend (nullptr, currency); + gnc_account_sel_set_new_account_ability (GNC_ACCOUNT_SEL (gas), true); + gnc_account_sel_set_acct_filters (GNC_ACCOUNT_SEL (gas), acct_list, curr_list); + gtk_widget_show (gas); + gtk_grid_attach (GTK_GRID(table), gas, 1, row, 1, 1); + gtk_label_set_mnemonic_widget (GTK_LABEL(label), gas); + g_list_free (acct_list); + g_list_free (curr_list); + return gas; +} + +static GtkWidget* +create_gae (GtkBuilder *builder, gint row, gnc_commodity *comm, + const gchar *table_ID, const gchar *label_ID) +{ + // shares amount + auto table = get_widget (builder, table_ID); + auto label = get_widget (builder, label_ID); + auto info = gnc_commodity_print_info (comm, true); + auto gae = gnc_amount_edit_new (); + gnc_amount_edit_set_evaluate_on_enter (GNC_AMOUNT_EDIT (gae), TRUE); + gnc_amount_edit_set_print_info (GNC_AMOUNT_EDIT (gae), info); + gtk_grid_attach (GTK_GRID(table), gae, 1, row, 1, 1); + gtk_widget_show (gae); + gnc_amount_edit_make_mnemonic_target (GNC_AMOUNT_EDIT (gae), label); + return gae; +} + +static GtkWidget* +create_date (GtkBuilder *builder, guint row, + const gchar *date_label, const gchar *table_label) +{ + auto date = gnc_date_edit_new (gnc_time (NULL), FALSE, FALSE); + auto label = get_widget (builder, date_label); + gtk_grid_attach (GTK_GRID(get_widget (builder, table_label)), date, 1, row, 1, 1); + gtk_widget_show (date); + gnc_date_make_mnemonic_target (GNC_DATE_EDIT(date), label); + return date; +} + +static GtkWidget* +get_treeview (GtkBuilder *builder, const gchar *treeview_label) +{ + auto view = GTK_TREE_VIEW (get_widget (builder, "transaction_view")); + gtk_tree_view_set_grid_lines (GTK_TREE_VIEW(view), gnc_tree_view_get_grid_lines_pref ()); + + auto store = gtk_list_store_new (NUM_SPLIT_COLS, G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + gtk_tree_view_set_model(view, GTK_TREE_MODEL(store)); + g_object_unref(store); + + auto renderer = gtk_cell_renderer_text_new(); + auto column = gtk_tree_view_column_new_with_attributes + (_("Account"), renderer, "text", SPLIT_COL_ACCOUNT, nullptr); + gtk_tree_view_append_column(view, column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes + (_("Memo"), renderer, "text", SPLIT_COL_MEMO, nullptr); + gtk_tree_view_append_column(view, column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes + (_("Debit"), renderer, "text", SPLIT_COL_DEBIT, nullptr); + gtk_tree_view_append_column(view, column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes + (_("Credit"), renderer, "text", SPLIT_COL_CREDIT, nullptr); + gtk_tree_view_append_column(view, column); + + return GTK_WIDGET (view); +} + +static GtkWidget * +stock_assistant_create (StockTransactionInfo *info) +{ + auto builder = gtk_builder_new(); + gnc_builder_add_from_file (builder , "assistant-stock-transaction.glade", "stock_transaction_assistant"); + info->window = get_widget (builder, "stock_transaction_assistant"); + + // Set the name for this assistant so it can be easily manipulated with css + gtk_widget_set_name (GTK_WIDGET(info->window), "gnc-id-assistant-stock-transaction"); + + auto balance = xaccAccountGetBalance (info->acct); + info->txn_types = gnc_numeric_zero_p (balance) ? &starting_types + : gnc_numeric_positive_p (balance) ? &long_types + : &short_types; + + info->currency = gnc_account_get_currency_or_parent (info->acct); + + /* Transaction Page Widgets */ + info->transaction_type_page = get_widget (builder, "transaction_type_page"); + info->transaction_type_combo = get_widget (builder, "transaction_type_page_combobox"); + info->transaction_type_explanation = get_widget (builder, "transaction_type_page_explanation"); + g_signal_connect (info->transaction_type_combo, "changed", + G_CALLBACK (refresh_page_transaction_type), info); + + /* Transaction Details Widgets */ + info->transaction_details_page = get_widget (builder, "transaction_details_page"); + info->date_edit = create_date (builder, 0, "transaction_date_label", "transaction_details_table"); + info->transaction_description_entry = get_widget (builder, "transaction_description_entry"); + + /* Stock Amount Page Widgets */ + info->stock_amount_page = get_widget (builder, "stock_amount_page"); + info->prev_amount = get_widget (builder, "prev_balance_amount"); + info->stock_amount_edit = create_gae (builder, 1, xaccAccountGetCommodity (info->acct), "stock_amount_table", "stock_amount_label"); + info->next_amount = get_widget (builder, "next_balance_amount"); + g_signal_connect (info->stock_amount_edit, "changed", + G_CALLBACK (refresh_page_stock_amount), info); + + /* Stock Value Page Widgets */ + info->stock_value_page = get_widget (builder, "stock_value_page"); + info->stock_value_edit = create_gae (builder, 0, info->currency, "stock_value_table", "stock_value_label"); + info->price_value = get_widget (builder, "stock_price_amount"); + info->stock_memo_edit = get_widget (builder, "stock_memo_entry"); + g_signal_connect (info->stock_value_edit, "changed", + G_CALLBACK (refresh_page_stock_value), info); + + /* Cash Page Widgets */ + info->cash_page = get_widget (builder, "cash_details_page"); + info->cash_account = create_gas (builder, 0, { ACCT_TYPE_ASSET, ACCT_TYPE_BANK }, info->currency, "cash_table", "cash_account_label"); + info->cash_value = create_gae (builder, 1, info->currency, "cash_table", "cash_label"); + info->cash_memo_edit = get_widget (builder, "cash_memo_entry"); + + /* Fees Page Widgets */ + info->fees_page = get_widget (builder, "fees_details_page"); + info->capitalize_fees_checkbox = get_widget (builder, "capitalize_fees_checkbutton"); + info->fees_account = create_gas (builder, 1, { ACCT_TYPE_EXPENSE }, info->currency, "fees_table", "fees_account_label"); + info->fees_value = create_gae (builder, 2, info->currency, "fees_table", "fees_label"); + info->fees_memo_edit = get_widget (builder, "fees_memo_entry"); + g_signal_connect (info->capitalize_fees_checkbox, "toggled", + G_CALLBACK (refresh_page_fees), info); + + /* Divi Page Widgets */ + info->dividend_page = get_widget (builder, "dividend_details_page"); + info->dividend_account = create_gas (builder, 0, { ACCT_TYPE_INCOME }, info->currency, "dividend_table", "dividend_account_label"); + info->dividend_value = create_gae (builder, 1, info->currency, "dividend_table", "dividend_label"); + info->dividend_memo_edit = get_widget (builder, "dividend_memo_entry"); + + /* Capgains Page Widgets */ + info->capgains_page = get_widget (builder, "capgains_details_page"); + info->capgains_account = create_gas (builder, 0, { ACCT_TYPE_INCOME }, info->currency, "capgains_table", "capgains_account_label"); + info->capgains_value = create_gae (builder, 1, info->currency, "capgains_table", "capgains_label"); + info->capgains_memo_edit = get_widget (builder, "capgains_memo_entry"); + + /* Finish Page Widgets */ + info->finish_page = get_widget (builder, "finish_page"); + info->finish_split_view = get_treeview (builder, "transaction_view"); + info->finish_summary = get_widget (builder, "finish_summary"); + g_signal_connect (G_OBJECT(info->window), "destroy", + G_CALLBACK (stock_assistant_window_destroy_cb), info); + + gtk_assistant_set_forward_page_func (GTK_ASSISTANT(info->window), + (GtkAssistantPageFunc)forward_page_func, + info, nullptr); + gtk_builder_connect_signals(builder, info); + g_object_unref(G_OBJECT(builder)); + + return info->window; +} + +static void +refresh_handler (GHashTable *changes, gpointer user_data) +{ + auto info = static_cast(user_data); + + if (!GNC_IS_ACCOUNT (info->acct)) + { + PWARN ("account %p does not exist anymore. abort", info->acct); + gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info); + } +} + +static void +close_handler (gpointer user_data) +{ + auto info = static_cast(user_data); + gtk_widget_destroy (info->window); +} + +/********************************************************************\ + * gnc_stock_transaction_assistant * + * opens up a assistant to record a stock transaction * + * * + * Args: parent - the parent ofthis window * + * initial - the initial account to use * + * Return: nothing * +\********************************************************************/ +void +gnc_stock_transaction_assistant (GtkWidget *parent, Account *account) +{ + StockTransactionInfo *info = g_new0 (StockTransactionInfo, 1); + info->acct = account; + stock_assistant_create (info); + auto component_id = gnc_register_gui_component + (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, refresh_handler, close_handler, info); + gnc_gui_component_watch_entity_type (component_id, GNC_ID_ACCOUNT, + QOF_EVENT_MODIFY | QOF_EVENT_DESTROY); + gtk_window_set_transient_for (GTK_WINDOW (info->window), GTK_WINDOW(parent)); + gtk_widget_show_all (info->window); + + // gnc_window_adjust_for_screen (GTK_WINDOW(info->window)); +} diff --git a/gnucash/gnome/assistant-stock-transaction.h b/gnucash/gnome/assistant-stock-transaction.h new file mode 100644 index 0000000000..3173976a45 --- /dev/null +++ b/gnucash/gnome/assistant-stock-transaction.h @@ -0,0 +1,38 @@ +/********************************************************************\ + * assistant-stock-transaction.h -- stock assistant for GnuCash * + * Copyright (C) 2022 Christopher Lam * + * * + * 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 GNC_ASSISTANT_STOCK_TRANSACTION_H +#define GNC_ASSISTANT_STOCK_TRANSACTION_H + +#include "Account.h" + +/********************************************************************\ + * gnc_stock_transaction_assistant * + * opens up a assistant to record a stock transaction * + * * + * Args: parent - the parent of this window * + * account - the initial account to use * + * Return: nothing * +\********************************************************************/ +void gnc_stock_transaction_assistant (GtkWidget *parent, Account * account); + +#endif diff --git a/gnucash/gtkbuilder/CMakeLists.txt b/gnucash/gtkbuilder/CMakeLists.txt index 348b7a3b6f..2a5201bff9 100644 --- a/gnucash/gtkbuilder/CMakeLists.txt +++ b/gnucash/gtkbuilder/CMakeLists.txt @@ -8,6 +8,7 @@ set (gtkbuilder_SOURCES assistant-loan.glade assistant-qif-import.glade assistant-stock-split.glade + assistant-stock-transaction.glade assistant-xml-encoding.glade business-options-gnome.glade business-prefs.glade diff --git a/gnucash/gtkbuilder/assistant-stock-transaction.glade b/gnucash/gtkbuilder/assistant-stock-transaction.glade new file mode 100644 index 0000000000..ac49bdc71f --- /dev/null +++ b/gnucash/gtkbuilder/assistant-stock-transaction.glade @@ -0,0 +1,941 @@ + + + + + + False + start + 12 + Stock Split Assistant + center + 500 + + + + + + True + False + start + 12 + 12 + This assistant will help you record a stock transaction. The transaction type (purchase, sale, dividend, distribution, return of capital, stock split) will determine the transaction splits involved in the transaction. + True + + + intro + Stock Transaction Assistant + True + + + + + True + False + 12 + 6 + vertical + + + True + False + 6 + Select the type of stock activity that you wish to record. The available types depend on whether the current stock balance is positive, nil, or negative (i.e. when shorting stock). The type will determine the component splits. + True + + + False + False + 0 + + + + + + True + False + 3 + 6 + + + True + False + Type + + + 0 + 0 + + + + + True + False + + a1 + b2 + c3 + + + + 1 + 0 + + + + + True + False + label + True + + + 1 + 1 + + + + + + + + False + True + 1 + + + + + Transaction Type + True + + + + + True + False + 12 + 6 + vertical + + + True + False + 6 + Select the type of transaction date and description for your records. + True + + + False + False + 0 + + + + + + True + False + 3 + 6 + + + True + False + Date + + + 0 + 0 + + + + + True + False + Description + + + 0 + 1 + + + + + True + True + + + 1 + 1 + + + + + + + + False + True + 1 + + + + + Transaction Details + True + + + + + True + False + 6 + vertical + + + True + False + 6 + Enter the date and the number of shares you gained or lost in the transaction. + True + + + False + True + 0 + + + + + + True + False + 6 + 6 + 6 + + + True + False + end + _Shares + True + center + + + 0 + 1 + + + + + True + False + end + Previous Balance + True + center + + + 0 + 0 + + + + + True + False + end + (Previous Balance) + True + center + + + 1 + 0 + + + + + True + False + end + Next Balance + True + center + + + 0 + 2 + + + + + True + False + end + (Next Balance) + True + center + + + 1 + 2 + + + + + + + + False + False + 1 + + + + + Stock Amount + True + + + + + True + False + 6 + vertical + + + True + False + 6 + Enter the value of the shares. + True + + + False + True + 0 + + + + + + True + False + 6 + 6 + 6 + + + True + False + end + (autocalculated price) + True + center + + + 1 + 1 + + + + + True + False + end + _Memo + True + center + + + 0 + 2 + + + + + True + True + + + 1 + 2 + + + + + True + False + end + Price + True + center + + + 0 + 1 + + + + + True + False + end + _Stock Value + True + center + + + 0 + 0 + + + + + + + + False + False + 1 + + + + + Stock Value + True + + + + + True + False + 6 + vertical + 12 + + + True + False + 6 + In this page, input the asset account, and the monetary amount received/spent. It may differ from the stock value from the last page, if there are broker fees associated with this transaction. + True + + + False + False + 0 + + + + + + True + False + 3 + 6 + + + True + False + end + _Amount + True + center + + + 0 + 1 + + + + + True + False + end + _Memo + True + center + cash_memo_entry + + + 0 + 2 + + + + + True + True + True + + Cash + + + 1 + 2 + + + + + True + False + True + + + 1 + 1 + + + + + True + False + end + Cash Account + True + center + cash_memo_entry + + + 0 + 0 + + + + + + + + False + False + 1 + + + + + Cash + True + + + + + True + False + 6 + vertical + 12 + + + True + False + 6 + In this page, input any broker fees incurred in this transaction. The fee may be capitalized into the asset account, or expensed into a broker fees account. Typically fees on purchase are capitalized, and fees on sales are expensed. If there are no fees involved in this transaction, it can be left blank. + True + + + False + False + 0 + + + + + + True + False + 3 + 6 + + + True + False + end + _Amount + True + center + + + 0 + 2 + + + + + True + False + end + _Memo + True + center + fees_memo_entry + + + 0 + 3 + + + + + True + True + True + + Fees + + + 1 + 3 + + + + + True + False + True + + + 1 + 2 + + + + + True + False + end + Broker Fees + True + center + fees_memo_entry + + + 0 + 1 + + + + + Capitalize broker fees into stock account? + True + True + False + True + + + 0 + 0 + 2 + + + + + + + + False + False + 1 + + + + + Fees + True + + + + + True + False + 6 + vertical + 12 + + + True + False + 6 + In this page, input any dividend income received in this transaction. + True + + + False + False + 0 + + + + + + True + False + 3 + 6 + + + True + False + end + _Amount + True + center + + + 0 + 1 + + + + + True + False + end + _Memo + True + center + dividend_memo_entry + + + 0 + 2 + + + + + True + True + True + + Dividend + + + 1 + 2 + + + + + True + False + True + + + 1 + 1 + + + + + True + False + end + Dividend Account + True + center + dividend_memo_entry + + + 0 + 0 + + + + + + + + False + False + 1 + + + + + Dividend + True + + + + + True + False + 6 + vertical + 12 + + + True + False + 6 + In this page, input any capital gains or losses incurred, and the associated income account. Capital gains are positive, and capital losses are negative. + True + + + False + False + 0 + + + + + + True + False + 3 + 6 + + + True + False + end + _Amount + True + center + + + 0 + 1 + + + + + True + False + end + _Memo + True + center + capgains_memo_entry + + + 0 + 2 + + + + + True + True + True + + + + 1 + 2 + + + + + True + False + True + + + 1 + 1 + + + + + True + False + end + Capital Gains Account + True + center + capgains_memo_entry + + + 0 + 0 + + + + + + + + False + False + 1 + + + + + Capital Gains + True + + + + + True + False + 6 + vertical + 12 + + + True + False + 6 + A summary of splits is shown as follows. If the summary is correct, and there are no errors, please press "Apply". You may also press "Back" to review your choices, or "Cancel" to quit without making any changes. + True + + + False + False + 0 + + + + + True + True + + + True + True + + + browse + + + + + + + True + True + 1 + + + + + True + False + summary of errors + + + False + True + 2 + + + + + confirm + Finish + + + + + False + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index 6c9be8e2c2..1fe34570f5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -52,6 +52,7 @@ gnucash/gnome/assistant-acct-period.c gnucash/gnome/assistant-hierarchy.c gnucash/gnome/assistant-loan.cpp gnucash/gnome/assistant-stock-split.c +gnucash/gnome/assistant-stock-transaction.cpp gnucash/gnome/business-gnome-utils.c gnucash/gnome/business-options-gnome.c gnucash/gnome/business-urls.c @@ -243,6 +244,7 @@ gnucash/gtkbuilder/assistant-hierarchy.glade gnucash/gtkbuilder/assistant-loan.glade gnucash/gtkbuilder/assistant-qif-import.glade gnucash/gtkbuilder/assistant-stock-split.glade +gnucash/gtkbuilder/assistant-stock-transaction.glade gnucash/gtkbuilder/assistant-xml-encoding.glade gnucash/gtkbuilder/business-options-gnome.glade gnucash/gtkbuilder/business-prefs.glade