gnucash stable: Multiple changes pushed

John Ralls jralls at code.gnucash.org
Sun Jun 11 16:16:15 EDT 2023


Updated	 via  https://github.com/Gnucash/gnucash/commit/6d60c112 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/96e349ba (commit)
	 via  https://github.com/Gnucash/gnucash/commit/c4e0ddb8 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/18696310 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/43b161c2 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/f4ac1f40 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/fdfa8353 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/b3b071e0 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/cebc7198 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/925999e0 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/4ebc0287 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/74f707bf (commit)
	 via  https://github.com/Gnucash/gnucash/commit/6d36d1d7 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/0d9dd3b7 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/55046f22 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/d67f0b3c (commit)
	 via  https://github.com/Gnucash/gnucash/commit/d92d5b4b (commit)
	 via  https://github.com/Gnucash/gnucash/commit/f36e5d56 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/f7bfd0bc (commit)
	 via  https://github.com/Gnucash/gnucash/commit/3c81b4c8 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/f46a958d (commit)
	 via  https://github.com/Gnucash/gnucash/commit/7fb03b1b (commit)
	from  https://github.com/Gnucash/gnucash/commit/3307b802 (commit)



commit 6d60c112525c9b8be04332eb9968199c5468f57e
Merge: 96e349ba8d f7bfd0bcf8
Author: John Ralls <jralls at ceridwen.us>
Date:   Sun Jun 11 13:11:41 2023 -0700

    Merge Bob Fewell's 'revised-reg-desc' into stable.


commit 96e349ba8d1d1b6102ddbad9049629e71dbfbaae
Merge: 3307b802fe c4e0ddb819
Author: John Ralls <jralls at ceridwen.us>
Date:   Sun Jun 11 13:08:51 2023 -0700

    Merge Christopher Lam's 'stock-txns-mvc' into stable.


commit c4e0ddb8192950718b8617b365730ed6f6da9ce8
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Jun 10 18:00:21 2023 -0700

    [stock-txn-asst] Don't use the initial value in amount edit widgets.
    
    Unless they're valid. Otherwise the invalid value will log an error
    that will prevent later valid input from working.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index 4feefc4101..116dd512bb 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -1926,39 +1926,48 @@ stock_assistant_prepare_cb (GtkAssistant  *assistant, GtkWidget *page,
     case PAGE_STOCK_AMOUNT:
         view->m_stock_amount_page.prepare (model->m_input_new_balance,
                                            model->get_stock_balance_str());
-
-        info->model->m_stock_entry->set_amount(view->m_stock_amount_page.get_stock_amount(), model->m_errors);
+        if (!gnc_numeric_check(view->m_stock_amount_page.get_stock_amount()))
+            info->model->m_stock_entry->set_amount(view->m_stock_amount_page.get_stock_amount(),
+                                                   model->m_errors);
         view->m_stock_amount_page.set_stock_amount(info->model->get_new_amount_str());
         view->m_stock_amount_page.m_amount.set_focus();
         break;
     case PAGE_STOCK_VALUE:
         model->m_stock_entry->m_memo = view->m_stock_value_page.get_memo();
-        model->m_stock_entry->set_value(view->m_stock_value_page.m_value.get(), "stock", model->m_errors);
+        if (!gnc_numeric_check(view->m_stock_value_page.m_value.get()))
+            model->m_stock_entry->set_value(view->m_stock_value_page.m_value.get(),
+                                            "stock", model->m_errors);
         view->m_stock_value_page.set_price(model->calculate_price());
         view->m_stock_value_page.m_value.set_focus();
         break;
     case PAGE_CASH:
         model->m_cash_entry->m_memo = view->m_cash_page.get_memo();
-        model->m_cash_entry->set_value (view->m_cash_page.m_value.get(), "cash", model->m_errors);
+        if (!gnc_numeric_check(view->m_cash_page.m_value.get()))
+            model->m_cash_entry->set_value (view->m_cash_page.m_value.get(),
+                                            "cash", model->m_errors);
         model->m_cash_entry->m_account = view->m_cash_page.m_account.get();
         view->m_cash_page.m_value.set_focus();
         break;
     case PAGE_FEES:
         view->m_fees_page.set_capitalize_fees (info);
         model->m_fees_entry->m_memo = view->m_fees_page.get_memo();
-        model->m_fees_entry->set_value (view->m_fees_page.m_value.get(), "fees", model->m_errors);
+        if (!gnc_numeric_check(view->m_fees_page.m_value.get()))
+            model->m_fees_entry->set_value (view->m_fees_page.m_value.get(), "fees",
+                                            model->m_errors);
         model->m_fees_entry->m_account = view->m_fees_page.m_account.get();
         view->m_fees_page.m_value.set_focus();
         break;
     case PAGE_DIVIDEND:
         model->m_dividend_entry->m_memo = view->m_dividend_page.get_memo();
-        model->m_dividend_entry->set_value (view->m_dividend_page.m_value.get(), "dividend", model->m_errors);
+        if (!gnc_numeric_check(view->m_dividend_page.m_value.get()))
+            model->m_dividend_entry->set_value (view->m_dividend_page.m_value.get(), "dividend", model->m_errors);
         model->m_dividend_entry->m_account = view->m_dividend_page.m_account.get();
         view->m_dividend_page.m_value.set_focus();
         break;
     case PAGE_CAPGAINS:
         model->m_capgains_entry->m_memo = view->m_capgain_page.get_memo();
-        model->m_capgains_entry->set_value(view->m_capgain_page.m_value.get(), "capgains", model->m_errors);
+        if (gnc_numeric_check(view->m_capgain_page.m_value.get()))
+            model->m_capgains_entry->set_value(view->m_capgain_page.m_value.get(), "capgains", model->m_errors);
         model->m_capgains_entry->m_account = view->m_capgain_page.m_account.get();
         view->m_capgain_page.m_value.set_focus();
         break;

commit 186963109d86615e5786c6a36051a522d3847d58
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Jun 10 17:23:35 2023 -0700

    [stock-txn-asst] Remove some superflous declarations.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index 9043bbdc6a..4feefc4101 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -89,11 +89,6 @@ void stock_assistant_finish_cb  (GtkAssistant *assistant, gpointer user_data);
 void stock_assistant_cancel_cb  (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* GNC_PREFS_GROUP = "dialogs.stock-assistant";
 static const char* ASSISTANT_STOCK_TRANSACTION_CM_CLASS = "assistant-stock-transaction";
 
@@ -151,22 +146,19 @@ enum class FieldMask : unsigned
     CAPGAINS_IN_STOCK    = 1 << 8, // capg only: add a balancing split in stock acct
 };
 
-FieldMask operator |(FieldMask lhs, FieldMask rhs)
+static FieldMask
+operator |(FieldMask lhs, FieldMask rhs)
 {
     return static_cast<FieldMask> (static_cast<unsigned>(lhs) |
                                    static_cast<unsigned>(rhs));
 };
 
-bool operator &(FieldMask lhs, FieldMask rhs)
+static bool
+operator &(FieldMask lhs, FieldMask rhs)
 {
     return (static_cast<unsigned>(lhs) & static_cast<unsigned>(rhs));
 };
 
-FieldMask operator ^(FieldMask lhs, FieldMask rhs)
-{
-    return static_cast<FieldMask> (static_cast<unsigned>(lhs) ^
-                                   static_cast<unsigned>(rhs));
-};
 /* The pages displayed by the assistant and which fields are enabled on each
  * page is controlled by TxnTypeInfos, one for each transaction type.
  */

commit 43b161c28b1345d8c9fd22ef7efb0a8e23c90341
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Jun 10 13:44:34 2023 -0700

    [stock-txn-asst] Add comments explaining workings.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index 046251ff90..9043bbdc6a 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -52,10 +52,37 @@
 
 static QofLogModule log_module = GNC_MOD_ASSISTANT;
 
+/** The Stock Transaction Assistant guides the user through collecting the
+ * information needed to properly account for one of several types of securities
+ * investment transaction, including opening and closing long and short
+ * positions, oridinary and capital gains dividends, returns of capital,
+ * notional dividends, splits, and reverse splits. It tailors the available
+ * transaction types and what information it requests based on the current state
+ * of the account: For example, if the account holds an open short position it
+ * will offer buy to cover short and dividend payments to the owner of the
+ * stock.
+ *
+ * The Assistant is built with the Model-View-Controller pattern where
+ * StockAssistantModel manages the data for the transaction, StockAssistantView
+ * creates the visuals using a GtkAssistant and a class for each page type, and
+ * StockAssistantController handles user input events.
+ *
+ * Depending on type a transaction is composed of some of the following splits:
+ * A stock split representing the amount of units and their value in currency, a
+ * cash split representing the source or disposition of that value, a fees split
+ * representing commissions, fees, and taxes paid on the transaction, a dividend
+ * entry representing currency paid by the issuer to its holders (or in the case
+ * of a short position the currency paid by the short seller to the entity
+ * borrowed from compensating them for the dividend that they would have been
+ * paid had they not lent the shares), and two capital gains split representing
+ * the change in currency value from the opening transaction.
+*/
+
 extern "C"
 {
 // These functions are the GtkAssistant primary button callbacks. They're
-// connected to their signals in assistant-stock-transaction.glade.
+// connected to their signals in assistant-stock-transaction.glade so they
+// mustn't be name-mangled.
 void stock_assistant_prepare_cb (GtkAssistant  *assistant, GtkWidget *page,
                                  gpointer user_data);
 void stock_assistant_finish_cb  (GtkAssistant *assistant, gpointer user_data);
@@ -95,8 +122,20 @@ enum split_cols
     SPLIT_COL_UNITS_COLOR,
     NUM_SPLIT_COLS
 };
-
-/** structures *********************************************************/
+/* StockAssistantModel contains a (subclassed) StockTransactionEntry for each split to be created.
+ *
+ * A StockTransactionEntry contains some boolean parameters that are set from a
+ * fieldmask, obtained from the corresponding element in the TxnTypeInfo for the
+ * selected transaction type.
+ *
+ * The available transaction types are populated into
+ * StockAssistantModel::m_txn_types by
+ * StockAssistantModel::maybe_reset_txn_types() based on the state of the
+ * account (long, short, or empty) on the seelected
+ * date. StockAssistantModel::set_txn_type() then sets m_txn_type to the
+ * selected template and calls StockTransactionEntry::set_fieldmask() on each of
+ * its StockTransactionEntry members.
+ */
 
 enum class FieldMask : unsigned
 {
@@ -128,7 +167,9 @@ FieldMask operator ^(FieldMask lhs, FieldMask rhs)
     return static_cast<FieldMask> (static_cast<unsigned>(lhs) ^
                                    static_cast<unsigned>(rhs));
 };
-
+/* The pages displayed by the assistant and which fields are enabled on each
+ * page is controlled by TxnTypeInfos, one for each transaction type.
+ */
 struct TxnTypeInfo
 {
     FieldMask stock_amount;
@@ -387,6 +428,10 @@ then record the reverse split.")
     }
 };
 
+/** Possibly misnamed. Collects the required information to create a single
+ * split in a transaction. This is the base class; there are child classes for
+ * many split types.
+ */
 
 struct StockTransactionEntry
 {
@@ -640,6 +685,7 @@ const char *StockTransactionSplitInfo::s_missing_str = N_("(missing)");
 
 using SplitInfoVec = std::vector<StockTransactionSplitInfo>;
 
+/** Manages the data and actions for the assistant. */
 struct StockAssistantModel
 {
     Account* m_acct;
@@ -1068,6 +1114,8 @@ get_widget (GtkBuilder *builder, const gchar * ID)
     return GTK_WIDGET (obj);
 }
 
+/* Editor widgets used in assistant pages. */
+
 struct GncDateEdit
 {
     GtkWidget *m_edit;
@@ -1234,6 +1282,8 @@ GncAccountSelector::connect (Account **acct)
     g_signal_connect(m_selector, "account_sel_changed", G_CALLBACK (gnc_account_sel_changed_cb), acct);
 }
 
+/* Assistant page classes. */
+
 struct PageTransType {
     // transaction type page
     GtkWidget * m_page;
@@ -1667,6 +1717,10 @@ PageCapGain::connect(Account **account, const char **memo, gnc_numeric *value)
     m_value.connect(value);
 }
 
+/* The last page of the assistant shows what the resulting transaction will look
+ * like.
+*/
+/* The GncFinishtreeview lays out the transaction.*/
 struct GncFinishTreeview
 {
     GtkWidget *m_treeview;
@@ -1770,7 +1824,9 @@ PageFinish::prepare (GtkWidget *window, StockAssistantModel *model)
     }
     gtk_label_set_text(GTK_LABEL(m_summary), summary.c_str());
     gtk_assistant_set_page_complete(GTK_ASSISTANT(window), m_page, success);
- }
+}
+
+/* The StockAssistantView contains the pages and manages displaying them one at a time. */
 
 struct StockAssistantView {
     GtkWidget * m_window;
@@ -1817,6 +1873,8 @@ StockAssistantView::~StockAssistantView()
     DEBUG ("StockAssistantView destructor\n");
 };
 
+/* The StockAssistantController manages the event handlers and user input. */
+
 static void connect_signals (gpointer, GtkBuilder*);
 
 struct StockAssistantController
@@ -1838,7 +1896,8 @@ struct StockAssistantController
     ~StockAssistantController (){ DEBUG ("StockAssistantController destructor\n"); };
 };
 
-/******* implementations ***********************************************/
+// These callbacks must be registered with the GtkAssistant so they can't be member functions.
+
 static void
 stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data)
 {

commit f4ac1f40fe6fcafb28daff4396177aea3208f084
Author: John Ralls <jralls at ceridwen.us>
Date:   Tue Feb 21 10:32:41 2023 -0800

    [stock-txn-asst] Extract page classes for StockAssistantView.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index 9492538a34..046251ff90 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -40,6 +40,9 @@
 #include "assistant-stock-transaction.h"
 #include "gnc-account-sel.h"
 #include "gnc-amount-edit.h"
+#include "gnc-date.h"
+#include <gnc-date-edit.h>
+#include "gnc-engine.h"
 #include "gnc-numeric.h"
 #include "gnc-prefs.h"
 #include "gnc-component-manager.h"
@@ -405,7 +408,7 @@ struct StockTransactionEntry
 
     virtual void set_fieldmask(FieldMask mask);
     virtual void set_capitalize(bool capitalize) {}
-    virtual void set_value(gnc_numeric amount); //, const char* page, StringVec& errors);
+    virtual void set_value(gnc_numeric amount, const char* page, StringVec& errors);
     virtual gnc_numeric amount() { return m_value; }
     virtual void set_amount(gnc_numeric, StringVec&) {}
     virtual void create_split(Transaction* trans, const char* action,
@@ -428,9 +431,9 @@ StockTransactionEntry::set_fieldmask(FieldMask mask)
 
 
 void
-StockTransactionEntry::set_value(gnc_numeric amount) //, const char* page, StringVec& errors)
+StockTransactionEntry::set_value(gnc_numeric amount, const char* page, StringVec& errors)
 {
-#if 0    //FIXME, logging is broken for want of the page name
+
     auto add_error = [&errors](const char* format_str, const char* arg)
     {
         char *buf = g_strdup_printf (_(format_str),
@@ -445,7 +448,7 @@ StockTransactionEntry::set_value(gnc_numeric amount) //, const char* page, Strin
         add_error (N_("Amount for %s is missing."), page);
         return;
     }
-#endif
+
     if (gnc_numeric_negative_p (amount))
     {
         if (m_allow_negative)
@@ -453,8 +456,6 @@ StockTransactionEntry::set_value(gnc_numeric amount) //, const char* page, Strin
             m_value = gnc_numeric_neg(amount);
             m_debit_side = !m_debit_side;
         }
-    }
-#if 0
         else
         {
             if (m_allow_zero)
@@ -467,7 +468,7 @@ StockTransactionEntry::set_value(gnc_numeric amount) //, const char* page, Strin
         add_error (N_("Amount for %s must be positive."), page);
         return;
     }
-#endif
+
     m_value = m_debit_side ? amount : gnc_numeric_neg (amount);
 }
 
@@ -534,7 +535,6 @@ struct StockTransactionStockEntry : public StockTransactionEntry
 void
 StockTransactionStockEntry::set_fieldmask(FieldMask mask)
 {
-//FIXME, need to combine stock-value and stock-amt fieldmasks
     StockTransactionEntry::set_fieldmask(mask);
     m_enabled = mask & (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT |
                         FieldMask::AMOUNT_CREDIT | FieldMask::AMOUNT_DEBIT);
@@ -662,6 +662,7 @@ struct StockAssistantModel
     StockTransactionEntryPtr m_dividend_entry;
     StockTransactionEntryPtr m_capgains_entry;
     StockTransactionEntryPtr m_stock_cg_entry; // Required at this level for lifetime management
+    StringVec m_errors, m_warnings, m_infos;
 
     StockAssistantModel (Account *account) :
         m_acct{account},
@@ -703,7 +704,8 @@ private:
     SplitInfoVec m_list_of_splits;
 
     void add_price (QofBook *book);
-    StockTransactionSplitInfo make_stock_split_info(StringVec& errors);
+    StockTransactionSplitInfo make_stock_split_info();
+    std::string summary_message();
 };
 
 bool
@@ -817,10 +819,10 @@ check_txn_date(GList* last_split_node, time64 txn_date, StringVec& warnings)
 }
 
 StockTransactionSplitInfo
-StockAssistantModel::make_stock_split_info(StringVec& errors)
+StockAssistantModel::make_stock_split_info()
 {
-    auto add_error_str = [&errors]
-        (const char* str) { errors.emplace_back (_(str)); };
+    auto add_error_str = [this]
+        (const char* str) { m_errors.emplace_back (_(str)); };
 
     StockTransactionSplitInfo line{m_stock_entry.get(),
         NC_ ("Stock Assistant: Page name", "stock value")};
@@ -836,7 +838,7 @@ StockAssistantModel::make_stock_split_info(StringVec& errors)
         auto ratio = gnc_numeric_div(stock_amount, m_balance_at_date,
                                      GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
         stock_amount = gnc_numeric_sub_fixed(stock_amount, m_balance_at_date);
-        line.m_entry->set_amount(stock_amount, errors);
+        line.m_entry->set_amount(stock_amount, m_errors);
         line.m_units_in_red =
             negative_in_red && gnc_numeric_negative_p(stock_amount);
         if (gnc_numeric_check(ratio) || !gnc_numeric_positive_p(ratio))
@@ -866,26 +868,25 @@ StockAssistantModel::make_stock_split_info(StringVec& errors)
     return line;
 }
 
-static inline std::string
-summary_message(const StringVec& infos, const StringVec& warnings,
-                const StringVec& errors)
+std::string
+StockAssistantModel::summary_message()
 {
     std::ostringstream summary;
     auto summary_add = [&summary](auto a) { summary << "\n• " << a; };
-    if (errors.empty())
+    if (m_errors.empty())
     {
         summary << _("No errors found. Click Apply to create transaction.");
-        std::for_each (infos.begin(), infos.end(), summary_add);
+        std::for_each (m_infos.begin(), m_infos.end(), summary_add);
     }
     else
     {
         summary << _("The following errors must be fixed:");
-        std::for_each (errors.begin(), errors.end(), summary_add);
+        std::for_each (m_errors.begin(), m_errors.end(), summary_add);
     }
-    if (!warnings.empty())
+    if (!m_warnings.empty())
     {
         summary << "\n\n" << _("The following warnings exist:");
-        std::for_each (warnings.begin(), warnings.end(), summary_add);
+        std::for_each (m_warnings.begin(), m_warnings.end(), summary_add);
     }
 
     return summary.str();
@@ -900,7 +901,6 @@ StockAssistantModel::generate_list_of_splits() {
 
     gnc_numeric debit = gnc_numeric_zero ();
     gnc_numeric credit = gnc_numeric_zero ();
-    StringVec errors, warnings, infos;
 
     // check the stock transaction date. If there are existing stock
     // transactions dated after the date specified, it is very likely
@@ -908,10 +908,10 @@ StockAssistantModel::generate_list_of_splits() {
     // to review them.
     auto last_split_node = g_list_last (xaccAccountGetSplitList (m_acct));
     if (last_split_node)
-        check_txn_date(last_split_node, m_transaction_date, warnings);
+        check_txn_date(last_split_node, m_transaction_date, m_warnings);
 
 
-    m_list_of_splits.push_back (make_stock_split_info(errors));
+    m_list_of_splits.push_back (make_stock_split_info());
 
     auto [has_price, price, price_str] = calculate_price ();
     if (has_price)
@@ -924,7 +924,7 @@ StockAssistantModel::generate_list_of_splits() {
             (_(tmpl),
              gnc_commodity_get_mnemonic (xaccAccountGetCommodity (m_acct)),
              price_str, date_str);
-        infos.emplace_back (price_msg);
+        m_infos.emplace_back (price_msg);
         g_free (date_str);
     }
 
@@ -978,7 +978,7 @@ StockAssistantModel::generate_list_of_splits() {
         if (err_act)
         {
             auto err_str = g_strdup_printf (N_("Transaction can't balance, %s is error value %s"), err_act, err_reason);
-            errors.emplace_back(err_str);
+            m_errors.emplace_back(err_str);
             g_free (err_str);
         }
         else
@@ -987,7 +987,7 @@ StockAssistantModel::generate_list_of_splits() {
             auto debit_str = g_strdup (xaccPrintAmount (debit, m_curr_pinfo));
             auto credit_str = g_strdup (xaccPrintAmount (credit, m_curr_pinfo));
             auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
-            errors.emplace_back (error_str);
+            m_errors.emplace_back (error_str);
             g_free (error_str);
             g_free (credit_str);
             g_free (debit_str);
@@ -996,8 +996,8 @@ StockAssistantModel::generate_list_of_splits() {
 
     // generate final summary message. Collates a header, the errors
     // and warnings. Then allow completion if errors is empty.
-    m_ready_to_create = errors.empty();
-    return { m_ready_to_create, summary_message(infos, warnings, errors), m_list_of_splits };
+    m_ready_to_create = m_errors.empty();
+    return { m_ready_to_create, summary_message(), m_list_of_splits };
 }
 
 std::tuple<bool, Transaction*>
@@ -1048,435 +1048,803 @@ StockAssistantModel::add_price (QofBook *book)
     gnc_price_unref (price);
 }
 
+/* ********************* View Classes ************************/
 
-struct StockAssistantView
+/* ***************** Generic Event Callbacks ****************/
+static void
+text_entry_changed_cb (GtkWidget *widget, const gchar **model_text)
 {
-    GtkWidget * window;
-
-    // transaction type page
-    GtkWidget * transaction_type_page;
-    GtkWidget * transaction_type_combo;
-    GtkWidget * transaction_type_explanation;
+    *model_text = gtk_entry_get_text (GTK_ENTRY (widget));
+}
 
-    // transaction details page
-    GtkWidget * transaction_details_page;
-    GtkWidget * transaction_date;
-    GtkWidget * transaction_description;
 
-    // stock amount page
-    GtkWidget * stock_amount_page;
-    GtkWidget * stock_amount_title;
-    GtkWidget * prev_amount;
-    GtkWidget * next_amount;
-    GtkWidget * next_amount_label;
-    GtkWidget * stock_amount_edit;
-    GtkWidget * stock_amount_label;
+static inline 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);
+}
 
-    // stock value page
-    GtkWidget * stock_value_page;
-    GtkWidget * stock_value_edit;
-    GtkWidget * price_value;
-    GtkWidget * stock_memo_edit;
+struct GncDateEdit
+{
+    GtkWidget *m_edit;
+    GncDateEdit(GtkBuilder *builder) :
+        m_edit{gnc_date_edit_new(gnc_time(nullptr), FALSE, FALSE)} {}
+    void attach(GtkBuilder *builder, const char *table_ID, const char *label_ID,
+                int row);
+    time64 get_date_time() { return gnc_date_edit_get_date_end(GNC_DATE_EDIT(m_edit)); }
+    void connect(time64 *target);
+};
 
-    // cash page
-    GtkWidget * cash_page;
-    GtkWidget * cash_account;
-    GtkWidget * cash_memo_edit;
-    GtkWidget * cash_value;
+static void
+gnc_date_edit_changed_cb (GtkWidget* widget, time64 *target)
+{
+    g_return_if_fail(GNC_IS_DATE_EDIT(widget));
+    *target = gnc_date_edit_get_date_end(GNC_DATE_EDIT(widget));
+}
 
-    // fees page
-    GtkWidget * fees_page;
-    GtkWidget * capitalize_fees_checkbox;
-    GtkWidget * fees_account;
-    GtkWidget * fees_memo_edit;
-    GtkWidget * fees_value;
+void
+GncDateEdit::attach(GtkBuilder *builder, const char *table_ID,
+                    const char *label_ID, int row)
+{
+    auto table = get_widget(builder, table_ID);
+    auto label = get_widget (builder, label_ID);
+    gtk_grid_attach(GTK_GRID(table), m_edit, 1, row, 1, 1);
+    gtk_widget_show(m_edit);
+    gnc_date_make_mnemonic_target (GNC_DATE_EDIT(m_edit), label);
+}
 
-    // dividend page
-    GtkWidget * dividend_page;
-    GtkWidget * dividend_account;
-    GtkWidget * dividend_memo_edit;
-    GtkWidget * dividend_value;
+void
+GncDateEdit::connect(time64 *time)
+{
+    g_signal_connect(m_edit, "date_changed", G_CALLBACK (gnc_date_edit_changed_cb), time);
+}
 
-    // capgains page
-    GtkWidget * capgains_page;
-    GtkWidget * capgains_account;
-    GtkWidget * capgains_memo_edit;
-    GtkWidget * capgains_value;
+struct GncAmountEdit
+{
+    GtkWidget *m_edit;
+
+    GncAmountEdit (GtkBuilder *builder, gnc_commodity *commodity);
+    void attach (GtkBuilder *builder, const char *table_id,
+                 const char *label_ID, int row);
+    gnc_numeric get ();
+    void connect (gnc_numeric *value);
+    void connect (GCallback cb, gpointer data);
+    void set_focus();
+    void set_owner (gpointer obj);
+};
 
-    // finish page
-    GtkWidget * finish_page;
-    GtkWidget * finish_split_view;
-    GtkWidget * finish_summary;
+static void
+gnc_amount_edit_changed_cb (GtkWidget* widget, gnc_numeric *value)
+{
+    g_return_if_fail(GNC_IS_AMOUNT_EDIT(widget));
+    gnc_numeric amt;
+    if (!gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT(widget), &amt, true, nullptr))
+        *value =  amt;
+    else
+        *value = gnc_numeric_error(GNC_ERROR_ARG);
+}
 
-    void set_focus (GtkWidget *widget) { gtk_widget_grab_focus (widget); }
-    void set_focus_gae (GtkWidget *gae) { set_focus (GTK_WIDGET (gnc_amount_edit_gtk_entry (GNC_AMOUNT_EDIT (gae)))); }
+GncAmountEdit::GncAmountEdit (GtkBuilder *builder, gnc_commodity *commodity) :
+    m_edit{gnc_amount_edit_new()}
+{
+    // shares amount
+    auto info = gnc_commodity_print_info(commodity, true);
+    gnc_amount_edit_set_evaluate_on_enter(GNC_AMOUNT_EDIT(m_edit), TRUE);
+    gnc_amount_edit_set_print_info(GNC_AMOUNT_EDIT(m_edit), info);
+}
 
-    int get_transaction_type_index ()
-    {
-        return gtk_combo_box_get_active (GTK_COMBO_BOX (transaction_type_combo));
-    }
+void
+GncAmountEdit::attach (GtkBuilder *builder, const char *table_ID,
+                       const char* label_ID, int row)
+{
+    auto table = get_widget(builder, table_ID);
+    auto label = get_widget(builder, label_ID);
+    gtk_grid_attach(GTK_GRID(table), m_edit, 1, row, 1, 1);
+    gtk_widget_show(m_edit);
+    gnc_amount_edit_make_mnemonic_target(GNC_AMOUNT_EDIT(m_edit), label);
+}
 
-    void
-    set_transaction_types (const TxnTypeVec& txn_types)
-    {
-        auto combo = GTK_COMBO_BOX_TEXT (this->transaction_type_combo);
-        gtk_combo_box_text_remove_all (combo);
-        std::for_each (txn_types.begin(), txn_types.end(),
-                       [&combo](const auto& it)
-                       { gtk_combo_box_text_append_text (combo, _(it.friendly_name)); });
-        gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
-    }
+gnc_numeric
+GncAmountEdit::get ()
+{
+    gnc_numeric amt;
+    if (!gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT(m_edit), &amt, true, nullptr))
+        return amt;
+    return gnc_numeric_error(GNC_ERROR_ARG);
+}
 
-    void set_txn_type_explanation (const gchar *txt)
-    {
-        gtk_label_set_text (GTK_LABEL (this->transaction_type_explanation), txt);
-    }
+void
+GncAmountEdit::connect (gnc_numeric *value)
+{
+    g_signal_connect(m_edit, "changed", G_CALLBACK (gnc_amount_edit_changed_cb), value);
+}
 
-    void
-    prepare_stock_amount_page (bool input_new_balance, const std::string prev_balance)
-    {
-        gtk_label_set_text_with_mnemonic
-            (GTK_LABEL (this->stock_amount_label),
-             input_new_balance ? _("Ne_w Balance") : _("_Shares"));
-        gtk_label_set_text
-            (GTK_LABEL (this->next_amount_label),
-             input_new_balance ? _("Ratio") : _("Next Balance"));
-        gtk_label_set_text
-            (GTK_LABEL (this->stock_amount_title),
-             input_new_balance ?
-             _("Enter the new balance of shares after the stock split.") :
-             _("Enter the number of shares you gained or lost in the transaction."));
-        gtk_label_set_text (GTK_LABEL (this->prev_amount), prev_balance.c_str());
-    };
+void
+GncAmountEdit::connect (GCallback cb, gpointer data)
+{
+    g_signal_connect(m_edit, "changed", cb, data);
+}
 
-    void
-    set_stock_amount (std::string new_amount_str)
-    {
-        gtk_label_set_text (GTK_LABEL(this->next_amount), new_amount_str.c_str());
-    };
+void
+GncAmountEdit::set_focus()
+{
+    gtk_widget_grab_focus (GTK_WIDGET (gnc_amount_edit_gtk_entry (GNC_AMOUNT_EDIT (m_edit))));
+}
 
-    void
-    set_price_value (const gchar *val)
-    { gtk_label_set_text (GTK_LABEL (this->price_value), val); };
+void
+GncAmountEdit::set_owner(gpointer obj)
+{
+    g_object_set_data(G_OBJECT (m_edit), "owner", obj);
+}
 
-    bool
-    get_capitalize_fees ()
-    { return gtk_toggle_button_get_active
-            (GTK_TOGGLE_BUTTON (this->capitalize_fees_checkbox)); }
+using AccountTypeList = std::vector<GNCAccountType>;
 
-    void
-    set_capitalize_fees (bool state)
-    {
-        gtk_toggle_button_set_active
-            (GTK_TOGGLE_BUTTON (this->capitalize_fees_checkbox), state);
-    }
+struct GncAccountSelector
+{
+    GtkWidget* m_selector;
+
+    GncAccountSelector (GtkBuilder *builder, AccountTypeList types,
+                        gnc_commodity *currency);
+    void attach (GtkBuilder *builder, const char *table_id,
+                 const char *label_ID, int row);
+    void connect (Account **acct);
+    void set (Account *acct) { gnc_account_sel_set_account (GNC_ACCOUNT_SEL (m_selector), acct, TRUE); }
+    Account *get () { return gnc_account_sel_get_account (GNC_ACCOUNT_SEL (m_selector)); }
+};
 
-    void
-    update_fees_acct_sensitive (bool sensitive)
-    { gtk_widget_set_sensitive (this->fees_account, sensitive); }
+static void
+gnc_account_sel_changed_cb (GtkWidget* widget, Account **acct)
+{
+    g_return_if_fail (GNC_IS_ACCOUNT_SEL (widget));
+    *acct = gnc_account_sel_get_account (GNC_ACCOUNT_SEL (widget));
+}
 
-    void
-    prepare_finish_page (bool success, const std::string& summary,
-                         const SplitInfoVec& list_of_splits, GNCPrintAmountInfo& pinfo)
-    {
-        auto gtv = GTK_TREE_VIEW (this->finish_split_view);
-        auto list = GTK_LIST_STORE (gtk_tree_view_get_model (gtv));
-        gtk_list_store_clear (list);
-        for (const auto& line : list_of_splits)
-        {
-            GtkTreeIter iter;
-            auto tooltip = g_markup_escape_text (line.m_entry->m_memo, -1);
-            gtk_list_store_append (list, &iter);
-            gtk_list_store_set (list, &iter,
-                                SPLIT_COL_ACCOUNT, xaccAccountGetName(line.m_entry->m_account),
-                                SPLIT_COL_MEMO, line.m_entry->m_memo,
-                                SPLIT_COL_TOOLTIP, tooltip,
-                                SPLIT_COL_DEBIT, line.m_entry->m_debit_side ? line.m_entry->print_value(pinfo) : nullptr,
-                                SPLIT_COL_CREDIT, line.m_entry->m_debit_side ? nullptr : line.m_entry->print_value(pinfo),
-                                SPLIT_COL_UNITS, line.m_entry->print_amount(line.m_entry->amount()),
-                                SPLIT_COL_UNITS_COLOR, line.m_units_in_red ? "red" : nullptr,
-                                -1);
-            g_free (tooltip);
-        }
-        gtk_assistant_set_page_complete (GTK_ASSISTANT (this->window),
-                                         this->finish_page, success);
-        gtk_label_set_text (GTK_LABEL (this->finish_summary), summary.c_str());
-    }
+GncAccountSelector::GncAccountSelector (GtkBuilder *builder, AccountTypeList types,
+                                        gnc_commodity *currency) :
+    m_selector{gnc_account_sel_new ()}
+{
+    auto accum = [](auto a, auto b) { return g_list_prepend(a, (gpointer)b); };
+    auto null_glist = static_cast<GList *>(nullptr);
+    auto acct_list = std::accumulate(types.begin(), types.end(), null_glist, accum);
+    auto curr_list = accum(null_glist, currency);
+    gnc_account_sel_set_new_account_ability(GNC_ACCOUNT_SEL(m_selector), true);
+    gnc_account_sel_set_acct_filters(GNC_ACCOUNT_SEL(m_selector), acct_list, curr_list);
+    g_list_free(acct_list);
+    g_list_free(curr_list);
+}
 
-    StockAssistantView (GtkBuilder *builder, gnc_commodity *stock_commodity,
-                        gnc_commodity *currency, GtkWidget *parent)
-        : window (get_widget (builder, "stock_transaction_assistant"))
-        , transaction_type_page (get_widget (builder, "transaction_type_page"))
-        , transaction_type_combo (get_widget (builder, "transaction_type_page_combobox"))
-        , transaction_type_explanation (get_widget (builder, "transaction_type_page_explanation"))
-        , transaction_details_page (get_widget (builder, "transaction_details_page"))
-        , transaction_date (create_date (builder, 0, "transaction_date_label", "transaction_details_table"))
-        , transaction_description (get_widget (builder, "transaction_description_entry"))
-        , stock_amount_page (get_widget (builder, "stock_amount_page"))
-        , stock_amount_title (get_widget (builder, "stock_amount_title"))
-        , prev_amount (get_widget (builder, "prev_balance_amount"))
-        , next_amount (get_widget (builder, "next_balance_amount"))
-        , next_amount_label (get_widget (builder, "next_balance_label"))
-        , stock_amount_edit (create_gae (builder, 1, stock_commodity, "stock_amount_table", "stock_amount_label"))
-        , stock_amount_label (get_widget (builder, "stock_amount_label"))
-        , stock_value_page (get_widget (builder, "stock_value_page"))
-        , stock_value_edit (create_gae (builder, 0, currency, "stock_value_table", "stock_value_label"))
-        , price_value (get_widget (builder, "stock_price_amount"))
-        , stock_memo_edit (get_widget (builder, "stock_memo_entry"))
-        , cash_page (get_widget (builder, "cash_details_page"))
-        , cash_account (create_gas (builder, 0, { ACCT_TYPE_ASSET, ACCT_TYPE_BANK }, currency,  "cash_table", "cash_account_label"))
-        , cash_memo_edit (get_widget (builder, "cash_memo_entry"))
-        , cash_value (create_gae (builder, 1, currency, "cash_table", "cash_label"))
-        , fees_page (get_widget (builder, "fees_details_page"))
-        , capitalize_fees_checkbox (get_widget (builder, "capitalize_fees_checkbutton"))
-        , fees_account (create_gas (builder, 1, { ACCT_TYPE_EXPENSE }, currency, "fees_table", "fees_account_label"))
-        , fees_memo_edit (get_widget (builder, "fees_memo_entry"))
-        , fees_value (create_gae (builder, 2, currency, "fees_table", "fees_label"))
-        , dividend_page (get_widget (builder, "dividend_details_page"))
-        , dividend_account (create_gas (builder, 0, { ACCT_TYPE_INCOME }, currency, "dividend_table", "dividend_account_label"))
-        , dividend_memo_edit (get_widget (builder, "dividend_memo_entry"))
-        , dividend_value (create_gae (builder, 1, currency, "dividend_table", "dividend_label"))
-        , capgains_page (get_widget (builder, "capgains_details_page"))
-        , capgains_account (create_gas (builder, 0, { ACCT_TYPE_INCOME }, currency, "capgains_table", "capgains_account_label"))
-        , capgains_memo_edit (get_widget (builder, "capgains_memo_entry"))
-        , capgains_value (create_gae (builder, 1, currency, "capgains_table", "capgains_label"))
-        , finish_page (get_widget (builder, "finish_page"))
-        , finish_split_view (get_treeview (builder, "transaction_view"))
-        , finish_summary (get_widget (builder, "finish_summary"))
-    {
-        // Set the name for this assistant so it can be easily manipulated with css
-        gtk_widget_set_name (GTK_WIDGET(this->window), "gnc-id-assistant-stock-transaction");
-        gtk_tree_view_set_tooltip_column (GTK_TREE_VIEW (this->finish_split_view),
-                                          SPLIT_COL_TOOLTIP);
-        gtk_window_set_transient_for (GTK_WINDOW (this->window), GTK_WINDOW(parent));
-        gnc_window_adjust_for_screen (GTK_WINDOW(this->window));
-        gnc_restore_window_size (GNC_PREFS_GROUP, GTK_WINDOW(this->window),
-                                 GTK_WINDOW(parent));
-        gtk_widget_show_all (this->window);
-        DEBUG ("StockAssistantView constructor\n");
-    };
-    ~StockAssistantView(){
-        gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(this->window));
-        DEBUG ("StockAssistantView destructor\n");
-    };
+void
+GncAccountSelector::attach (GtkBuilder *builder, const char *table_ID,
+                            const char *label_ID, int row)
+{
+    auto table = get_widget(builder, table_ID);
+    auto label = get_widget(builder, label_ID);
+    gtk_grid_attach(GTK_GRID(table), m_selector, 1, row, 1, 1);
+    gtk_widget_show(m_selector);
+    gtk_label_set_mnemonic_widget(GTK_LABEL(label), m_selector);
+}
 
-private:
-    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);
-    }
+void
+GncAccountSelector::connect (Account **acct)
+{
+    g_signal_connect(m_selector, "account_sel_changed", G_CALLBACK (gnc_account_sel_changed_cb), acct);
+}
 
-    GtkWidget* create_gas (GtkBuilder *builder, gint row,
-                           std::vector<GNCAccountType> 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 ();
-        auto accum = [](auto a, auto b){ return g_list_prepend (a, (gpointer)b); };
-        auto null_glist = static_cast<GList*>(nullptr);
-        auto acct_list = std::accumulate (type.begin(), type.end(), null_glist, accum);
-        auto curr_list = accum (null_glist, 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;
-    }
+struct PageTransType {
+    // transaction type page
+    GtkWidget * m_page;
+    GtkWidget * m_type;
+    GtkWidget * m_explanation;
+    PageTransType(GtkBuilder *builder);
+    void prepare(StockAssistantModel* model);
+    int get_transaction_type_index ();
+    void set_transaction_types (const TxnTypeVec& txn_types);
+    void set_txn_type_explanation (const gchar *txt);
+    void set_focus() { gtk_widget_grab_focus (m_type); }
+    void connect (StockAssistantModel *model);
+    void change_txn_type (StockAssistantModel *model);
+};
 
-    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;
-    }
+PageTransType::PageTransType(GtkBuilder *builder)
+    : m_page(get_widget(builder, "transaction_type_page")),
+      m_type(get_widget(builder, "transaction_type_page_combobox")),
+      m_explanation(get_widget(builder, "transaction_type_page_explanation"))
+{
+    g_object_set_data(G_OBJECT(m_type), "owner", this);
+}
 
-    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 void
+page_trans_type_changed_cb (GtkWidget* widget, StockAssistantModel *model)
+{
+    auto me = static_cast<PageTransType *>(g_object_get_data (G_OBJECT (widget), "owner"));
+    g_return_if_fail (me);
+    me->change_txn_type (model);
+}
 
-    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,
-                                         G_TYPE_STRING, G_TYPE_STRING);
-        gtk_tree_view_set_model(view, GTK_TREE_MODEL(store));
-        gtk_tree_selection_set_mode (gtk_tree_view_get_selection (view),
-                                     GTK_SELECTION_NONE);
-        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();
-        g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, nullptr);
-        column = gtk_tree_view_column_new_with_attributes
-            (_("Memo"), renderer, "text", SPLIT_COL_MEMO, nullptr);
-        gtk_tree_view_column_set_expand (column, true);
-        gtk_tree_view_append_column(view, column);
-
-        renderer = gtk_cell_renderer_text_new();
-        gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
-        gtk_cell_renderer_set_padding (renderer, 5, 0);
-        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();
-        gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
-        gtk_cell_renderer_set_padding (renderer, 5, 0);
-        column = gtk_tree_view_column_new_with_attributes
-            (_("Credit"), renderer, "text", SPLIT_COL_CREDIT, nullptr);
-        gtk_tree_view_append_column(view, column);
-
-        renderer = gtk_cell_renderer_text_new();
-        gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
-        gtk_cell_renderer_set_padding (renderer, 5, 0);
-        column = gtk_tree_view_column_new_with_attributes
-            (_("Units"), renderer,
-             "text", SPLIT_COL_UNITS,
-             "foreground", SPLIT_COL_UNITS_COLOR,
-             nullptr);
-        gtk_tree_view_append_column(view, column);
-
-        return GTK_WIDGET (view);
-    }
-};
+void
+PageTransType::prepare(StockAssistantModel *model)
+{
+    if (!model->m_txn_types)
+        return;
 
-static void connect_signals (gpointer, GtkBuilder*);
+    set_transaction_types(model->m_txn_types.value());
+    change_txn_type (model);
+}
 
-struct StockAssistantController
+int
+PageTransType::get_transaction_type_index ()
 {
-    std::unique_ptr<StockAssistantModel> model;
-    std::unique_ptr<StockAssistantView> view;
-    StockAssistantController (GtkWidget *parent, Account* acct)
-        : model (std::make_unique<StockAssistantModel>(acct))
-    {
-        auto builder = gtk_builder_new();
-        gnc_builder_add_from_file (builder, "assistant-stock-transaction.glade",
-                                   "stock_transaction_assistant");
-        this->view = std::make_unique<StockAssistantView>
-            (builder, xaccAccountGetCommodity (acct), this->model->m_currency, parent);
-        connect_signals (this, builder);
-        g_object_unref (builder);
-        DEBUG ("StockAssistantController constructor\n");
-    };
-    ~StockAssistantController (){ DEBUG ("StockAssistantController destructor\n"); };
-};
+    return gtk_combo_box_get_active (GTK_COMBO_BOX (m_type));
+}
 
-/******* implementations ***********************************************/
-static void
-stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data)
+void
+PageTransType::set_transaction_types (const TxnTypeVec& txn_types)
 {
-    auto info = static_cast<StockAssistantController*>(user_data);
-    gnc_unregister_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info);
-    delete info;
+    auto combo = GTK_COMBO_BOX_TEXT (m_type);
+    gtk_combo_box_text_remove_all (combo);
+    std::for_each (txn_types.begin(), txn_types.end(),
+                   [&combo](const auto& it)
+                   { gtk_combo_box_text_append_text (combo, _(it.friendly_name)); });
+    gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
 }
 
-static void
-controller_transaction_type (GtkWidget *widget, StockAssistantController* info)
+void
+PageTransType::set_txn_type_explanation (const gchar *txt)
 {
-    if (!info->model->m_txn_types)
-        return;
+    gtk_label_set_text (GTK_LABEL (this->m_explanation), txt);
+}
 
-    auto type_idx = info->view->get_transaction_type_index();
+void
+PageTransType::change_txn_type (StockAssistantModel *model)
+{
+    auto type_idx = get_transaction_type_index();
     if (type_idx < 0)           // combo isn't initialized yet.
         return;
 
-    if (!info->model->set_txn_type (type_idx))
+    if (!model->set_txn_type (type_idx))
         return;
 
-    info->view->set_txn_type_explanation (info->model->m_txn_type->explanation);
-    auto fees_entry = dynamic_cast<StockTransactionFeesEntry*>(info->model->m_fees_entry.get());
-    info->view->set_capitalize_fees (fees_entry->m_capitalize);
+    set_txn_type_explanation (model->m_txn_type->explanation);
 }
 
-static void controller_gde (GtkWidget *widget, time64* date)
+void
+PageTransType::connect(StockAssistantModel *model)
 {
-    *date = gnc_date_edit_get_date_end (GNC_DATE_EDIT (widget));
+    g_signal_connect(m_type, "changed",
+                     G_CALLBACK (page_trans_type_changed_cb), model);
 }
 
-static void controller_gtk_entry (GtkWidget *widget, const gchar **model_text)
+struct PageTransDeets
 {
-    *model_text = gtk_entry_get_text (GTK_ENTRY (widget));
+    // transaction details page
+    GtkWidget *m_page;
+    GncDateEdit m_date;
+    GtkWidget *m_description;
+    PageTransDeets (GtkBuilder *builder);
+    time64 get_date_time () { return m_date.get_date_time(); }
+    const char* get_description () { return gtk_entry_get_text (GTK_ENTRY (m_description)); }
+    void set_focus () { gtk_widget_grab_focus (m_description); }
+    void connect (time64 *date, const char **description);
+};
+
+PageTransDeets::PageTransDeets (GtkBuilder *builder) :
+    m_page (get_widget (builder, "transaction_details_page")),
+    m_date (builder),
+    m_description (get_widget (builder, "transaction_description_entry"))
+{
+    m_date.attach(builder,  "transaction_details_table", "transaction_date_label", 0);
 }
 
-static void controller_gae (GtkWidget *widget, gnc_numeric* num)
+void
+PageTransDeets::connect(time64 *date, const char **description)
 {
-    gnc_numeric amt;
-    if (!gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT(widget), &amt, true, nullptr))
-        *num = amt;
-    else
-        *num = gnc_numeric_error(GNC_ERROR_ARG);
+    m_date.connect(date);
+    g_signal_connect(m_description, "changed", G_CALLBACK (text_entry_changed_cb), description);
 }
 
-static void controller_gas (GtkWidget *widget, Account **acct)
+struct PageStockAmount
 {
-    *acct = gnc_account_sel_get_account (GNC_ACCOUNT_SEL (widget));
+    // stock amount page
+    GtkWidget * m_page;
+    GtkWidget * m_title;
+    GtkWidget * m_prev_amount;
+    GtkWidget * m_next_amount;
+    GtkWidget * m_next_amount_label;
+    GncAmountEdit m_amount;
+    GtkWidget * m_amount_label;
+    PageStockAmount (GtkBuilder *builder, gnc_commodity *commodity);
+    void prepare (bool input_new_balance, const std::string prev_balance);
+    gnc_numeric get_stock_amount () { return m_amount.get(); }
+    void set_stock_amount (std::string new_amount_str);
+    void connect(StockAssistantModel *model);
+};
+
+PageStockAmount::PageStockAmount (GtkBuilder *builder, gnc_commodity *stock_commodity) :
+    m_page (get_widget (builder, "stock_amount_page")),
+    m_title (get_widget (builder, "stock_amount_title")),
+    m_prev_amount (get_widget (builder, "prev_balance_amount")),
+    m_next_amount (get_widget (builder, "next_balance_amount")),
+    m_next_amount_label (get_widget (builder, "next_balance_label")),
+    m_amount (builder, stock_commodity),
+    m_amount_label (get_widget (builder, "stock_amount_label"))
+{
+    m_amount.attach (builder, "stock_amount_table", "stock_amount_label", 1);
+}
+
+void
+PageStockAmount::prepare (bool input_new_balance, const std::string prev_balance)
+{
+    gtk_label_set_text_with_mnemonic
+        (GTK_LABEL (m_amount_label),
+         input_new_balance ? _("Ne_w Balance") : _("_Shares"));
+    gtk_label_set_text
+        (GTK_LABEL (m_next_amount_label),
+         input_new_balance ? _("Ratio") : _("Next Balance"));
+    gtk_label_set_text (GTK_LABEL (m_title),
+         input_new_balance ?
+         _("Enter the new balance of shares after the stock split.") :
+         _("Enter the number of shares you gained or lost in the transaction."));
+    gtk_label_set_text (GTK_LABEL (m_prev_amount), prev_balance.c_str());
 }
 
 static void
-controller_stock_amount (GtkWidget *widget, StockAssistantController* info)
+page_stock_amount_changed_cb(GtkWidget *widget, StockAssistantModel *model)
 {
-    g_return_if_fail (info && info->model->m_txn_type);
-    StringVec errors;
-    auto amount = info->model->m_stock_entry->amount();
-    controller_gae (widget, &amount);
-    info->model->m_stock_entry->set_amount(amount, errors);
-    auto amount_str{info->model->m_stock_entry->print_amount(amount)};
-    if (amount_str)
-        info->view->set_stock_amount (amount_str);
+    auto me = static_cast<PageStockAmount*>(g_object_get_data (G_OBJECT (widget), "owner"));
+    model->m_stock_entry->set_amount(me->m_amount.get(), model->m_errors);
+    me->set_stock_amount (model->get_new_amount_str());
 }
 
+void
+PageStockAmount::connect(StockAssistantModel *model)
+{
+    m_amount.connect(G_CALLBACK (page_stock_amount_changed_cb), model);
+    m_amount.set_owner(static_cast<gpointer>(this));
+}
+
+void
+PageStockAmount::set_stock_amount (std::string new_amount_str)
+{
+    gtk_label_set_text (GTK_LABEL(m_next_amount), new_amount_str.c_str());
+}
+
+struct PageStockValue
+{
+    // stock value page
+    GtkWidget * m_page;
+    GncAmountEdit m_value;
+    GtkWidget * m_price;
+    GtkWidget * m_memo;
+    PageStockValue (GtkBuilder *builder, gnc_commodity *currency);
+    const char* get_memo ();
+    void connect (StockAssistantModel *model);
+    void set_price(const gchar *val);
+    void set_price (std::tuple<bool, gnc_numeric, const char*> price_tuple);
+};
+
 static void
-controller_stock_value (GtkWidget *widget, StockAssistantController* info)
+page_stock_value_changed_cb(GtkWidget *widget, StockAssistantModel *model)
+{
+    auto me = static_cast<PageStockValue*>(g_object_get_data (G_OBJECT (widget), "owner"));
+    auto value = me->m_value.get ();
+    model->m_stock_entry->set_value (value, "stocks", model->m_errors);
+    me->set_price (model->calculate_price());
+}
+
+PageStockValue::PageStockValue(GtkBuilder *builder, gnc_commodity *currency)
+    : m_page(get_widget(builder, "stock_value_page")),
+      m_value(builder, currency),
+      m_price(get_widget(builder, "stock_price_amount")),
+      m_memo(get_widget(builder, "stock_memo_entry"))
+{
+    m_value.attach(builder, "stock_value_table", "stock_value_label", 0);
+}
+
+void
+PageStockValue::connect(StockAssistantModel *model)
 {
-    g_return_if_fail (info && info->model->m_txn_type);
+    m_value.connect(G_CALLBACK (page_stock_value_changed_cb), model);
+    m_value.set_owner (static_cast<gpointer>(this));
+    g_signal_connect (m_memo, "changed", G_CALLBACK(text_entry_changed_cb), &model->m_stock_entry->m_memo);
+}
+
+const char *
+PageStockValue::get_memo()
+{
+    return gtk_entry_get_text(GTK_ENTRY (m_memo));
+}
 
-    auto value = info->model->m_stock_entry->m_value;
-    controller_gae (widget, &value);
-    info->model->m_stock_entry->set_value(value);
-    auto [has_price, price, price_str] = info->model->calculate_price ();
+void
+PageStockValue::set_price (const gchar *val)
+{
+    gtk_label_set_text(GTK_LABEL(this->m_price), val);
+};
+
+void
+PageStockValue::set_price (std::tuple<bool, gnc_numeric, const char*> price_tuple)
+{
+    auto [has_price, price, price_str] = price_tuple;
     // Translators: StockAssistant: N/A denotes stock price is not computable
-    info->view->set_price_value (has_price ? price_str : _("N/A"));
+    set_price(has_price ? price_str : _("N/A"));
+}
+
+struct PageCash
+{
+    // cash page
+    GtkWidget * m_page;
+    GncAccountSelector m_account;
+    GtkWidget * m_memo;
+    GncAmountEdit m_value;
+    PageCash (GtkBuilder *builder, gnc_commodity *currency);
+    void connect(Account **account, const char **memo, gnc_numeric *value);
+    const char* get_memo();
+};
+
+PageCash::PageCash(GtkBuilder *builder, gnc_commodity *currency)
+    : m_page(get_widget(builder, "cash_details_page")),
+      m_account(builder, {ACCT_TYPE_ASSET, ACCT_TYPE_BANK},
+                   currency),
+      m_memo(get_widget(builder, "cash_memo_entry")),
+      m_value(builder, currency)
+{
+    m_account.attach (builder, "cash_table", "cash_account_label", 0);
+    m_value.attach (builder, "cash_table", "cash_label", 1);
+}
+
+void
+PageCash::connect(Account **account, const char **memo, gnc_numeric *value)
+{
+    m_account.connect(account);
+    g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb), memo);
+    m_value.connect(value);
+}
+
+const char *
+PageCash::get_memo()
+{
+    return gtk_entry_get_text(GTK_ENTRY (m_memo));
+}
+
+struct PageFees
+{
+    // fees page
+    GtkWidget * m_page;
+    GtkWidget * m_capitalize;
+    GncAccountSelector m_account;
+    GtkWidget * m_memo;
+    GncAmountEdit m_value;
+    PageFees (GtkBuilder *builder, gnc_commodity *currency);
+    void connect(StockAssistantModel *model);
+    bool get_capitalize_fees ();
+    const char* get_memo();
+    void set_capitalize_fees (bool state);
+    void set_capitalize_fees (StockAssistantModel *model);
+    void set_account (Account *acct) { m_account.set(acct); }
+    void update_fees_acct_sensitive (bool sensitive);
+};
+
+PageFees::PageFees(GtkBuilder *builder, gnc_commodity *currency)
+    : m_page(get_widget(builder, "fees_details_page")),
+      m_capitalize(
+          get_widget(builder, "capitalize_fees_checkbutton")),
+      m_account(builder, {ACCT_TYPE_EXPENSE}, currency),
+      m_memo(get_widget(builder, "fees_memo_entry")),
+      m_value(builder, currency)
+{
+    m_account.attach (builder, "fees_table", "fees_account_label", 1);
+    m_value.attach(builder, "fees_table", "fees_label", 2);
+}
+
+bool
+PageFees::get_capitalize_fees()
+{
+    return gtk_toggle_button_get_active(
+        GTK_TOGGLE_BUTTON(m_capitalize));
+}
+
+const char *
+PageFees::get_memo()
+{
+    return gtk_entry_get_text(GTK_ENTRY (m_memo));
+}
+
+void
+PageFees::set_capitalize_fees(bool state)
+{
+    gtk_toggle_button_set_active(
+        GTK_TOGGLE_BUTTON(m_capitalize), state);
+}
+
+void
+PageFees::set_capitalize_fees(StockAssistantModel *model)
+{
+    auto fees_entry = dynamic_cast<StockTransactionFeesEntry*>(model->m_fees_entry.get());
+    set_capitalize_fees (fees_entry->m_capitalize);
+}
+
+void
+PageFees::update_fees_acct_sensitive(bool sensitive)
+{
+    gtk_widget_set_sensitive(m_account.m_selector, sensitive);
 }
 
 static void
-controller_capitalize_fees (GtkWidget *widget, StockAssistantController* info)
+capitalize_fees_toggled_cb (GtkWidget *widget, StockAssistantModel *model)
 {
-    g_return_if_fail (info && info->model->m_txn_type);
+    g_return_if_fail (model && model->m_txn_type);
+    auto me = static_cast<PageFees *>(g_object_get_data (G_OBJECT (widget), "owner"));
+    g_return_if_fail (me);
+    bool cap =  me->get_capitalize_fees();
+    model->m_fees_entry->set_capitalize(cap);
+    me->update_fees_acct_sensitive (!cap);
+}
 
-    bool cap =  info->view->get_capitalize_fees();
-    info->model->m_fees_entry->set_capitalize(cap);
-    info->view->update_fees_acct_sensitive (!cap);
+void
+PageFees::connect(StockAssistantModel *model)
+{
+    m_account.connect(&model->m_fees_entry->m_account);
+    g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb),  &model->m_fees_entry->m_memo);
+    m_value.connect(&model->m_fees_entry->m_value);
+    g_object_set_data(G_OBJECT (m_capitalize), "owner", this);
+    g_signal_connect (m_capitalize, "toggled", G_CALLBACK (capitalize_fees_toggled_cb), model);
+}
+
+struct PageDividend
+{
+    // dividend page
+    GtkWidget *m_page;
+    GncAccountSelector m_account;
+    GtkWidget *m_memo;
+    GncAmountEdit m_value;
+    PageDividend (GtkBuilder *builder, gnc_commodity *currency);
+    void connect(Account **account, const char **memo, gnc_numeric *value);
+    const char* get_memo();
+};
+
+PageDividend::PageDividend(GtkBuilder *builder, gnc_commodity *currency)
+    : m_page(get_widget(builder, "dividend_details_page")),
+      m_account(builder, {ACCT_TYPE_INCOME}, currency),
+      m_memo(get_widget(builder, "dividend_memo_entry")),
+      m_value(builder, currency)
+{
+    m_account.attach(builder, "dividend_table", "dividend_account_label", 0);
+    m_value.attach(builder, "dividend_table", "dividend_label", 1);
+}
+
+
+void
+PageDividend::connect(Account **account, const char **memo, gnc_numeric *value)
+{
+    m_account.connect(account);
+    g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb), memo);
+    m_value.connect(value);
+}
+
+const char *
+PageDividend::get_memo()
+{
+    return gtk_entry_get_text(GTK_ENTRY (m_memo));
+}
+
+struct PageCapGain
+{
+    // capgains page
+    GtkWidget * m_page;
+    GncAccountSelector m_account;
+    GtkWidget * m_memo;
+    GncAmountEdit m_value;
+    PageCapGain (GtkBuilder *builder, gnc_commodity *currency);
+    void connect(Account **account, const char **memo, gnc_numeric *value);
+    const char* get_memo();
+};
+
+PageCapGain::PageCapGain (GtkBuilder *builder, gnc_commodity *currency) :
+    m_page (get_widget (builder, "capgains_details_page")),
+    m_account (builder, { ACCT_TYPE_INCOME }, currency),
+    m_memo (get_widget (builder, "capgains_memo_entry")),
+    m_value (builder, currency)
+{
+    m_account.attach(builder, "capgains_table", "capgains_account_label", 0);
+    m_value.attach(builder, "capgains_table", "capgains_label", 1);
+}
+
+const char *
+PageCapGain::get_memo()
+{
+    return gtk_entry_get_text(GTK_ENTRY (m_memo));
+}
+
+
+void
+PageCapGain::connect(Account **account, const char **memo, gnc_numeric *value)
+{
+    m_account.connect(account);
+    g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb), memo);
+    m_value.connect(value);
+}
+
+struct GncFinishTreeview
+{
+    GtkWidget *m_treeview;
+    GncFinishTreeview(GtkBuilder *builder);
+    void set_tooltip_column(int);
+};
+
+GncFinishTreeview::GncFinishTreeview (GtkBuilder *builder) :
+    m_treeview{get_widget (builder, "transaction_view")}
+{
+    auto view = GTK_TREE_VIEW (m_treeview);
+    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,
+                                     G_TYPE_STRING, G_TYPE_STRING);
+    gtk_tree_view_set_model(view, GTK_TREE_MODEL(store));
+    gtk_tree_selection_set_mode (gtk_tree_view_get_selection (view),
+                                     GTK_SELECTION_NONE);
+    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();
+    g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, nullptr);
+    column = gtk_tree_view_column_new_with_attributes
+        (_("Memo"), renderer, "text", SPLIT_COL_MEMO, nullptr);
+    gtk_tree_view_column_set_expand (column, true);
+    gtk_tree_view_append_column(view, column);
+
+    renderer = gtk_cell_renderer_text_new();
+    gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
+    gtk_cell_renderer_set_padding (renderer, 5, 0);
+    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();
+    gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
+    gtk_cell_renderer_set_padding (renderer, 5, 0);
+    column = gtk_tree_view_column_new_with_attributes
+        (_("Credit"), renderer, "text", SPLIT_COL_CREDIT, nullptr);
+    gtk_tree_view_append_column(view, column);
+
+    renderer = gtk_cell_renderer_text_new();
+    gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
+    gtk_cell_renderer_set_padding (renderer, 5, 0);
+    column = gtk_tree_view_column_new_with_attributes
+        (_("Units"), renderer,
+         "text", SPLIT_COL_UNITS,
+         "foreground", SPLIT_COL_UNITS_COLOR,
+         nullptr);
+    gtk_tree_view_append_column(view, column);
+}
+
+void
+GncFinishTreeview::set_tooltip_column(int column)
+{
+    gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(m_treeview), column);
+}
+
+struct PageFinish
+{
+    // finish page
+    GtkWidget * m_page;
+    GncFinishTreeview m_view;
+    GtkWidget * m_summary;
+    PageFinish (GtkBuilder *builder);
+    void prepare (GtkWidget *window, StockAssistantModel *model);
+};
+
+PageFinish::PageFinish (GtkBuilder *builder) :
+    m_page (get_widget (builder, "finish_page")), m_view (builder),
+    m_summary (get_widget (builder, "finish_summary")) {}
+
+
+void
+PageFinish::prepare (GtkWidget *window, StockAssistantModel *model)
+{
+    auto [success, summary, list_of_splits] = model->generate_list_of_splits ();
+    auto gtv = GTK_TREE_VIEW(m_view.m_treeview);
+    auto list = GTK_LIST_STORE(gtk_tree_view_get_model(gtv));
+    gtk_list_store_clear(list);
+    for (const auto &line : list_of_splits) {
+        GtkTreeIter iter;
+        auto tooltip = g_markup_escape_text(line.m_entry->m_memo, -1);
+        gtk_list_store_append(list, &iter);
+        gtk_list_store_set(
+            list, &iter, SPLIT_COL_ACCOUNT,
+            xaccAccountGetName(line.m_entry->m_account), SPLIT_COL_MEMO,
+            line.m_entry->m_memo, SPLIT_COL_TOOLTIP, tooltip, SPLIT_COL_DEBIT,
+            line.m_entry->m_debit_side ? line.m_entry->print_value(model->m_curr_pinfo) : nullptr,
+            SPLIT_COL_CREDIT,
+            line.m_entry->m_debit_side ? nullptr : line.m_entry->print_value(model->m_curr_pinfo),
+            SPLIT_COL_UNITS, line.m_entry->print_amount(line.m_entry->amount()),
+            SPLIT_COL_UNITS_COLOR, line.m_units_in_red ? "red" : nullptr, -1);
+        g_free(tooltip);
+    }
+    gtk_label_set_text(GTK_LABEL(m_summary), summary.c_str());
+    gtk_assistant_set_page_complete(GTK_ASSISTANT(window), m_page, success);
+ }
+
+struct StockAssistantView {
+    GtkWidget * m_window;
+
+    PageTransType m_type_page;
+    PageTransDeets m_deets_page;
+    PageStockAmount m_stock_amount_page;
+    PageStockValue m_stock_value_page;
+    PageCash m_cash_page;
+    PageFees m_fees_page;
+    PageDividend m_dividend_page;
+    PageCapGain m_capgain_page;
+    PageFinish m_finish_page;
+
+    StockAssistantView(GtkBuilder *builder, gnc_commodity *stock_commodity,
+                       gnc_commodity *currency, GtkWidget *parent);
+    ~StockAssistantView();
+    void set_focus (GtkWidget *widget) { gtk_widget_grab_focus (widget); }
+    void set_focus_gae (GtkWidget *gae) { set_focus (GTK_WIDGET (gnc_amount_edit_gtk_entry (GNC_AMOUNT_EDIT (gae)))); }
+
+};
+
+StockAssistantView::StockAssistantView (GtkBuilder *builder, gnc_commodity *stock_commodity,
+                                        gnc_commodity *currency, GtkWidget *parent) :
+    m_window (get_widget (builder, "stock_transaction_assistant")), m_type_page(builder), m_deets_page(builder),
+    m_stock_amount_page (builder, currency), m_stock_value_page (builder, currency), m_cash_page (builder, currency),
+    m_fees_page (builder, currency), m_dividend_page (builder, currency), m_capgain_page (builder, currency),
+    m_finish_page (builder)
+{
+    // Set the name for this assistant so it can be easily manipulated with css
+    gtk_widget_set_name (GTK_WIDGET(m_window), "gnc-id-assistant-stock-transaction");
+    m_finish_page.m_view.set_tooltip_column(SPLIT_COL_TOOLTIP);
+    gtk_window_set_transient_for (GTK_WINDOW (m_window), GTK_WINDOW(parent));
+    gnc_window_adjust_for_screen (GTK_WINDOW(m_window));
+    gnc_restore_window_size (GNC_PREFS_GROUP, GTK_WINDOW(m_window),
+                             GTK_WINDOW(parent));
+    gtk_widget_show_all (m_window);
+    DEBUG ("StockAssistantView constructor\n");
+};
+
+StockAssistantView::~StockAssistantView()
+{
+    gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(m_window));
+    DEBUG ("StockAssistantView destructor\n");
+};
+
+static void connect_signals (gpointer, GtkBuilder*);
+
+struct StockAssistantController
+{
+    std::unique_ptr<StockAssistantModel> model;
+    std::unique_ptr<StockAssistantView> view;
+    StockAssistantController (GtkWidget *parent, Account* acct)
+        : model (std::make_unique<StockAssistantModel>(acct))
+    {
+        auto builder = gtk_builder_new();
+        gnc_builder_add_from_file (builder, "assistant-stock-transaction.glade",
+                                   "stock_transaction_assistant");
+        this->view = std::make_unique<StockAssistantView>
+            (builder, xaccAccountGetCommodity (acct), this->model->m_currency, parent);
+        connect_signals (this, builder);
+        g_object_unref (builder);
+        DEBUG ("StockAssistantController constructor\n");
+    };
+    ~StockAssistantController (){ DEBUG ("StockAssistantController destructor\n"); };
+};
+
+/******* implementations ***********************************************/
+static void
+stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data)
+{
+    auto info = static_cast<StockAssistantController*>(user_data);
+    gnc_unregister_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info);
+    delete info;
 }
 
 void
@@ -1495,55 +1863,57 @@ stock_assistant_prepare_cb (GtkAssistant  *assistant, GtkWidget *page,
     case PAGE_TRANSACTION_TYPE:
         if (!model->maybe_reset_txn_types())
             break;
-        view->set_transaction_types (model->m_txn_types.value());
-        controller_transaction_type (view->transaction_type_combo, info);
-        view->set_focus (view->transaction_type_combo);
+        view->m_type_page.prepare(model);
+        view->m_type_page.set_focus();
+        view->m_fees_page.set_capitalize_fees(model);
         break;
     case PAGE_TRANSACTION_DETAILS:
-        controller_gde (view->transaction_date, &model->m_transaction_date);
-        controller_gtk_entry (view->transaction_description, &model->m_transaction_description);
-        view->set_focus (view->transaction_description);
+        model->m_transaction_date = view->m_deets_page.get_date_time();
+        model->m_transaction_description = view->m_deets_page.get_description();
+        view->m_deets_page.set_focus ();
         break;
     case PAGE_STOCK_AMOUNT:
-        view->prepare_stock_amount_page (model->m_input_new_balance,
-                                         model->get_stock_balance_str());
-        controller_stock_amount (view->stock_amount_edit, info);
-        view->set_focus_gae (view->stock_amount_edit);
+        view->m_stock_amount_page.prepare (model->m_input_new_balance,
+                                           model->get_stock_balance_str());
+
+        info->model->m_stock_entry->set_amount(view->m_stock_amount_page.get_stock_amount(), model->m_errors);
+        view->m_stock_amount_page.set_stock_amount(info->model->get_new_amount_str());
+        view->m_stock_amount_page.m_amount.set_focus();
         break;
     case PAGE_STOCK_VALUE:
-        controller_gtk_entry (view->stock_memo_edit, &model->m_stock_entry->m_memo);
-        controller_stock_value (view->stock_value_edit, info);
-        view->set_focus_gae (view->stock_value_edit);
+        model->m_stock_entry->m_memo = view->m_stock_value_page.get_memo();
+        model->m_stock_entry->set_value(view->m_stock_value_page.m_value.get(), "stock", model->m_errors);
+        view->m_stock_value_page.set_price(model->calculate_price());
+        view->m_stock_value_page.m_value.set_focus();
         break;
     case PAGE_CASH:
-        controller_gtk_entry (view->cash_memo_edit, &model->m_cash_entry->m_memo);
-        controller_gae (view->cash_value, &model->m_cash_entry->m_value);
-        controller_gas (view->cash_account, &model->m_cash_entry->m_account);
-        view->set_focus_gae (view->cash_value);
+        model->m_cash_entry->m_memo = view->m_cash_page.get_memo();
+        model->m_cash_entry->set_value (view->m_cash_page.m_value.get(), "cash", model->m_errors);
+        model->m_cash_entry->m_account = view->m_cash_page.m_account.get();
+        view->m_cash_page.m_value.set_focus();
         break;
     case PAGE_FEES:
-        controller_capitalize_fees (view->capitalize_fees_checkbox, info);
-        controller_gtk_entry (view->fees_memo_edit, &model->m_fees_entry->m_memo);
-        controller_gae (view->fees_value, &model->m_fees_entry->m_value);
-        controller_gas (view->fees_account, &model->m_fees_entry->m_account);
-        view->set_focus_gae (view->fees_value);
+        view->m_fees_page.set_capitalize_fees (info);
+        model->m_fees_entry->m_memo = view->m_fees_page.get_memo();
+        model->m_fees_entry->set_value (view->m_fees_page.m_value.get(), "fees", model->m_errors);
+        model->m_fees_entry->m_account = view->m_fees_page.m_account.get();
+        view->m_fees_page.m_value.set_focus();
         break;
     case PAGE_DIVIDEND:
-        controller_gtk_entry (view->dividend_memo_edit, &model->m_dividend_entry->m_memo);
-        controller_gae (view->dividend_value, &model->m_dividend_entry->m_value);
-        controller_gas (view->dividend_account, &model->m_dividend_entry->m_account);
-        view->set_focus_gae (view->dividend_value);
+        model->m_dividend_entry->m_memo = view->m_dividend_page.get_memo();
+        model->m_dividend_entry->set_value (view->m_dividend_page.m_value.get(), "dividend", model->m_errors);
+        model->m_dividend_entry->m_account = view->m_dividend_page.m_account.get();
+        view->m_dividend_page.m_value.set_focus();
         break;
     case PAGE_CAPGAINS:
-        controller_gtk_entry (view->capgains_memo_edit, &model->m_capgains_entry->m_memo);
-        controller_gae (view->capgains_value, &model->m_capgains_entry->m_value);
-        controller_gas (view->capgains_account, &model->m_capgains_entry->m_account);
-        view->set_focus_gae (view->capgains_value);
+        model->m_capgains_entry->m_memo = view->m_capgain_page.get_memo();
+        model->m_capgains_entry->set_value(view->m_capgain_page.m_value.get(), "capgains", model->m_errors);
+        model->m_capgains_entry->m_account = view->m_capgain_page.m_account.get();
+        view->m_capgain_page.m_value.set_focus();
         break;
     case PAGE_FINISH:
     {
-        auto [success, summary, list_of_splits] = model->generate_list_of_splits ();
-        view->prepare_finish_page (success, summary, list_of_splits, model->m_curr_pinfo);
+        view->m_finish_page.prepare (view->m_window, model);
         break;
     }
     default:
@@ -1614,7 +1984,7 @@ static void
 close_handler (gpointer user_data)
 {
     auto info = static_cast<StockAssistantController*>(user_data);
-    gtk_widget_destroy (info->view->window);
+    gtk_widget_destroy (info->view->m_window);
 }
 
 static void connect_signals (gpointer data, GtkBuilder *builder)
@@ -1623,36 +1993,21 @@ static void connect_signals (gpointer data, GtkBuilder *builder)
     auto model = info->model.get();
     auto view = info->view.get();
 
-    struct SignalData { GtkWidget* widget; const char* signal; GCallback callback; gpointer data; };
-    std::vector<SignalData> signals =
-    {
-        { view->transaction_type_combo  , "changed"            , G_CALLBACK (controller_transaction_type)      , info },
-        { view->transaction_date        , "date_changed"       , G_CALLBACK (controller_gde)                   , &model->m_transaction_date },
-        { view->transaction_description , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_transaction_description },
-        { view->stock_amount_edit       , "changed"            , G_CALLBACK (controller_stock_amount)          , info },
-        { view->stock_value_edit        , "changed"            , G_CALLBACK (controller_stock_value)           , info },
-        { view->stock_memo_edit         , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_stock_entry->m_memo },
-        { view->cash_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_cash_entry->m_account },
-        { view->cash_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_cash_entry->m_memo },
-        { view->cash_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_cash_entry->m_value },
-        { view->capitalize_fees_checkbox, "toggled"            , G_CALLBACK (controller_capitalize_fees)       , info },
-        { view->fees_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_fees_entry->m_account },
-        { view->fees_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_fees_entry->m_memo },
-        { view->fees_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_fees_entry->m_value },
-        { view->dividend_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_dividend_entry->m_account },
-        { view->dividend_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_dividend_entry->m_memo },
-        { view->dividend_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_dividend_entry->m_value },
-        { view->capgains_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_capgains_entry->m_account },
-        { view->capgains_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_capgains_entry->m_memo },
-        { view->capgains_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_capgains_entry->m_value },
-        { view->window                  , "destroy"            , G_CALLBACK (stock_assistant_window_destroy_cb), info }
-    };
-    for (const auto& [widget, signal, callback, data] : signals)
-        g_signal_connect (widget, signal, callback, data);
-    gtk_assistant_set_forward_page_func (GTK_ASSISTANT(view->window),
+    view->m_type_page.connect(model);
+    view->m_deets_page.connect(&model->m_transaction_date, &model->m_transaction_description);
+    view->m_stock_amount_page.connect(model);
+    view->m_stock_value_page.connect(model);
+    view->m_cash_page.connect(&model->m_cash_entry->m_account,  &model->m_cash_entry->m_memo, &model->m_cash_entry->m_value);
+    view->m_fees_page.connect(model);
+    view->m_dividend_page.connect(&model->m_dividend_entry->m_account, &model->m_dividend_entry->m_memo, &model->m_dividend_entry->m_value);
+    view->m_capgain_page.connect(&model->m_capgains_entry->m_account, &model->m_capgains_entry->m_memo,  &model->m_capgains_entry->m_value);
+
+    g_signal_connect (view->m_window, "destroy", G_CALLBACK (stock_assistant_window_destroy_cb), info);
+
+    gtk_assistant_set_forward_page_func (GTK_ASSISTANT(view->m_window),
                                          (GtkAssistantPageFunc)forward_page_func,
                                          info, nullptr);
-    gtk_builder_connect_signals (builder, info);
+    gtk_builder_connect_signals (builder, info); //Stock Assistant View: cancel, close, prepare
 
     auto component_id = gnc_register_gui_component
         (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, refresh_handler, close_handler, info);

commit fdfa8353dda70abf12852fe4a2987c908074600e
Author: John Ralls <jralls at ceridwen.us>
Date:   Sun Jan 29 14:01:59 2023 -0800

    [stock-txn-asst] Extract StockTransactionEntry classes.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index c8c0736eb4..9492538a34 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -33,16 +33,19 @@
 #include <stdexcept>
 #include <sstream>
 
+#include "Account.h"
 #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-numeric.h"
 #include "gnc-prefs.h"
 #include "gnc-component-manager.h"
 #include "gnc-date-edit.h"
 #include "gnc-tree-view-account.h"
+#include "gnc-ui-util.h"
 
 static QofLogModule log_module = GNC_MOD_ASSISTANT;
 
@@ -95,13 +98,15 @@ enum split_cols
 enum class FieldMask : unsigned
 {
     DISABLED = 0,
-    ENABLED_DEBIT,
-    ENABLED_CREDIT,
-    ALLOW_ZERO = 4,
-    ALLOW_NEGATIVE = 8,
-    INPUT_NEW_BALANCE = 16,     // stock_amt only: instead of amount, get new balance
-    CAPITALIZE_DEFAULT = 32,    // fees only: capitalize by default into stock acct
-    CAPGAINS_IN_STOCK = 64,     // capg only: add a balancing split in stock acct
+    ENABLED_DEBIT        = 1,
+    ENABLED_CREDIT       = 1 << 1,
+    AMOUNT_DEBIT         = 1 << 2, // stock only
+    AMOUNT_CREDIT        = 1 << 3, // stock only
+    INPUT_NEW_BALANCE    = 1 << 4, // stock_amt only: instead of amount, get new balance
+    ALLOW_ZERO           = 1 << 5,
+    ALLOW_NEGATIVE       = 1 << 6,
+    CAPITALIZE_DEFAULT   = 1 << 7, // fees only: capitalize by default into stock acct
+    CAPGAINS_IN_STOCK    = 1 << 8, // capg only: add a balancing split in stock acct
 };
 
 FieldMask operator |(FieldMask lhs, FieldMask rhs)
@@ -124,7 +129,6 @@ FieldMask operator ^(FieldMask lhs, FieldMask rhs)
 struct TxnTypeInfo
 {
     FieldMask stock_amount;
-    FieldMask stock_value;
     FieldMask cash_value;
     FieldMask fees_value;
     FieldMask dividend_value;
@@ -140,8 +144,7 @@ using AccountVec = std::vector<Account*>;
 static const TxnTypeVec starting_types
 {
     {
-        FieldMask::ENABLED_DEBIT,          // stock_amt
-        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT,          // stock_amt
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -152,8 +155,7 @@ static const TxnTypeVec starting_types
         N_("Initial stock long purchase.")
     },
     {
-        FieldMask::ENABLED_CREDIT,         // stock_amt
-        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::ENABLED_CREDIT | FieldMask::AMOUNT_CREDIT,         // stock_amt
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -168,8 +170,7 @@ static const TxnTypeVec starting_types
 static const TxnTypeVec long_types
 {
     {
-        FieldMask::ENABLED_DEBIT,          // stock_amt
-        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT,          // stock_amt
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -180,8 +181,7 @@ static const TxnTypeVec long_types
         N_("Buying stock long.")
     },
     {
-        FieldMask::ENABLED_CREDIT,         // stock_amt
-        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::ENABLED_CREDIT | FieldMask::AMOUNT_CREDIT,         // stock_amt
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -193,7 +193,6 @@ static const TxnTypeVec long_types
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::ENABLED_CREDIT,         // dividend_amt
@@ -205,8 +204,7 @@ static const TxnTypeVec long_types
 reinvested must be subsequently recorded as a regular stock purchase.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::ENABLED_CREDIT,         // stock_amt
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -217,8 +215,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Company returns capital, reducing the cost basis without affecting # units.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::ENABLED_CREDIT,         // stock_amt
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::ENABLED_DEBIT,          // dividend_amt
@@ -229,8 +226,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Company returns capital, reducing the cost basis without affecting # units. A distribution previously recorded as a dividend is reclassified to return of capital, often due to end-of-year tax information.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::ENABLED_DEBIT,          // stock_amt
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::ENABLED_CREDIT,         // dividend_amt
@@ -241,8 +237,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Company issues a notional distribution, which is recorded as dividend income and increases the cost basis without affecting # units.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::ENABLED_DEBIT,          // stock_amt
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -253,8 +248,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Company issues a notional distribution, which is recorded as capital gain and increases the cost basis without affecting # units.")
     },
     {
-        FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
-        FieldMask::DISABLED,               // stock_val
+        FieldMask::DISABLED | FieldMask::AMOUNT_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -265,8 +259,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Company issues additional units, thereby reducing the stock price by a divisor, while keeping the total monetary value of the overall investment constant.")
     },
     {
-        FieldMask::ENABLED_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
-        FieldMask::DISABLED,               // stock_val
+        FieldMask::DISABLED | FieldMask::AMOUNT_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -284,8 +277,7 @@ record the reverse split.")
 static const TxnTypeVec short_types
 {
     {
-        FieldMask::ENABLED_CREDIT,         // stock_amt
-        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::ENABLED_CREDIT | FieldMask::AMOUNT_CREDIT,         // stock_amt
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -296,8 +288,7 @@ static const TxnTypeVec short_types
         N_("Selling stock short.")
     },
     {
-        FieldMask::ENABLED_DEBIT,          // stock_amt
-        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT,          // stock_amt
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -309,7 +300,6 @@ static const TxnTypeVec short_types
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::ENABLED_DEBIT,          // dividend_amt
@@ -320,8 +310,7 @@ static const TxnTypeVec short_types
         N_("Company issues dividends, and the short stock holder must make a compensatory payment for the dividend.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::ENABLED_DEBIT,          // stock_amt
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -332,8 +321,7 @@ static const TxnTypeVec short_types
         N_("Company returns capital, and the short stock holder must make a compensatory payment for the returned capital. This reduces the cost basis (less negative, towards 0.00 value) without affecting # units.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::ENABLED_DEBIT,          // stock_amt
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::ENABLED_CREDIT,         // dividend_amt
@@ -345,8 +333,7 @@ static const TxnTypeVec short_types
         N_("Company returns capital, and the short stock holder must make a compensatory payment for the returned capital. This reduces the cost basis (less negative, towards 0.00 value) without affecting # units. A distribution previously recorded as a compensatory dividend is reclassified to compensatory return of capital, often due to end-of-year tax information.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::ENABLED_CREDIT,         // stock_amt
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::ENABLED_DEBIT,          // dividend_amt
@@ -358,8 +345,7 @@ static const TxnTypeVec short_types
         N_("Company issues a notional distribution, and the short stock holder must make a compensatory payment for the notional distribution. This is recorded as a loss/negative dividend income amount, and increases the cost basis (more negative, away from 0.00 value) without affecting # units.")
     },
     {
-        FieldMask::DISABLED,               // stock_amt
-        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::ENABLED_CREDIT,         // stock_amt
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -371,8 +357,7 @@ static const TxnTypeVec short_types
         N_("Company issues a notional distribution, and the short stock holder must make a compensatory payment for the notional distribution. This is recorded as a capital loss amount, and increases the cost basis (more negative, away from 0.00 value) without affecting # units.")
     },
     {
-        FieldMask::ENABLED_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
-        FieldMask::DISABLED,               // stock_val
+        FieldMask::DISABLED | FieldMask::AMOUNT_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -383,8 +368,7 @@ static const TxnTypeVec short_types
         N_("Company issues additional units, thereby reducing the stock price by a divisor, while keeping the total monetary value of the overall investment constant.")
     },
     {
-        FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
-        FieldMask::DISABLED,               // stock_val
+        FieldMask::DISABLED | FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
@@ -400,138 +384,260 @@ then record the reverse split.")
     }
 };
 
-struct StockTransactionSplitInfo
+
+struct StockTransactionEntry
 {
+    bool m_enabled;
     bool m_debit_side;
-    std::string m_account_str;
-    std::string m_memo_str;
-    std::string m_action_str;
-    std::string m_value_str;
-    std::string m_units_str;
-    bool m_units_in_red = false;
-    Account* m_account = nullptr;
-    gnc_numeric m_value_numeric = gnc_numeric_create (1, 0); // invalid gnc_numerics
-    gnc_numeric m_units_numeric = gnc_numeric_create (1, 0);
+    bool m_allow_zero;
+    bool m_allow_negative;
+    Account *m_account;
+    gnc_numeric m_value;
+    const char* m_memo;
+
+    StockTransactionEntry() :
+        m_enabled{false}, m_debit_side{false}, m_allow_zero{false},  m_account{nullptr},
+        m_value{gnc_numeric_error(GNC_ERROR_ARG)}, m_memo{nullptr} {}
+    StockTransactionEntry(bool debit_side, bool allow_zero, bool allow_negative, Account* account, gnc_numeric value) :
+        m_enabled{false}, m_debit_side{debit_side}, m_allow_zero{allow_zero}, m_allow_negative{allow_negative},
+        m_account{account}, m_value{value}, m_memo{nullptr} {}
+    virtual ~StockTransactionEntry() = default;
+
+    virtual void set_fieldmask(FieldMask mask);
+    virtual void set_capitalize(bool capitalize) {}
+    virtual void set_value(gnc_numeric amount); //, const char* page, StringVec& errors);
+    virtual gnc_numeric amount() { return m_value; }
+    virtual void set_amount(gnc_numeric, StringVec&) {}
+    virtual void create_split(Transaction* trans, const char* action,
+                              AccountVec& commits);
+    virtual const char* print_value(GNCPrintAmountInfo info);
+    virtual const char* print_amount(gnc_numeric amt);
+    virtual gnc_numeric calculate_price(bool) { return gnc_numeric_error(GNC_ERROR_ARG); }
+};
 
-    static const char* s_missing_str;
+using StockTransactionEntryPtr = std::unique_ptr<StockTransactionEntry>;
 
-    StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo constructor\n"); };
-    StockTransactionSplitInfo (Account *acct, gnc_numeric val)
-        : m_account_str{xaccAccountGetName (acct)} , m_account{acct} , m_value_numeric{val}
-    { DEBUG ("StockTransactionSplitInfo constructor\n"); }
-    StockTransactionSplitInfo(gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
-                              FieldMask splitfield, Account *acct, const char *memo,
-                              gnc_numeric amount, const char* page, GNCPrintAmountInfo curr_pinfo);
-    ~StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo destructor\n"); }
-    void create_split(Transaction *trans, AccountVec &account_commits);
-};
+void
+StockTransactionEntry::set_fieldmask(FieldMask mask)
+{
+    m_enabled = mask != FieldMask::DISABLED;
+    m_debit_side = mask & FieldMask::ENABLED_DEBIT;
+    m_allow_zero = mask & FieldMask::ALLOW_ZERO;
+    m_allow_negative = mask & FieldMask::ALLOW_NEGATIVE;
+}
 
-// Translators: (missing) denotes that the amount or account is
-// not provided, or incorrect, in the Stock Transaction Assistant.
-const char* StockTransactionSplitInfo::s_missing_str = N_("(missing)");
-
-StockTransactionSplitInfo::StockTransactionSplitInfo (gnc_numeric& debit, gnc_numeric& credit,
-                                                      StringVec& errors, FieldMask splitfield,
-                                                      Account *acct, const char *memo,
-                                                      gnc_numeric amount, const char* page,
-                                                      GNCPrintAmountInfo curr_pinfo) :
-    m_debit_side{splitfield & FieldMask::ENABLED_DEBIT},
-    m_account_str{acct ? xaccAccountGetName (acct) : ""},
-    m_memo_str{memo ? memo : ""},
-    m_action_str{page ? page : ""},
-    m_value_str{gnc_numeric_check(amount) ? "" : xaccPrintAmount (amount, curr_pinfo)},
-    m_account{acct}
 
+void
+StockTransactionEntry::set_value(gnc_numeric amount) //, const char* page, StringVec& errors)
 {
+#if 0    //FIXME, logging is broken for want of the page name
     auto add_error = [&errors](const char* format_str, const char* arg)
     {
-        gchar *buf = g_strdup_printf (_(format_str),
+        char *buf = g_strdup_printf (_(format_str),
                                       g_dpgettext2 (nullptr, "Stock Assistant: Page name", arg));
         errors.emplace_back (buf);
         g_free (buf);
     };
 
-    DEBUG ("page=%s, amount=%s", page, gnc_num_dbg_to_string (amount));
-    if (memo)
-        m_memo_str = memo;
-    m_debit_side = (splitfield & FieldMask::ENABLED_DEBIT);
-    if (page)
-        m_action_str = page;
 
     if (gnc_numeric_check (amount))
     {
-        if (splitfield & FieldMask::ALLOW_ZERO)
-            // m_value_numeric contains an invalid gnc_numeric
-            m_value_str = "";
-        else
-        {
-            add_error (N_("Amount for %s is missing."), page);
-            m_value_str = _(s_missing_str);
-        }
+        add_error (N_("Amount for %s is missing."), page);
+        return;
     }
-    else
+#endif
+    if (gnc_numeric_negative_p (amount))
     {
-        if (!(splitfield & FieldMask::ALLOW_NEGATIVE))
-        {
-            if ((splitfield & FieldMask::ALLOW_ZERO) && gnc_numeric_negative_p (amount))
-                add_error (N_("Amount for %s must not be negative."), page);
-            else if (!(splitfield & FieldMask::ALLOW_ZERO) && !gnc_numeric_positive_p (amount))
-                add_error (N_("Amount for %s must be positive."), page);
-        }
-        if (gnc_numeric_negative_p (amount))
+        if (m_allow_negative)
         {
-            amount = gnc_numeric_neg (amount);
+            m_value = gnc_numeric_neg(amount);
             m_debit_side = !m_debit_side;
         }
-        if (m_debit_side)
-            debit = gnc_numeric_add_fixed (debit, amount);
+    }
+#if 0
         else
-            credit = gnc_numeric_add_fixed (credit, amount);
-        m_units_numeric = m_debit_side ? amount : gnc_numeric_neg (amount);
-        m_value_numeric = m_debit_side ? amount : gnc_numeric_neg (amount);
-        m_value_str = xaccPrintAmount (amount, curr_pinfo);
+        {
+            if (m_allow_zero)
+                add_error (N_("Amount for %s must not be negative."), page);
+        }
     }
 
-    if (acct)
-    {
-        m_account = acct;
-        m_account_str = xaccAccountGetName (acct);
-    }
-    else if ((splitfield & FieldMask::ALLOW_ZERO) &&
-             (gnc_numeric_check (amount) || gnc_numeric_zero_p (amount)))
-        m_account_str = "";
-    else
+    if (!m_allow_zero && !gnc_numeric_positive_p (amount))
     {
-        add_error (N_("Account for %s is missing."), page);
-        m_account_str = _(s_missing_str);
+        add_error (N_("Amount for %s must be positive."), page);
+        return;
     }
+#endif
+    m_value = m_debit_side ? amount : gnc_numeric_neg (amount);
+}
+
+const char *
+StockTransactionEntry::print_value(GNCPrintAmountInfo pinfo)
+{
+    if (gnc_numeric_check(m_value) ||
+        (gnc_numeric_zero_p(m_value) && !m_allow_zero))
+        return _("missing");
+    return xaccPrintAmount(m_value, pinfo);
+}
+
+const char *
+StockTransactionEntry::print_amount(gnc_numeric amt)
+{
+    if (!m_account || gnc_numeric_check(amt))
+        return nullptr;
+    auto commodity{xaccAccountGetCommodity(m_account)};
+    auto pinfo{gnc_commodity_print_info(commodity, TRUE)};
+    return xaccPrintAmount(amt, pinfo);
 }
 
 void
-StockTransactionSplitInfo::create_split(Transaction *trans, AccountVec &account_commits) {
+StockTransactionEntry::create_split(Transaction *trans, const char* action,
+                                    AccountVec &account_commits) {
   g_return_if_fail(trans);
-  if (!m_account || gnc_numeric_check(m_value_numeric) ||
-      gnc_numeric_check(m_units_numeric))
+  if (!m_account || gnc_numeric_check(m_value))
     return;
   auto split = xaccMallocSplit(qof_instance_get_book(trans));
   xaccSplitSetParent(split, trans);
   xaccAccountBeginEdit(m_account);
-  account_commits.emplace_back(m_account);
+  account_commits.push_back(m_account);
   xaccSplitSetAccount(split, m_account);
-  xaccSplitSetMemo(split, m_memo_str.c_str());
-  xaccSplitSetValue(split, m_value_numeric);
-  xaccSplitSetAmount(split, m_units_numeric);
-  DEBUG("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
-        m_action_str.c_str(), m_account_str.c_str(),
-        gnc_num_dbg_to_string(m_value_numeric),
-        gnc_num_dbg_to_string(m_units_numeric),
+  xaccSplitSetMemo(split, m_memo);
+  xaccSplitSetValue(split, m_debit_side ? m_value : gnc_numeric_neg(m_value));
+  xaccSplitSetAmount(split, amount());
+  PINFO("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
+        action, xaccAccountGetName (m_account),
+        gnc_num_dbg_to_string(m_value),
+        gnc_num_dbg_to_string(amount()),
         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",
-                                  m_action_str.c_str()));
+                                  action));
+}
+
+struct StockTransactionStockEntry : public StockTransactionEntry
+{
+    bool m_amount_enabled;
+    gnc_numeric m_amount;
+
+    StockTransactionStockEntry() :
+        StockTransactionEntry{},
+        m_amount{gnc_numeric_error(GNC_ERROR_ARG)} {
+        PINFO("Stock Entry");
+    }
+    void set_fieldmask(FieldMask mask) override;
+    gnc_numeric amount() override { return m_amount; }
+    void set_amount(gnc_numeric amount, StringVec& errors) override;
+    gnc_numeric calculate_price(bool new_balance) override;
+};
+
+void
+StockTransactionStockEntry::set_fieldmask(FieldMask mask)
+{
+//FIXME, need to combine stock-value and stock-amt fieldmasks
+    StockTransactionEntry::set_fieldmask(mask);
+    m_enabled = mask & (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT |
+                        FieldMask::AMOUNT_CREDIT | FieldMask::AMOUNT_DEBIT);
+    m_amount_enabled = mask & (FieldMask::AMOUNT_CREDIT | FieldMask::AMOUNT_DEBIT);
+    m_debit_side = mask & (FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT);
+}
+
+void
+StockTransactionStockEntry::set_amount(gnc_numeric amount, StringVec& errors)
+{
+     if (!m_amount_enabled)
+        return;
+
+    if (gnc_numeric_check(amount) || gnc_numeric_zero_p(amount))
+    {
+        const char* err{_("Amount for stock value is missing.")};
+
+        errors.emplace_back(err);
+        return;
+    }
+
+    bool neg{gnc_numeric_negative_p(amount) == TRUE};
+
+    if ((m_debit_side && !neg) || (!m_debit_side && neg))
+        m_amount = amount;
+    else
+        m_amount = gnc_numeric_neg(amount);
+    PINFO("%s set amount %s", m_memo, print_amount(amount));
+}
+
+gnc_numeric
+StockTransactionStockEntry::calculate_price(bool new_balance)
+{
+    if (new_balance ||
+        !m_amount_enabled || gnc_numeric_check(m_amount) ||
+        !m_enabled || gnc_numeric_check(m_value) ||
+        gnc_numeric_zero_p(m_amount) || gnc_numeric_zero_p(m_value))
+        return gnc_numeric_error(GNC_ERROR_ARG);
+
+    return gnc_numeric_div(m_value, m_amount,
+                           GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
+}
+
+struct StockTransactionsStockCapGainsEntry : public StockTransactionEntry
+{
+    StockTransactionsStockCapGainsEntry(const StockTransactionEntry* cg_entry,
+                                        const StockTransactionEntry* stk_entry);
+    gnc_numeric amount() { return gnc_numeric_zero(); }
+};
+
+StockTransactionsStockCapGainsEntry::StockTransactionsStockCapGainsEntry(const StockTransactionEntry* cg_entry,
+                                                                         const StockTransactionEntry* stk_entry) :
+    StockTransactionEntry(!cg_entry->m_debit_side, cg_entry->m_allow_zero, cg_entry->m_allow_negative,
+                          stk_entry->m_account, cg_entry->m_value) {}
+
+struct StockTransactionFeesEntry : public StockTransactionEntry
+{
+    bool m_capitalize;
+
+    StockTransactionFeesEntry() :
+        StockTransactionEntry{},
+        m_capitalize{false} {}
+    void set_fieldmask(FieldMask mask) override;
+    void set_capitalize(bool capitalize) override { m_capitalize = capitalize; }
+    void create_split(Transaction *trans, const char *action,
+                      AccountVec &commits) override;
+};
+
+void
+StockTransactionFeesEntry::set_fieldmask(FieldMask mask)
+{
+    StockTransactionEntry::set_fieldmask(mask);
+    m_capitalize = mask & FieldMask::CAPITALIZE_DEFAULT;
 }
 
+void
+StockTransactionFeesEntry::create_split(Transaction* trans, const char* action,
+                              AccountVec& commits)
+{
+    if (!m_capitalize)
+        StockTransactionEntry::create_split(trans, action, commits);
+}
+
+struct StockTransactionSplitInfo
+{
+    StockTransactionEntry* m_entry;
+    bool m_units_in_red = false;
+    const char* m_action;
+    static const char* s_missing_str;
+
+    StockTransactionSplitInfo(StockTransactionEntry* entry, const char* page) :
+        m_entry{entry}, m_action{page}
+    {
+        DEBUG ("StockTransactionSplitInfo constructor\n");
+    }
+
+    ~StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo destructor\n"); }
+};
+
+// Translators: (missing) denotes that the amount or account is
+// not provided, or incorrect, in the Stock Transaction Assistant.
+const char *StockTransactionSplitInfo::s_missing_str = N_("(missing)");
+
 using SplitInfoVec = std::vector<StockTransactionSplitInfo>;
 
 struct StockAssistantModel
@@ -540,53 +646,35 @@ struct StockAssistantModel
     gnc_commodity* m_currency;
 
     GNCPrintAmountInfo m_curr_pinfo;
-    GNCPrintAmountInfo m_price_pinfo;
-    GNCPrintAmountInfo m_stock_pinfo;
 
     time64 m_transaction_date;
+    const char* m_transaction_description;
     std::optional<TxnTypeVec> m_txn_types;
 
     std::optional<TxnTypeInfo> m_txn_type;
 
-    const char* m_transaction_description;
     gnc_numeric m_balance_at_date = gnc_numeric_create (1, 0);
 
     bool m_input_new_balance;
-    bool m_stock_amount_enabled;
-    gnc_numeric m_stock_amount = gnc_numeric_create (1, 0);
-
-    bool m_stock_value_enabled;
-    gnc_numeric m_stock_value = gnc_numeric_create (1, 0);
-    const gchar* m_stock_memo = nullptr;
-
-    bool m_cash_enabled;
-    Account* m_cash_account = nullptr;
-    const gchar* m_cash_memo = nullptr;
-    gnc_numeric m_cash_value = gnc_numeric_create (1, 0);
-
-    bool m_fees_enabled;
-    bool m_fees_capitalize;
-    Account* m_fees_account = nullptr;
-    const char* m_fees_memo = nullptr;
-    gnc_numeric m_fees_value = gnc_numeric_create (1, 0);
-
-    bool m_dividend_enabled;
-    Account* m_dividend_account = nullptr;
-    const char* m_dividend_memo = nullptr;
-    gnc_numeric m_dividend_value = gnc_numeric_create (1, 0);
-
-    bool m_capgains_enabled;
-    Account* m_capgains_account = nullptr;
-    const char* m_capgains_memo = nullptr;
-    gnc_numeric m_capgains_value = gnc_numeric_create (1, 0);
+    StockTransactionEntryPtr m_stock_entry;
+    StockTransactionEntryPtr m_cash_entry;
+    StockTransactionEntryPtr m_fees_entry;
+    StockTransactionEntryPtr m_dividend_entry;
+    StockTransactionEntryPtr m_capgains_entry;
+    StockTransactionEntryPtr m_stock_cg_entry; // Required at this level for lifetime management
 
     StockAssistantModel (Account *account) :
-     m_acct (account), m_currency (gnc_account_get_currency_or_parent (account)),
-     m_curr_pinfo (gnc_commodity_print_info (m_currency, true)),
-     m_price_pinfo (gnc_price_print_info (m_currency, true)),
-     m_stock_pinfo (gnc_commodity_print_info (xaccAccountGetCommodity (account), true))
+        m_acct{account},
+        m_currency{gnc_account_get_currency_or_parent(account)},
+        m_curr_pinfo (gnc_commodity_print_info (m_currency, true)),
+        m_stock_entry{std::make_unique<StockTransactionStockEntry>()},
+        m_cash_entry{std::make_unique<StockTransactionEntry>()},
+        m_fees_entry{std::make_unique<StockTransactionFeesEntry>()},
+        m_dividend_entry{std::make_unique<StockTransactionEntry>()},
+        m_capgains_entry{std::make_unique<StockTransactionEntry>()}
     {
         DEBUG ("StockAssistantModel constructor\n");
+        m_stock_entry->m_account = m_acct;
     };
 
     ~StockAssistantModel()
@@ -600,7 +688,7 @@ struct StockAssistantModel
     bool set_txn_type (guint type_idx);
     std::string get_stock_balance_str ()
     {
-        return xaccPrintAmount (m_balance_at_date, m_stock_pinfo);
+        return m_stock_entry->print_amount(m_balance_at_date);
     };
 
     std::string get_new_amount_str ();
@@ -615,6 +703,7 @@ private:
     SplitInfoVec m_list_of_splits;
 
     void add_price (QofBook *book);
+    StockTransactionSplitInfo make_stock_split_info(StringVec& errors);
 };
 
 bool
@@ -650,61 +739,160 @@ StockAssistantModel::set_txn_type (guint type_idx)
         PERR ("out of range type_idx=%d", type_idx);
         return false;
     }
+
     m_input_new_balance = m_txn_type->stock_amount & FieldMask::INPUT_NEW_BALANCE;
-    m_stock_amount_enabled = m_txn_type->stock_amount != FieldMask::DISABLED;
-    m_stock_value_enabled = m_txn_type->stock_value != FieldMask::DISABLED;
-    m_fees_capitalize = m_txn_type->fees_value & FieldMask::CAPITALIZE_DEFAULT;
-    m_fees_enabled = m_txn_type->fees_value != FieldMask::DISABLED;
-    m_capgains_enabled = m_txn_type->capgains_value != FieldMask::DISABLED;
-    m_dividend_enabled = m_txn_type->dividend_value != FieldMask::DISABLED;
-    m_cash_enabled = m_txn_type->cash_value != FieldMask::DISABLED;
+    m_stock_entry->set_fieldmask(m_txn_type->stock_amount);
+    m_fees_entry->set_fieldmask(m_txn_type->fees_value);
+    m_capgains_entry->set_fieldmask(m_txn_type->capgains_value);
+    m_dividend_entry->set_fieldmask(m_txn_type->dividend_value);
+    m_cash_entry->set_fieldmask(m_txn_type->cash_value);
     return true;
 };
 
 std::string
 StockAssistantModel::get_new_amount_str ()
 {
-    if (gnc_numeric_check (m_stock_amount))
-        return "";
+    std::string rv{""};
+    auto stock_entry = dynamic_cast<StockTransactionStockEntry*>(m_stock_entry.get());
+
+    if (gnc_numeric_check (stock_entry->m_amount))
+        return rv;
 
     if (m_input_new_balance)
     {
-        auto ratio = gnc_numeric_div (m_stock_amount, m_balance_at_date,
+        auto ratio = gnc_numeric_div (stock_entry->m_amount, m_balance_at_date,
                                       GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
         if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
-            return "";
+            return rv;
 
         std::ostringstream ret;
         ret << ratio.num << ':' << ratio.denom;
-        return ret.str();
+        rv = ret.str();
     }
     else
     {
+        auto stock_entry = dynamic_cast<StockTransactionStockEntry*>(m_stock_entry.get());
         auto amount = (m_txn_type->stock_amount & FieldMask::ENABLED_CREDIT) ?
-            gnc_numeric_neg (m_stock_amount) : m_stock_amount;
+            gnc_numeric_neg (stock_entry->m_amount) : stock_entry->m_amount;
         amount = gnc_numeric_add_fixed (amount, m_balance_at_date);
-        return xaccPrintAmount (amount, m_stock_pinfo);
+        rv = m_stock_entry->print_amount(amount);
     }
+
+    return rv;
 };
 
 std::tuple<bool, gnc_numeric, const char*>
 StockAssistantModel::calculate_price ()
 {
-    if (m_input_new_balance ||
-        !m_stock_amount_enabled || gnc_numeric_check (m_stock_amount) ||
-        !m_stock_value_enabled || gnc_numeric_check (m_stock_value) ||
-        gnc_numeric_zero_p (m_stock_amount) ||
-        gnc_numeric_zero_p (m_stock_value))
-        return { false, gnc_numeric_create (1, 0), nullptr };
-
-    auto price = gnc_numeric_div (m_stock_value, m_stock_amount,
-                                  GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
-    return {true, price, xaccPrintAmount (price, m_price_pinfo)};
+    auto price{m_stock_entry->calculate_price(m_input_new_balance)};
+    if (gnc_numeric_check(price))
+        return {false, price, nullptr};
+    auto pinfo{gnc_price_print_info (m_currency, true)};
+    return {true, price, xaccPrintAmount (price, pinfo)};
 }
 
-std::tuple<bool, std::string, SplitInfoVec>
-StockAssistantModel::generate_list_of_splits ()
+static void
+check_txn_date(GList* last_split_node, time64 txn_date, StringVec& warnings)
+{
+    auto last_split = static_cast<const Split *>(last_split_node->data);
+    auto last_split_date = xaccTransGetDate(xaccSplitGetParent(last_split));
+    if (txn_date <= last_split_date) {
+        auto last_split_date_str = qof_print_date(last_split_date);
+        auto new_date_str = qof_print_date(txn_date);
+        // Translators: the first %s is the new transaction date;
+        // the second %s is the current stock account's latest
+        // transaction date.
+        auto warn_txt = g_strdup_printf(
+            _("You will enter a transaction "
+              "with date %s which is earlier than the latest transaction in this account, "
+              "dated %s. Doing so may affect the cost basis, and therefore capital gains, "
+              "of transactions dated after the new entry. Please review all transactions "
+              "to ensure proper recording."),
+            new_date_str, last_split_date_str);
+        warnings.push_back(warn_txt);
+        g_free(warn_txt);
+        g_free(new_date_str);
+        g_free(last_split_date_str);
+    }
+}
+
+StockTransactionSplitInfo
+StockAssistantModel::make_stock_split_info(StringVec& errors)
+{
+    auto add_error_str = [&errors]
+        (const char* str) { errors.emplace_back (_(str)); };
+
+    StockTransactionSplitInfo line{m_stock_entry.get(),
+        NC_ ("Stock Assistant: Page name", "stock value")};
+    auto stock_entry = dynamic_cast<StockTransactionStockEntry*>(m_stock_entry.get());
+    bool negative_in_red = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL,
+                                               GNC_PREF_NEGATIVE_IN_RED);
+
+    if (m_input_new_balance)
+    {
+        auto& stock_amount = stock_entry->m_amount;
+        auto credit_side = (m_txn_type->stock_amount & FieldMask::AMOUNT_CREDIT);
+        auto delta = gnc_numeric_sub_fixed(stock_amount, m_balance_at_date);
+        auto ratio = gnc_numeric_div(stock_amount, m_balance_at_date,
+                                     GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+        stock_amount = gnc_numeric_sub_fixed(stock_amount, m_balance_at_date);
+        line.m_entry->set_amount(stock_amount, errors);
+        line.m_units_in_red =
+            negative_in_red && gnc_numeric_negative_p(stock_amount);
+        if (gnc_numeric_check(ratio) || !gnc_numeric_positive_p(ratio))
+            add_error_str(N_("Invalid stock new balance."));
+        else if (gnc_numeric_negative_p(delta) && !credit_side)
+            add_error_str(N_("New balance must be higher than old balance."));
+        else if (gnc_numeric_positive_p(delta) && credit_side)
+            add_error_str(N_("New balance must be lower than old balance."));
+    }
+    else if (stock_entry->m_amount_enabled)
+    {
+        auto& stock_amount = stock_entry->m_amount;
+        if (!gnc_numeric_positive_p(stock_amount))
+            add_error_str(N_("Stock amount must be positive."));
+        if (m_txn_type->stock_amount & FieldMask::AMOUNT_CREDIT)
+            stock_amount = gnc_numeric_neg(stock_amount);
+        line.m_units_in_red =
+            negative_in_red && gnc_numeric_negative_p(stock_amount);
+        auto new_bal = gnc_numeric_add_fixed(m_balance_at_date, stock_amount);
+        if (gnc_numeric_positive_p(m_balance_at_date) &&
+            gnc_numeric_negative_p(new_bal))
+            add_error_str(N_("Cannot sell more units than owned."));
+        else if (gnc_numeric_negative_p(m_balance_at_date) &&
+                 gnc_numeric_positive_p(new_bal))
+            add_error_str(N_("Cannot cover buy more units than owed."));
+    }
+    return line;
+}
+
+static inline std::string
+summary_message(const StringVec& infos, const StringVec& warnings,
+                const StringVec& errors)
 {
+    std::ostringstream summary;
+    auto summary_add = [&summary](auto a) { summary << "\n• " << a; };
+    if (errors.empty())
+    {
+        summary << _("No errors found. Click Apply to create transaction.");
+        std::for_each (infos.begin(), infos.end(), summary_add);
+    }
+    else
+    {
+        summary << _("The following errors must be fixed:");
+        std::for_each (errors.begin(), errors.end(), summary_add);
+    }
+    if (!warnings.empty())
+    {
+        summary << "\n\n" << _("The following warnings exist:");
+        std::for_each (warnings.begin(), warnings.end(), summary_add);
+    }
+
+    return summary.str();
+}
+
+std::tuple<bool, std::string, SplitInfoVec>
+StockAssistantModel::generate_list_of_splits() {
     if (!m_txn_types || !m_txn_type)
         return { false, "Error: txn_type not initialized", {} };
 
@@ -713,11 +901,6 @@ StockAssistantModel::generate_list_of_splits ()
     gnc_numeric debit = gnc_numeric_zero ();
     gnc_numeric credit = gnc_numeric_zero ();
     StringVec errors, warnings, infos;
-    StockTransactionSplitInfo line;
-    bool negative_in_red = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL,
-                                               GNC_PREF_NEGATIVE_IN_RED);
-    auto add_error_str = [&errors]
-        (const char* str) { errors.emplace_back (_(str)); };
 
     // check the stock transaction date. If there are existing stock
     // transactions dated after the date specified, it is very likely
@@ -725,83 +908,10 @@ StockAssistantModel::generate_list_of_splits ()
     // to review them.
     auto last_split_node = g_list_last (xaccAccountGetSplitList (m_acct));
     if (last_split_node)
-    {
-        auto last_split = static_cast<const Split*> (last_split_node->data);
-        auto last_split_date = xaccTransGetDate (xaccSplitGetParent (last_split));
-        if (m_transaction_date <= last_split_date)
-        {
-            auto last_split_date_str = qof_print_date (last_split_date);
-            auto new_date_str = qof_print_date (m_transaction_date);
-            // Translators: the first %s is the new transaction date;
-            // the second %s is the current stock account's latest
-            // transaction date.
-            auto warn_txt =  g_strdup_printf (_("You will enter a transaction \
-with date %s which is earlier than the latest transaction in this account, \
-dated %s. Doing so may affect the cost basis, and therefore capital gains, \
-of transactions dated after the new entry. Please review all transactions \
-to ensure proper recording."), new_date_str, last_split_date_str);
-            warnings.push_back (warn_txt);
-            g_free (warn_txt);
-            g_free (new_date_str);
-            g_free (last_split_date_str);
-        }
-    }
+        check_txn_date(last_split_node, m_transaction_date, warnings);
 
-    if (!m_stock_value_enabled)
-        line = StockTransactionSplitInfo (m_acct, gnc_numeric_zero());
-    else
-        line = StockTransactionSplitInfo(debit, credit, errors, m_txn_type->stock_value,
-                                         m_acct, m_stock_memo, m_stock_value,
-                                         NC_ ("Stock Assistant: Page name", "stock value"),
-                                         m_curr_pinfo);
 
-
-    if (!m_stock_amount_enabled)
-        line.m_units_numeric = gnc_numeric_zero();
-    else if (gnc_numeric_check (m_stock_amount))
-    {
-        line.m_units_str = _("(missing)");
-        line.m_units_numeric = gnc_numeric_zero();
-        add_error_str (N_("Amount for stock units is missing"));
-    }
-    else if (m_input_new_balance)
-    {
-        auto stock_amount = m_stock_amount;
-        auto credit_side = (m_txn_type->stock_amount & FieldMask::ENABLED_CREDIT);
-        auto delta = gnc_numeric_sub_fixed (stock_amount, m_balance_at_date);
-        auto ratio = gnc_numeric_div (stock_amount, m_balance_at_date,
-                                      GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
-        stock_amount = gnc_numeric_sub_fixed (stock_amount, m_balance_at_date);
-        line.m_units_numeric = stock_amount;
-        line.m_units_str = xaccPrintAmount (stock_amount, m_stock_pinfo);
-        line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
-        if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
-            add_error_str (N_("Invalid stock new balance."));
-        else if (gnc_numeric_negative_p (delta) && !credit_side)
-            add_error_str (N_("New balance must be higher than old balance."));
-        else if (gnc_numeric_positive_p (delta) && credit_side)
-            add_error_str (N_("New balance must be lower than old balance."));
-    }
-    else
-    {
-        auto stock_amount = m_stock_amount;
-        if (!gnc_numeric_positive_p (stock_amount))
-            add_error_str (N_("Stock amount must be positive."));
-        if (m_txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
-            stock_amount = gnc_numeric_neg (stock_amount);
-        line.m_units_numeric = stock_amount;
-        line.m_units_str = xaccPrintAmount (stock_amount, m_stock_pinfo);
-        line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
-        auto new_bal = gnc_numeric_add_fixed (m_balance_at_date, stock_amount);
-        if (gnc_numeric_positive_p (m_balance_at_date) &&
-            gnc_numeric_negative_p (new_bal))
-            add_error_str (N_("Cannot sell more units than owned."));
-        else if (gnc_numeric_negative_p (m_balance_at_date) &&
-                 gnc_numeric_positive_p (new_bal))
-            add_error_str (N_("Cannot cover buy more units than owed."));
-    }
-
-    m_list_of_splits.push_back (std::move (line));
+    m_list_of_splits.push_back (make_stock_split_info(errors));
 
     auto [has_price, price, price_str] = calculate_price ();
     if (has_price)
@@ -818,95 +928,76 @@ to ensure proper recording."), new_date_str, last_split_date_str);
         g_free (date_str);
     }
 
-    if (m_cash_enabled)
-    {
-        line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->cash_value,
-                                          m_cash_account, m_cash_memo, m_cash_value,
-                                          NC_ ("Stock Assistant: Page name", "cash"),
-                                          m_curr_pinfo);
-        m_list_of_splits.push_back (std::move (line));
-    }
+    if (m_cash_entry->m_enabled)
+        m_list_of_splits.push_back (StockTransactionSplitInfo{m_cash_entry.get(),
+             NC_ ("Stock Assistant: Page name", "cash")});
 
-    if (m_fees_enabled)
-    {
-        line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->fees_value,
-                                          m_fees_capitalize ? m_acct : m_fees_account,
-                                          m_fees_memo, m_fees_value,
-                                          NC_ ("Stock Assistant: Page name", "fees"),
-                                          m_curr_pinfo);
-        if (m_fees_capitalize)
-            line.m_units_numeric = gnc_numeric_zero();
-        m_list_of_splits.push_back (std::move (line));
-    }
+    if (m_fees_entry->m_enabled)
+        m_list_of_splits.push_back (StockTransactionSplitInfo{m_fees_entry.get(),
+             NC_ ("Stock Assistant: Page name", "fees")});
+
+    if (m_dividend_entry->m_enabled)
+        m_list_of_splits.push_back (StockTransactionSplitInfo{m_dividend_entry.get(),
+             NC_ ("Stock Assistant: Page name", "dividend")});
 
-    if (m_dividend_enabled)
+    if (m_capgains_entry->m_enabled)
     {
-        line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->dividend_value,
-                                          m_dividend_account, m_dividend_memo,
-                                          m_dividend_value,
-                                          NC_ ("Stock Assistant: Page name", "dividend"),
-                                          m_curr_pinfo);
-        m_list_of_splits.push_back (std::move (line));
+        m_stock_cg_entry =
+            std::make_unique<StockTransactionsStockCapGainsEntry>(m_capgains_entry.get(),
+                                                                  m_stock_entry.get());
+        m_list_of_splits.push_back(StockTransactionSplitInfo{m_stock_cg_entry.get(),
+             NC_ ("Stock Assistant: Page name", "capital gains")});
+        m_list_of_splits.push_back (StockTransactionSplitInfo{m_capgains_entry.get(),
+             NC_ ("Stock Assistant: Page name", "capital gains")});
     }
 
-    // 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 (m_capgains_enabled)
+    std::for_each(m_list_of_splits.begin(), m_list_of_splits.end(),
+                  [&debit, &credit](const auto& splitinfo) {
+                      if (splitinfo.m_entry->m_debit_side)
+                          debit = gnc_numeric_add(debit, splitinfo.m_entry->m_value,
+                                                   GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+                      else
+                          credit = gnc_numeric_add(credit, splitinfo.m_entry->m_value,
+                                                    GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+                  });
+
+    if (gnc_numeric_check(debit) || gnc_numeric_check(credit) ||!gnc_numeric_equal (debit, credit))
     {
-        if (m_txn_type->capgains_value & FieldMask::CAPGAINS_IN_STOCK)
+        const char *err_act = NULL, *err_reason = NULL;
+        if (gnc_numeric_check(debit))
         {
-            line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->capgains_value ^
-                                              (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
-                                              m_acct, m_capgains_memo, m_capgains_value,
-                                              NC_ ("Stock Assistant: Page name", "capital gains"),
-                                              m_curr_pinfo);
-            line.m_units_numeric = gnc_numeric_zero();
-            m_list_of_splits.push_back (std::move (line));
+            err_act = "debit";
+            err_reason = gnc_numeric_errorCode_to_string(gnc_numeric_check(debit));
+        }
+        else if (gnc_numeric_check(credit))
+        {
+            err_act = "credit";
+            err_reason = gnc_numeric_errorCode_to_string(gnc_numeric_check(credit));
         }
 
-        line = StockTransactionSplitInfo(debit, credit, errors, m_txn_type->capgains_value,
-                                         m_capgains_account, m_capgains_memo,
-                                         m_capgains_value,
-                                         NC_ ("Stock Assistant: Page name", "capital gains"),
-                                         m_curr_pinfo);
-        m_list_of_splits.push_back (std::move (line));
-    }
-
-    if (!gnc_numeric_equal (debit, credit))
-    {
-        auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
-        auto debit_str = g_strdup (xaccPrintAmount (debit, m_curr_pinfo));
-        auto credit_str = g_strdup (xaccPrintAmount (credit, m_curr_pinfo));
-        auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
-        errors.emplace_back (error_str);
-        g_free (error_str);
-        g_free (credit_str);
-        g_free (debit_str);
+        if (err_act)
+        {
+            auto err_str = g_strdup_printf (N_("Transaction can't balance, %s is error value %s"), err_act, err_reason);
+            errors.emplace_back(err_str);
+            g_free (err_str);
+        }
+        else
+        {
+            auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
+            auto debit_str = g_strdup (xaccPrintAmount (debit, m_curr_pinfo));
+            auto credit_str = g_strdup (xaccPrintAmount (credit, m_curr_pinfo));
+            auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
+            errors.emplace_back (error_str);
+            g_free (error_str);
+            g_free (credit_str);
+            g_free (debit_str);
+        }
     }
 
     // generate final summary message. Collates a header, the errors
     // and warnings. Then allow completion if errors is empty.
-    std::ostringstream summary;
-    auto summary_add = [&summary](auto a) { summary << "\n• " << a; };
-    if (errors.empty())
-    {
-        summary << _("No errors found. Click Apply to create transaction.");
-        std::for_each (infos.begin(), infos.end(), summary_add);
-    }
-    else
-    {
-        summary << _("The following errors must be fixed:");
-        std::for_each (errors.begin(), errors.end(), summary_add);
-    }
-    if (!warnings.empty())
-    {
-        summary << "\n\n" << _("The following warnings exist:");
-        std::for_each (warnings.begin(), warnings.end(), summary_add);
-    }
     m_ready_to_create = errors.empty();
-    return { m_ready_to_create, summary.str(), m_list_of_splits };
+    return { m_ready_to_create, summary_message(infos, warnings, errors), m_list_of_splits };
 }
 
 std::tuple<bool, Transaction*>
@@ -925,7 +1016,7 @@ StockAssistantModel::create_transaction ()
     xaccTransSetDatePostedSecsNormalized (trans, m_transaction_date);
     AccountVec accounts;
     std::for_each (m_list_of_splits.begin(), m_list_of_splits.end(),
-                   [&](auto& line){ line.create_split (trans, accounts); });
+                   [&](auto& line){ line.m_entry->create_split (trans, line.m_action, accounts); });
     add_price (book);
     xaccTransCommitEdit (trans);
     std::for_each (accounts.begin(), accounts.end(), xaccAccountCommitEdit);
@@ -1021,9 +1112,12 @@ struct StockAssistantView
     void set_focus_gae (GtkWidget *gae) { set_focus (GTK_WIDGET (gnc_amount_edit_gtk_entry (GNC_AMOUNT_EDIT (gae)))); }
 
     int get_transaction_type_index ()
-    { return gtk_combo_box_get_active (GTK_COMBO_BOX (transaction_type_combo)); }
+    {
+        return gtk_combo_box_get_active (GTK_COMBO_BOX (transaction_type_combo));
+    }
 
-    void set_transaction_types (const TxnTypeVec& txn_types)
+    void
+    set_transaction_types (const TxnTypeVec& txn_types)
     {
         auto combo = GTK_COMBO_BOX_TEXT (this->transaction_type_combo);
         gtk_combo_box_text_remove_all (combo);
@@ -1031,10 +1125,12 @@ struct StockAssistantView
                        [&combo](const auto& it)
                        { gtk_combo_box_text_append_text (combo, _(it.friendly_name)); });
         gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
-    };
+    }
 
     void set_txn_type_explanation (const gchar *txt)
-    { gtk_label_set_text (GTK_LABEL (this->transaction_type_explanation), txt); };
+    {
+        gtk_label_set_text (GTK_LABEL (this->transaction_type_explanation), txt);
+    }
 
     void
     prepare_stock_amount_page (bool input_new_balance, const std::string prev_balance)
@@ -1053,29 +1149,35 @@ struct StockAssistantView
         gtk_label_set_text (GTK_LABEL (this->prev_amount), prev_balance.c_str());
     };
 
-    void set_stock_amount (std::string new_amount_str)
+    void
+    set_stock_amount (std::string new_amount_str)
     {
         gtk_label_set_text (GTK_LABEL(this->next_amount), new_amount_str.c_str());
     };
 
-    void set_price_value (const gchar *val)
+    void
+    set_price_value (const gchar *val)
     { gtk_label_set_text (GTK_LABEL (this->price_value), val); };
 
-    bool get_capitalize_fees ()
+    bool
+    get_capitalize_fees ()
     { return gtk_toggle_button_get_active
             (GTK_TOGGLE_BUTTON (this->capitalize_fees_checkbox)); }
 
-    void set_capitalize_fees (bool state)
+    void
+    set_capitalize_fees (bool state)
     {
         gtk_toggle_button_set_active
             (GTK_TOGGLE_BUTTON (this->capitalize_fees_checkbox), state);
     }
 
-    void update_fees_acct_sensitive (bool sensitive)
+    void
+    update_fees_acct_sensitive (bool sensitive)
     { gtk_widget_set_sensitive (this->fees_account, sensitive); }
 
-    void prepare_finish_page (bool success, const std::string& summary,
-                              const SplitInfoVec& list_of_splits)
+    void
+    prepare_finish_page (bool success, const std::string& summary,
+                         const SplitInfoVec& list_of_splits, GNCPrintAmountInfo& pinfo)
     {
         auto gtv = GTK_TREE_VIEW (this->finish_split_view);
         auto list = GTK_LIST_STORE (gtk_tree_view_get_model (gtv));
@@ -1083,15 +1185,15 @@ struct StockAssistantView
         for (const auto& line : list_of_splits)
         {
             GtkTreeIter iter;
-            auto tooltip = g_markup_escape_text (line.m_memo_str.c_str(), -1);
+            auto tooltip = g_markup_escape_text (line.m_entry->m_memo, -1);
             gtk_list_store_append (list, &iter);
             gtk_list_store_set (list, &iter,
-                                SPLIT_COL_ACCOUNT, line.m_account_str.c_str(),
-                                SPLIT_COL_MEMO, line.m_memo_str.c_str(),
+                                SPLIT_COL_ACCOUNT, xaccAccountGetName(line.m_entry->m_account),
+                                SPLIT_COL_MEMO, line.m_entry->m_memo,
                                 SPLIT_COL_TOOLTIP, tooltip,
-                                SPLIT_COL_DEBIT, line.m_debit_side ? line.m_value_str.c_str() : nullptr,
-                                SPLIT_COL_CREDIT, line.m_debit_side ? nullptr : line.m_value_str.c_str(),
-                                SPLIT_COL_UNITS, line.m_units_str.c_str(),
+                                SPLIT_COL_DEBIT, line.m_entry->m_debit_side ? line.m_entry->print_value(pinfo) : nullptr,
+                                SPLIT_COL_CREDIT, line.m_entry->m_debit_side ? nullptr : line.m_entry->print_value(pinfo),
+                                SPLIT_COL_UNITS, line.m_entry->print_amount(line.m_entry->amount()),
                                 SPLIT_COL_UNITS_COLOR, line.m_units_in_red ? "red" : nullptr,
                                 -1);
             g_free (tooltip);
@@ -1313,7 +1415,8 @@ controller_transaction_type (GtkWidget *widget, StockAssistantController* info)
         return;
 
     info->view->set_txn_type_explanation (info->model->m_txn_type->explanation);
-    info->view->set_capitalize_fees (info->model->m_fees_capitalize);
+    auto fees_entry = dynamic_cast<StockTransactionFeesEntry*>(info->model->m_fees_entry.get());
+    info->view->set_capitalize_fees (fees_entry->m_capitalize);
 }
 
 static void controller_gde (GtkWidget *widget, time64* date)
@@ -1326,13 +1429,13 @@ static void controller_gtk_entry (GtkWidget *widget, const gchar **model_text)
     *model_text = gtk_entry_get_text (GTK_ENTRY (widget));
 }
 
-static void controller_gae (GtkWidget *widget, gnc_numeric *num)
+static void controller_gae (GtkWidget *widget, gnc_numeric* num)
 {
     gnc_numeric amt;
     if (!gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT(widget), &amt, true, nullptr))
         *num = amt;
     else
-        num->denom = 0;
+        *num = gnc_numeric_error(GNC_ERROR_ARG);
 }
 
 static void controller_gas (GtkWidget *widget, Account **acct)
@@ -1344,9 +1447,13 @@ static void
 controller_stock_amount (GtkWidget *widget, StockAssistantController* info)
 {
     g_return_if_fail (info && info->model->m_txn_type);
-
-    controller_gae (widget, &info->model->m_stock_amount);
-    info->view->set_stock_amount (info->model->get_new_amount_str());
+    StringVec errors;
+    auto amount = info->model->m_stock_entry->amount();
+    controller_gae (widget, &amount);
+    info->model->m_stock_entry->set_amount(amount, errors);
+    auto amount_str{info->model->m_stock_entry->print_amount(amount)};
+    if (amount_str)
+        info->view->set_stock_amount (amount_str);
 }
 
 static void
@@ -1354,7 +1461,9 @@ controller_stock_value (GtkWidget *widget, StockAssistantController* info)
 {
     g_return_if_fail (info && info->model->m_txn_type);
 
-    controller_gae (widget, &info->model->m_stock_value);
+    auto value = info->model->m_stock_entry->m_value;
+    controller_gae (widget, &value);
+    info->model->m_stock_entry->set_value(value);
     auto [has_price, price, price_str] = info->model->calculate_price ();
     // Translators: StockAssistant: N/A denotes stock price is not computable
     info->view->set_price_value (has_price ? price_str : _("N/A"));
@@ -1364,8 +1473,10 @@ static void
 controller_capitalize_fees (GtkWidget *widget, StockAssistantController* info)
 {
     g_return_if_fail (info && info->model->m_txn_type);
-    info->model->m_fees_capitalize = info->view->get_capitalize_fees ();
-    info->view->update_fees_acct_sensitive (!info->model->m_fees_capitalize);
+
+    bool cap =  info->view->get_capitalize_fees();
+    info->model->m_fees_entry->set_capitalize(cap);
+    info->view->update_fees_acct_sensitive (!cap);
 }
 
 void
@@ -1400,39 +1511,39 @@ stock_assistant_prepare_cb (GtkAssistant  *assistant, GtkWidget *page,
         view->set_focus_gae (view->stock_amount_edit);
         break;
     case PAGE_STOCK_VALUE:
-        controller_gtk_entry (view->stock_memo_edit, &model->m_stock_memo);
+        controller_gtk_entry (view->stock_memo_edit, &model->m_stock_entry->m_memo);
         controller_stock_value (view->stock_value_edit, info);
         view->set_focus_gae (view->stock_value_edit);
         break;
     case PAGE_CASH:
-        controller_gtk_entry (view->cash_memo_edit, &model->m_cash_memo);
-        controller_gae (view->cash_value, &model->m_cash_value);
-        controller_gas (view->cash_account, &model->m_cash_account);
+        controller_gtk_entry (view->cash_memo_edit, &model->m_cash_entry->m_memo);
+        controller_gae (view->cash_value, &model->m_cash_entry->m_value);
+        controller_gas (view->cash_account, &model->m_cash_entry->m_account);
         view->set_focus_gae (view->cash_value);
         break;
     case PAGE_FEES:
         controller_capitalize_fees (view->capitalize_fees_checkbox, info);
-        controller_gtk_entry (view->fees_memo_edit, &model->m_fees_memo);
-        controller_gae (view->fees_value, &model->m_fees_value);
-        controller_gas (view->fees_account, &model->m_fees_account);
+        controller_gtk_entry (view->fees_memo_edit, &model->m_fees_entry->m_memo);
+        controller_gae (view->fees_value, &model->m_fees_entry->m_value);
+        controller_gas (view->fees_account, &model->m_fees_entry->m_account);
         view->set_focus_gae (view->fees_value);
         break;
     case PAGE_DIVIDEND:
-        controller_gtk_entry (view->dividend_memo_edit, &model->m_dividend_memo);
-        controller_gae (view->dividend_value, &model->m_dividend_value);
-        controller_gas (view->dividend_account, &model->m_dividend_account);
+        controller_gtk_entry (view->dividend_memo_edit, &model->m_dividend_entry->m_memo);
+        controller_gae (view->dividend_value, &model->m_dividend_entry->m_value);
+        controller_gas (view->dividend_account, &model->m_dividend_entry->m_account);
         view->set_focus_gae (view->dividend_value);
         break;
     case PAGE_CAPGAINS:
-        controller_gtk_entry (view->capgains_memo_edit, &model->m_capgains_memo);
-        controller_gae (view->capgains_value, &model->m_capgains_value);
-        controller_gas (view->capgains_account, &model->m_capgains_account);
+        controller_gtk_entry (view->capgains_memo_edit, &model->m_capgains_entry->m_memo);
+        controller_gae (view->capgains_value, &model->m_capgains_entry->m_value);
+        controller_gas (view->capgains_account, &model->m_capgains_entry->m_account);
         view->set_focus_gae (view->capgains_value);
         break;
     case PAGE_FINISH:
     {
         auto [success, summary, list_of_splits] = model->generate_list_of_splits ();
-        view->prepare_finish_page (success, summary, list_of_splits);
+        view->prepare_finish_page (success, summary, list_of_splits, model->m_curr_pinfo);
         break;
     }
     default:
@@ -1448,18 +1559,19 @@ forward_page_func (gint current_page, StockAssistantController* info)
     current_page++;
     if (!model->m_txn_type)
         return current_page;
+    auto stock_entry = dynamic_cast<StockTransactionStockEntry*>(model->m_stock_entry.get());
 
-    if (!model->m_stock_amount_enabled && current_page == PAGE_STOCK_AMOUNT)
+    if (!stock_entry->m_amount_enabled && current_page == PAGE_STOCK_AMOUNT)
         current_page++;
-    if (!model->m_stock_value_enabled && current_page == PAGE_STOCK_VALUE)
+    if (!model->m_stock_entry->m_enabled && current_page == PAGE_STOCK_VALUE)
         current_page++;
-    if (!model->m_cash_enabled && current_page == PAGE_CASH)
+    if (!model->m_cash_entry->m_enabled && current_page == PAGE_CASH)
         current_page++;
-    if (!model->m_fees_enabled && current_page == PAGE_FEES)
+    if (!model->m_fees_entry->m_enabled && current_page == PAGE_FEES)
         current_page++;
-    if (!model->m_dividend_enabled && current_page == PAGE_DIVIDEND)
+    if (!model->m_dividend_entry->m_enabled && current_page == PAGE_DIVIDEND)
         current_page++;
-    if (!model->m_capgains_enabled && current_page == PAGE_CAPGAINS)
+    if (!model->m_capgains_entry->m_enabled && current_page == PAGE_CAPGAINS)
         current_page++;
 
     return current_page;
@@ -1519,20 +1631,20 @@ static void connect_signals (gpointer data, GtkBuilder *builder)
         { view->transaction_description , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_transaction_description },
         { view->stock_amount_edit       , "changed"            , G_CALLBACK (controller_stock_amount)          , info },
         { view->stock_value_edit        , "changed"            , G_CALLBACK (controller_stock_value)           , info },
-        { view->stock_memo_edit         , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_stock_memo },
-        { view->cash_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_cash_account },
-        { view->cash_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_cash_memo },
-        { view->cash_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_cash_value },
+        { view->stock_memo_edit         , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_stock_entry->m_memo },
+        { view->cash_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_cash_entry->m_account },
+        { view->cash_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_cash_entry->m_memo },
+        { view->cash_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_cash_entry->m_value },
         { view->capitalize_fees_checkbox, "toggled"            , G_CALLBACK (controller_capitalize_fees)       , info },
-        { view->fees_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_fees_account },
-        { view->fees_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_fees_memo },
-        { view->fees_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_fees_value },
-        { view->dividend_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_dividend_account },
-        { view->dividend_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_dividend_memo },
-        { view->dividend_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_dividend_value },
-        { view->capgains_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_capgains_account },
-        { view->capgains_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_capgains_memo },
-        { view->capgains_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_capgains_value },
+        { view->fees_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_fees_entry->m_account },
+        { view->fees_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_fees_entry->m_memo },
+        { view->fees_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_fees_entry->m_value },
+        { view->dividend_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_dividend_entry->m_account },
+        { view->dividend_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_dividend_entry->m_memo },
+        { view->dividend_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_dividend_entry->m_value },
+        { view->capgains_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_capgains_entry->m_account },
+        { view->capgains_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_capgains_entry->m_memo },
+        { view->capgains_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_capgains_entry->m_value },
         { view->window                  , "destroy"            , G_CALLBACK (stock_assistant_window_destroy_cb), info }
     };
     for (const auto& [widget, signal, callback, data] : signals)
diff --git a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
index 8ee30d853a..22433f0fdd 100644
--- a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
+++ b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
@@ -168,20 +168,22 @@ StockAssistantTest::instantiate_model(StockAssistantModel &model, const ASTTestC
 {
     model.m_transaction_date = gnc_dmy2time64 (t.dd, t.mm, t.yy);
     model.maybe_reset_txn_types ();
+    auto fees_entry = dynamic_cast<StockTransactionFeesEntry*>(model.m_fees_entry.get());
+    auto stock_entry = dynamic_cast<StockTransactionStockEntry*>(model.m_stock_entry.get());
 
     model.set_txn_type (t.type_idx);
     model.m_transaction_description = t.desc;
-    model.m_stock_amount = gnc_numeric_create (t.stock_amt * 100, 100);
-    model.m_stock_value = gnc_numeric_create (t.stock_val, 100);
-    model.m_cash_value = gnc_numeric_create (t.cash_val, 100);
-    model.m_cash_account = cash_account;
-    model.m_fees_account = fees_account;
-    model.m_fees_capitalize = t.capitalize;
-    model.m_fees_value = gnc_numeric_create (t.fees_val, 100);
-    model.m_capgains_account = capgains_account;
-    model.m_capgains_value = gnc_numeric_create (t.capg_val, 100);
-    model.m_dividend_account = dividend_account;
-    model.m_dividend_value = gnc_numeric_create (t.divi_val, 100);
+    stock_entry->m_amount = gnc_numeric_create (t.stock_amt * 100, 100);
+    model.m_stock_entry->m_value = gnc_numeric_create (t.stock_val, 100);
+    model.m_cash_entry->m_value = gnc_numeric_create (t.cash_val, 100);
+    model.m_cash_entry->m_account = cash_account;
+    model.m_fees_entry->m_account = fees_account;
+    fees_entry->m_capitalize = t.capitalize;
+    model.m_fees_entry->m_value = gnc_numeric_create (t.fees_val, 100);
+    model.m_capgains_entry->m_account = capgains_account;
+    model.m_capgains_entry->m_value = gnc_numeric_create (t.capg_val, 100);
+    model.m_dividend_entry->m_account = dividend_account;
+    model.m_dividend_entry->m_value = gnc_numeric_create (t.divi_val, 100);
 }
 
 class StockAssistantTestParameterized :

commit b3b071e0112c926ad02be13f9c85e3c35d1bf7d3
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Jan 28 10:49:33 2023 -0800

    [stock-txn-asst] StockAssistantModel variable names
    
    Rename member variables with m_ prefix, remove this-> from
    in-class references to those variables.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index 457d7887bb..c8c0736eb4 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -536,55 +536,55 @@ using SplitInfoVec = std::vector<StockTransactionSplitInfo>;
 
 struct StockAssistantModel
 {
-    Account   * acct;
+    Account* m_acct;
+    gnc_commodity* m_currency;
 
-    gnc_commodity * currency;
-    GNCPrintAmountInfo curr_pinfo;
-    GNCPrintAmountInfo price_pinfo;
-    GNCPrintAmountInfo stock_pinfo;
+    GNCPrintAmountInfo m_curr_pinfo;
+    GNCPrintAmountInfo m_price_pinfo;
+    GNCPrintAmountInfo m_stock_pinfo;
 
-    time64      transaction_date;
-    std::optional<TxnTypeVec> txn_types;
+    time64 m_transaction_date;
+    std::optional<TxnTypeVec> m_txn_types;
 
-    std::optional<TxnTypeInfo> txn_type;
+    std::optional<TxnTypeInfo> m_txn_type;
 
-    const gchar *transaction_description;
-    gnc_numeric balance_at_date = gnc_numeric_create (1, 0);
+    const char* m_transaction_description;
+    gnc_numeric m_balance_at_date = gnc_numeric_create (1, 0);
 
-    bool input_new_balance;
-    bool stock_amount_enabled;
-    gnc_numeric stock_amount = gnc_numeric_create (1, 0);
+    bool m_input_new_balance;
+    bool m_stock_amount_enabled;
+    gnc_numeric m_stock_amount = gnc_numeric_create (1, 0);
 
-    bool stock_value_enabled;
-    gnc_numeric stock_value = gnc_numeric_create (1, 0);
-    const gchar* stock_memo = nullptr;
+    bool m_stock_value_enabled;
+    gnc_numeric m_stock_value = gnc_numeric_create (1, 0);
+    const gchar* m_stock_memo = nullptr;
 
-    bool cash_enabled;
-    Account *cash_account = nullptr;
-    const gchar* cash_memo = nullptr;
-    gnc_numeric cash_value = gnc_numeric_create (1, 0);
+    bool m_cash_enabled;
+    Account* m_cash_account = nullptr;
+    const gchar* m_cash_memo = nullptr;
+    gnc_numeric m_cash_value = gnc_numeric_create (1, 0);
 
-    bool fees_enabled;
-    bool fees_capitalize;
-    Account *fees_account = nullptr;
-    const gchar* fees_memo = nullptr;
-    gnc_numeric fees_value = gnc_numeric_create (1, 0);
+    bool m_fees_enabled;
+    bool m_fees_capitalize;
+    Account* m_fees_account = nullptr;
+    const char* m_fees_memo = nullptr;
+    gnc_numeric m_fees_value = gnc_numeric_create (1, 0);
 
-    bool dividend_enabled;
-    Account *dividend_account = nullptr;
-    const gchar* dividend_memo = nullptr;
-    gnc_numeric dividend_value = gnc_numeric_create (1, 0);
+    bool m_dividend_enabled;
+    Account* m_dividend_account = nullptr;
+    const char* m_dividend_memo = nullptr;
+    gnc_numeric m_dividend_value = gnc_numeric_create (1, 0);
 
-    bool capgains_enabled;
-    Account *capgains_account = nullptr;
-    const gchar* capgains_memo = nullptr;
-    gnc_numeric capgains_value = gnc_numeric_create (1, 0);
+    bool m_capgains_enabled;
+    Account* m_capgains_account = nullptr;
+    const char* m_capgains_memo = nullptr;
+    gnc_numeric m_capgains_value = gnc_numeric_create (1, 0);
 
     StockAssistantModel (Account *account) :
-     acct (account), currency (gnc_account_get_currency_or_parent (account)),
-     curr_pinfo (gnc_commodity_print_info (this->currency, true)),
-     price_pinfo (gnc_price_print_info (this->currency, true)),
-     stock_pinfo (gnc_commodity_print_info (xaccAccountGetCommodity (account), true))
+     m_acct (account), m_currency (gnc_account_get_currency_or_parent (account)),
+     m_curr_pinfo (gnc_commodity_print_info (m_currency, true)),
+     m_price_pinfo (gnc_price_print_info (m_currency, true)),
+     m_stock_pinfo (gnc_commodity_print_info (xaccAccountGetCommodity (account), true))
     {
         DEBUG ("StockAssistantModel constructor\n");
     };
@@ -600,7 +600,7 @@ struct StockAssistantModel
     bool set_txn_type (guint type_idx);
     std::string get_stock_balance_str ()
     {
-        return xaccPrintAmount (this->balance_at_date, this->stock_pinfo);
+        return xaccPrintAmount (m_balance_at_date, m_stock_pinfo);
     };
 
     std::string get_new_amount_str ();
@@ -609,10 +609,10 @@ struct StockAssistantModel
     std::tuple<bool, Transaction*> create_transaction ();
 
 private:
-    std::optional<time64>     txn_types_date;
-    bool ready_to_create = false;
+    std::optional<time64>     m_txn_types_date;
+    bool m_ready_to_create = false;
 
-    SplitInfoVec list_of_splits;
+    SplitInfoVec m_list_of_splits;
 
     void add_price (QofBook *book);
 };
@@ -621,14 +621,14 @@ bool
 StockAssistantModel::maybe_reset_txn_types ()
 {
     auto new_bal = xaccAccountGetBalanceAsOfDate
-        (this->acct, gnc_time64_get_day_end (this->transaction_date));
-    if (this->txn_types_date && this->txn_types_date == this->transaction_date &&
-        gnc_numeric_equal (this->balance_at_date, new_bal))
+        (m_acct, gnc_time64_get_day_end (m_transaction_date));
+    if (m_txn_types_date && m_txn_types_date == m_transaction_date &&
+        gnc_numeric_equal (m_balance_at_date, new_bal))
         return false;
-    this->balance_at_date = new_bal;
-    this->txn_types_date = this->transaction_date;
-    this->txn_types = gnc_numeric_zero_p (this->balance_at_date) ? starting_types
-        : gnc_numeric_positive_p (this->balance_at_date) ? long_types
+    m_balance_at_date = new_bal;
+    m_txn_types_date = m_transaction_date;
+    m_txn_types = gnc_numeric_zero_p (m_balance_at_date) ? starting_types
+        : gnc_numeric_positive_p (m_balance_at_date) ? long_types
         : short_types;
     return true;
 };
@@ -636,40 +636,40 @@ StockAssistantModel::maybe_reset_txn_types ()
 bool
 StockAssistantModel::set_txn_type (guint type_idx)
 {
-    if (!this->txn_types_date || this->txn_types_date != this->transaction_date)
+    if (!m_txn_types_date || m_txn_types_date != m_transaction_date)
     {
         PERR ("transaction_date has changed. rerun maybe_reset_txn_types!");
         return false;
     }
     try
     {
-        this->txn_type = this->txn_types->at (type_idx);
+        m_txn_type = m_txn_types->at (type_idx);
     }
     catch (const std::out_of_range&)
     {
         PERR ("out of range type_idx=%d", type_idx);
         return false;
     }
-    this->input_new_balance = this->txn_type->stock_amount & FieldMask::INPUT_NEW_BALANCE;
-    this->stock_amount_enabled = this->txn_type->stock_amount != FieldMask::DISABLED;
-    this->stock_value_enabled = this->txn_type->stock_value != FieldMask::DISABLED;
-    this->fees_capitalize = this->txn_type->fees_value & FieldMask::CAPITALIZE_DEFAULT;
-    this->fees_enabled = this->txn_type->fees_value != FieldMask::DISABLED;
-    this->capgains_enabled = this->txn_type->capgains_value != FieldMask::DISABLED;
-    this->dividend_enabled = this->txn_type->dividend_value != FieldMask::DISABLED;
-    this->cash_enabled = this->txn_type->cash_value != FieldMask::DISABLED;
+    m_input_new_balance = m_txn_type->stock_amount & FieldMask::INPUT_NEW_BALANCE;
+    m_stock_amount_enabled = m_txn_type->stock_amount != FieldMask::DISABLED;
+    m_stock_value_enabled = m_txn_type->stock_value != FieldMask::DISABLED;
+    m_fees_capitalize = m_txn_type->fees_value & FieldMask::CAPITALIZE_DEFAULT;
+    m_fees_enabled = m_txn_type->fees_value != FieldMask::DISABLED;
+    m_capgains_enabled = m_txn_type->capgains_value != FieldMask::DISABLED;
+    m_dividend_enabled = m_txn_type->dividend_value != FieldMask::DISABLED;
+    m_cash_enabled = m_txn_type->cash_value != FieldMask::DISABLED;
     return true;
 };
 
 std::string
 StockAssistantModel::get_new_amount_str ()
 {
-    if (gnc_numeric_check (this->stock_amount))
+    if (gnc_numeric_check (m_stock_amount))
         return "";
 
-    if (this->input_new_balance)
+    if (m_input_new_balance)
     {
-        auto ratio = gnc_numeric_div (this->stock_amount, this->balance_at_date,
+        auto ratio = gnc_numeric_div (m_stock_amount, m_balance_at_date,
                                       GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
         if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
             return "";
@@ -680,35 +680,35 @@ StockAssistantModel::get_new_amount_str ()
     }
     else
     {
-        auto amount = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT) ?
-            gnc_numeric_neg (this->stock_amount) : this->stock_amount;
-        amount = gnc_numeric_add_fixed (amount, this->balance_at_date);
-        return xaccPrintAmount (amount, stock_pinfo);
+        auto amount = (m_txn_type->stock_amount & FieldMask::ENABLED_CREDIT) ?
+            gnc_numeric_neg (m_stock_amount) : m_stock_amount;
+        amount = gnc_numeric_add_fixed (amount, m_balance_at_date);
+        return xaccPrintAmount (amount, m_stock_pinfo);
     }
 };
 
 std::tuple<bool, gnc_numeric, const char*>
 StockAssistantModel::calculate_price ()
 {
-    if (this->input_new_balance ||
-        !this->stock_amount_enabled || gnc_numeric_check (this->stock_amount) ||
-        !this->stock_value_enabled || gnc_numeric_check (this->stock_value) ||
-        gnc_numeric_zero_p (this->stock_amount) ||
-        gnc_numeric_zero_p (this->stock_value))
+    if (m_input_new_balance ||
+        !m_stock_amount_enabled || gnc_numeric_check (m_stock_amount) ||
+        !m_stock_value_enabled || gnc_numeric_check (m_stock_value) ||
+        gnc_numeric_zero_p (m_stock_amount) ||
+        gnc_numeric_zero_p (m_stock_value))
         return { false, gnc_numeric_create (1, 0), nullptr };
 
-    auto price = gnc_numeric_div (this->stock_value, this->stock_amount,
+    auto price = gnc_numeric_div (m_stock_value, m_stock_amount,
                                   GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
-    return {true, price, xaccPrintAmount (price, this->price_pinfo)};
+    return {true, price, xaccPrintAmount (price, m_price_pinfo)};
 }
 
 std::tuple<bool, std::string, SplitInfoVec>
 StockAssistantModel::generate_list_of_splits ()
 {
-    if (!this->txn_types || !this->txn_type)
+    if (!m_txn_types || !m_txn_type)
         return { false, "Error: txn_type not initialized", {} };
 
-    this->list_of_splits.clear();
+    m_list_of_splits.clear();
 
     gnc_numeric debit = gnc_numeric_zero ();
     gnc_numeric credit = gnc_numeric_zero ();
@@ -723,15 +723,15 @@ StockAssistantModel::generate_list_of_splits ()
     // transactions dated after the date specified, it is very likely
     // the later stock transactions will be invalidated. warn the user
     // to review them.
-    auto last_split_node = g_list_last (xaccAccountGetSplitList (this->acct));
+    auto last_split_node = g_list_last (xaccAccountGetSplitList (m_acct));
     if (last_split_node)
     {
         auto last_split = static_cast<const Split*> (last_split_node->data);
         auto last_split_date = xaccTransGetDate (xaccSplitGetParent (last_split));
-        if (this->transaction_date <= last_split_date)
+        if (m_transaction_date <= last_split_date)
         {
             auto last_split_date_str = qof_print_date (last_split_date);
-            auto new_date_str = qof_print_date (this->transaction_date);
+            auto new_date_str = qof_print_date (m_transaction_date);
             // Translators: the first %s is the new transaction date;
             // the second %s is the current stock account's latest
             // transaction date.
@@ -747,33 +747,33 @@ to ensure proper recording."), new_date_str, last_split_date_str);
         }
     }
 
-    if (!this->stock_value_enabled)
-        line = StockTransactionSplitInfo (this->acct, gnc_numeric_zero());
+    if (!m_stock_value_enabled)
+        line = StockTransactionSplitInfo (m_acct, gnc_numeric_zero());
     else
-        line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->stock_value,
-                                         this->acct, this->stock_memo, this->stock_value,
+        line = StockTransactionSplitInfo(debit, credit, errors, m_txn_type->stock_value,
+                                         m_acct, m_stock_memo, m_stock_value,
                                          NC_ ("Stock Assistant: Page name", "stock value"),
-                                         this->curr_pinfo);
+                                         m_curr_pinfo);
 
 
-    if (!this->stock_amount_enabled)
+    if (!m_stock_amount_enabled)
         line.m_units_numeric = gnc_numeric_zero();
-    else if (gnc_numeric_check (this->stock_amount))
+    else if (gnc_numeric_check (m_stock_amount))
     {
         line.m_units_str = _("(missing)");
         line.m_units_numeric = gnc_numeric_zero();
         add_error_str (N_("Amount for stock units is missing"));
     }
-    else if (this->input_new_balance)
+    else if (m_input_new_balance)
     {
-        auto stock_amount = this->stock_amount;
-        auto credit_side = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT);
-        auto delta = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
-        auto ratio = gnc_numeric_div (stock_amount, this->balance_at_date,
+        auto stock_amount = m_stock_amount;
+        auto credit_side = (m_txn_type->stock_amount & FieldMask::ENABLED_CREDIT);
+        auto delta = gnc_numeric_sub_fixed (stock_amount, m_balance_at_date);
+        auto ratio = gnc_numeric_div (stock_amount, m_balance_at_date,
                                       GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
-        stock_amount = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
+        stock_amount = gnc_numeric_sub_fixed (stock_amount, m_balance_at_date);
         line.m_units_numeric = stock_amount;
-        line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+        line.m_units_str = xaccPrintAmount (stock_amount, m_stock_pinfo);
         line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
         if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
             add_error_str (N_("Invalid stock new balance."));
@@ -784,101 +784,101 @@ to ensure proper recording."), new_date_str, last_split_date_str);
     }
     else
     {
-        auto stock_amount = this->stock_amount;
+        auto stock_amount = m_stock_amount;
         if (!gnc_numeric_positive_p (stock_amount))
             add_error_str (N_("Stock amount must be positive."));
-        if (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
+        if (m_txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
             stock_amount = gnc_numeric_neg (stock_amount);
         line.m_units_numeric = stock_amount;
-        line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+        line.m_units_str = xaccPrintAmount (stock_amount, m_stock_pinfo);
         line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
-        auto new_bal = gnc_numeric_add_fixed (this->balance_at_date, stock_amount);
-        if (gnc_numeric_positive_p (this->balance_at_date) &&
+        auto new_bal = gnc_numeric_add_fixed (m_balance_at_date, stock_amount);
+        if (gnc_numeric_positive_p (m_balance_at_date) &&
             gnc_numeric_negative_p (new_bal))
             add_error_str (N_("Cannot sell more units than owned."));
-        else if (gnc_numeric_negative_p (this->balance_at_date) &&
+        else if (gnc_numeric_negative_p (m_balance_at_date) &&
                  gnc_numeric_positive_p (new_bal))
             add_error_str (N_("Cannot cover buy more units than owed."));
     }
 
-    this->list_of_splits.push_back (std::move (line));
+    m_list_of_splits.push_back (std::move (line));
 
-    auto [has_price, price, price_str] = this->calculate_price ();
+    auto [has_price, price, price_str] = calculate_price ();
     if (has_price)
     {
         // Translators: %s refer to: stock mnemonic, broker currency,
         // date of transaction.
         auto tmpl = N_("A price of 1 %s = %s on %s will be recorded.");
-        auto date_str = qof_print_date (this->transaction_date);
+        auto date_str = qof_print_date (m_transaction_date);
         auto price_msg = g_strdup_printf
             (_(tmpl),
-             gnc_commodity_get_mnemonic (xaccAccountGetCommodity (this->acct)),
+             gnc_commodity_get_mnemonic (xaccAccountGetCommodity (m_acct)),
              price_str, date_str);
         infos.emplace_back (price_msg);
         g_free (date_str);
     }
 
-    if (this->cash_enabled)
+    if (m_cash_enabled)
     {
-        line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->cash_value,
-                                          this->cash_account, this->cash_memo, this->cash_value,
+        line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->cash_value,
+                                          m_cash_account, m_cash_memo, m_cash_value,
                                           NC_ ("Stock Assistant: Page name", "cash"),
-                                          this->curr_pinfo);
-        this->list_of_splits.push_back (std::move (line));
+                                          m_curr_pinfo);
+        m_list_of_splits.push_back (std::move (line));
     }
 
-    if (this->fees_enabled)
+    if (m_fees_enabled)
     {
-        line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->fees_value,
-                                          this->fees_capitalize ? this->acct : this->fees_account,
-                                          this->fees_memo, this->fees_value,
+        line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->fees_value,
+                                          m_fees_capitalize ? m_acct : m_fees_account,
+                                          m_fees_memo, m_fees_value,
                                           NC_ ("Stock Assistant: Page name", "fees"),
-                                          this->curr_pinfo);
-        if (this->fees_capitalize)
+                                          m_curr_pinfo);
+        if (m_fees_capitalize)
             line.m_units_numeric = gnc_numeric_zero();
-        this->list_of_splits.push_back (std::move (line));
+        m_list_of_splits.push_back (std::move (line));
     }
 
-    if (this->dividend_enabled)
+    if (m_dividend_enabled)
     {
-        line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->dividend_value,
-                                          this->dividend_account, this->dividend_memo,
-                                          this->dividend_value,
+        line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->dividend_value,
+                                          m_dividend_account, m_dividend_memo,
+                                          m_dividend_value,
                                           NC_ ("Stock Assistant: Page name", "dividend"),
-                                          this->curr_pinfo);
-        this->list_of_splits.push_back (std::move (line));
+                                          m_curr_pinfo);
+        m_list_of_splits.push_back (std::move (line));
     }
 
     // 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 (this->capgains_enabled)
+    if (m_capgains_enabled)
     {
-        if (this->txn_type->capgains_value & FieldMask::CAPGAINS_IN_STOCK)
+        if (m_txn_type->capgains_value & FieldMask::CAPGAINS_IN_STOCK)
         {
-            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->capgains_value ^
+            line = StockTransactionSplitInfo (debit, credit, errors, m_txn_type->capgains_value ^
                                               (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
-                                              this->acct, this->capgains_memo, this->capgains_value,
+                                              m_acct, m_capgains_memo, m_capgains_value,
                                               NC_ ("Stock Assistant: Page name", "capital gains"),
-                                              this->curr_pinfo);
+                                              m_curr_pinfo);
             line.m_units_numeric = gnc_numeric_zero();
-            this->list_of_splits.push_back (std::move (line));
+            m_list_of_splits.push_back (std::move (line));
         }
 
-        line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->capgains_value,
-                                         this->capgains_account, this->capgains_memo,
-                                         this->capgains_value,
+        line = StockTransactionSplitInfo(debit, credit, errors, m_txn_type->capgains_value,
+                                         m_capgains_account, m_capgains_memo,
+                                         m_capgains_value,
                                          NC_ ("Stock Assistant: Page name", "capital gains"),
-                                         this->curr_pinfo);
-        this->list_of_splits.push_back (std::move (line));
+                                         m_curr_pinfo);
+        m_list_of_splits.push_back (std::move (line));
     }
 
     if (!gnc_numeric_equal (debit, credit))
     {
         auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
-        auto debit_str = g_strdup (xaccPrintAmount (debit, this->curr_pinfo));
-        auto credit_str = g_strdup (xaccPrintAmount (credit, this->curr_pinfo));
+        auto debit_str = g_strdup (xaccPrintAmount (debit, m_curr_pinfo));
+        auto credit_str = g_strdup (xaccPrintAmount (credit, m_curr_pinfo));
         auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
         errors.emplace_back (error_str);
         g_free (error_str);
@@ -905,46 +905,46 @@ to ensure proper recording."), new_date_str, last_split_date_str);
         summary << "\n\n" << _("The following warnings exist:");
         std::for_each (warnings.begin(), warnings.end(), summary_add);
     }
-    this->ready_to_create = errors.empty();
-    return { this->ready_to_create, summary.str(), this->list_of_splits };
+    m_ready_to_create = errors.empty();
+    return { m_ready_to_create, summary.str(), m_list_of_splits };
 }
 
 std::tuple<bool, Transaction*>
 StockAssistantModel::create_transaction ()
 {
-    if (!this->ready_to_create)
+    if (!m_ready_to_create)
     {
         PERR ("errors exist. cannot create transaction.");
         return { false, nullptr };
     }
-    auto book = qof_instance_get_book (acct);
+    auto book = qof_instance_get_book (m_acct);
     auto trans = xaccMallocTransaction (book);
     xaccTransBeginEdit (trans);
-    xaccTransSetCurrency (trans, this->currency);
-    xaccTransSetDescription (trans, this->transaction_description);
-    xaccTransSetDatePostedSecsNormalized (trans, this->transaction_date);
+    xaccTransSetCurrency (trans, m_currency);
+    xaccTransSetDescription (trans, m_transaction_description);
+    xaccTransSetDatePostedSecsNormalized (trans, m_transaction_date);
     AccountVec accounts;
-    std::for_each (this->list_of_splits.begin(), this->list_of_splits.end(),
+    std::for_each (m_list_of_splits.begin(), m_list_of_splits.end(),
                    [&](auto& line){ line.create_split (trans, accounts); });
-    this->add_price (book);
+    add_price (book);
     xaccTransCommitEdit (trans);
     std::for_each (accounts.begin(), accounts.end(), xaccAccountCommitEdit);
-    this->ready_to_create = false;
+    m_ready_to_create = false;
     return { true, trans };
 }
 
 void
 StockAssistantModel::add_price (QofBook *book)
 {
-    auto [has_price, p, price_str] = this->calculate_price ();
+    auto [has_price, p, price_str] = calculate_price ();
     if (!has_price)
         return;
 
     auto price = gnc_price_create (book);
     gnc_price_begin_edit (price);
-    gnc_price_set_commodity (price, xaccAccountGetCommodity (this->acct));
-    gnc_price_set_currency (price, this->currency);
-    gnc_price_set_time64 (price, this->transaction_date);
+    gnc_price_set_commodity (price, xaccAccountGetCommodity (m_acct));
+    gnc_price_set_currency (price, m_currency);
+    gnc_price_set_time64 (price, m_transaction_date);
     gnc_price_set_source (price, PRICE_SOURCE_STOCK_TRANSACTION);
     gnc_price_set_typestr (price, PRICE_TYPE_UNK);
     gnc_price_set_value (price, p);
@@ -1282,7 +1282,7 @@ struct StockAssistantController
         gnc_builder_add_from_file (builder, "assistant-stock-transaction.glade",
                                    "stock_transaction_assistant");
         this->view = std::make_unique<StockAssistantView>
-            (builder, xaccAccountGetCommodity (acct), this->model->currency, parent);
+            (builder, xaccAccountGetCommodity (acct), this->model->m_currency, parent);
         connect_signals (this, builder);
         g_object_unref (builder);
         DEBUG ("StockAssistantController constructor\n");
@@ -1302,7 +1302,7 @@ stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data)
 static void
 controller_transaction_type (GtkWidget *widget, StockAssistantController* info)
 {
-    if (!info->model->txn_types)
+    if (!info->model->m_txn_types)
         return;
 
     auto type_idx = info->view->get_transaction_type_index();
@@ -1312,8 +1312,8 @@ controller_transaction_type (GtkWidget *widget, StockAssistantController* info)
     if (!info->model->set_txn_type (type_idx))
         return;
 
-    info->view->set_txn_type_explanation (info->model->txn_type->explanation);
-    info->view->set_capitalize_fees (info->model->fees_capitalize);
+    info->view->set_txn_type_explanation (info->model->m_txn_type->explanation);
+    info->view->set_capitalize_fees (info->model->m_fees_capitalize);
 }
 
 static void controller_gde (GtkWidget *widget, time64* date)
@@ -1343,18 +1343,18 @@ static void controller_gas (GtkWidget *widget, Account **acct)
 static void
 controller_stock_amount (GtkWidget *widget, StockAssistantController* info)
 {
-    g_return_if_fail (info && info->model->txn_type);
+    g_return_if_fail (info && info->model->m_txn_type);
 
-    controller_gae (widget, &info->model->stock_amount);
+    controller_gae (widget, &info->model->m_stock_amount);
     info->view->set_stock_amount (info->model->get_new_amount_str());
 }
 
 static void
 controller_stock_value (GtkWidget *widget, StockAssistantController* info)
 {
-    g_return_if_fail (info && info->model->txn_type);
+    g_return_if_fail (info && info->model->m_txn_type);
 
-    controller_gae (widget, &info->model->stock_value);
+    controller_gae (widget, &info->model->m_stock_value);
     auto [has_price, price, price_str] = info->model->calculate_price ();
     // Translators: StockAssistant: N/A denotes stock price is not computable
     info->view->set_price_value (has_price ? price_str : _("N/A"));
@@ -1363,9 +1363,9 @@ controller_stock_value (GtkWidget *widget, StockAssistantController* info)
 static void
 controller_capitalize_fees (GtkWidget *widget, StockAssistantController* info)
 {
-    g_return_if_fail (info && info->model->txn_type);
-    info->model->fees_capitalize = info->view->get_capitalize_fees ();
-    info->view->update_fees_acct_sensitive (!info->model->fees_capitalize);
+    g_return_if_fail (info && info->model->m_txn_type);
+    info->model->m_fees_capitalize = info->view->get_capitalize_fees ();
+    info->view->update_fees_acct_sensitive (!info->model->m_fees_capitalize);
 }
 
 void
@@ -1384,49 +1384,49 @@ stock_assistant_prepare_cb (GtkAssistant  *assistant, GtkWidget *page,
     case PAGE_TRANSACTION_TYPE:
         if (!model->maybe_reset_txn_types())
             break;
-        view->set_transaction_types (model->txn_types.value());
+        view->set_transaction_types (model->m_txn_types.value());
         controller_transaction_type (view->transaction_type_combo, info);
         view->set_focus (view->transaction_type_combo);
         break;
     case PAGE_TRANSACTION_DETAILS:
-        controller_gde (view->transaction_date, &model->transaction_date);
-        controller_gtk_entry (view->transaction_description, &model->transaction_description);
+        controller_gde (view->transaction_date, &model->m_transaction_date);
+        controller_gtk_entry (view->transaction_description, &model->m_transaction_description);
         view->set_focus (view->transaction_description);
         break;
     case PAGE_STOCK_AMOUNT:
-        view->prepare_stock_amount_page (model->input_new_balance,
+        view->prepare_stock_amount_page (model->m_input_new_balance,
                                          model->get_stock_balance_str());
         controller_stock_amount (view->stock_amount_edit, info);
         view->set_focus_gae (view->stock_amount_edit);
         break;
     case PAGE_STOCK_VALUE:
-        controller_gtk_entry (view->stock_memo_edit, &model->stock_memo);
+        controller_gtk_entry (view->stock_memo_edit, &model->m_stock_memo);
         controller_stock_value (view->stock_value_edit, info);
         view->set_focus_gae (view->stock_value_edit);
         break;
     case PAGE_CASH:
-        controller_gtk_entry (view->cash_memo_edit, &model->cash_memo);
-        controller_gae (view->cash_value, &model->cash_value);
-        controller_gas (view->cash_account, &model->cash_account);
+        controller_gtk_entry (view->cash_memo_edit, &model->m_cash_memo);
+        controller_gae (view->cash_value, &model->m_cash_value);
+        controller_gas (view->cash_account, &model->m_cash_account);
         view->set_focus_gae (view->cash_value);
         break;
     case PAGE_FEES:
         controller_capitalize_fees (view->capitalize_fees_checkbox, info);
-        controller_gtk_entry (view->fees_memo_edit, &model->fees_memo);
-        controller_gae (view->fees_value, &model->fees_value);
-        controller_gas (view->fees_account, &model->fees_account);
+        controller_gtk_entry (view->fees_memo_edit, &model->m_fees_memo);
+        controller_gae (view->fees_value, &model->m_fees_value);
+        controller_gas (view->fees_account, &model->m_fees_account);
         view->set_focus_gae (view->fees_value);
         break;
     case PAGE_DIVIDEND:
-        controller_gtk_entry (view->dividend_memo_edit, &model->dividend_memo);
-        controller_gae (view->dividend_value, &model->dividend_value);
-        controller_gas (view->dividend_account, &model->dividend_account);
+        controller_gtk_entry (view->dividend_memo_edit, &model->m_dividend_memo);
+        controller_gae (view->dividend_value, &model->m_dividend_value);
+        controller_gas (view->dividend_account, &model->m_dividend_account);
         view->set_focus_gae (view->dividend_value);
         break;
     case PAGE_CAPGAINS:
-        controller_gtk_entry (view->capgains_memo_edit, &model->capgains_memo);
-        controller_gae (view->capgains_value, &model->capgains_value);
-        controller_gas (view->capgains_account, &model->capgains_account);
+        controller_gtk_entry (view->capgains_memo_edit, &model->m_capgains_memo);
+        controller_gae (view->capgains_value, &model->m_capgains_value);
+        controller_gas (view->capgains_account, &model->m_capgains_account);
         view->set_focus_gae (view->capgains_value);
         break;
     case PAGE_FINISH:
@@ -1446,20 +1446,20 @@ forward_page_func (gint current_page, StockAssistantController* info)
     auto model = info->model.get();
 
     current_page++;
-    if (!model->txn_type)
+    if (!model->m_txn_type)
         return current_page;
 
-    if (!model->stock_amount_enabled && current_page == PAGE_STOCK_AMOUNT)
+    if (!model->m_stock_amount_enabled && current_page == PAGE_STOCK_AMOUNT)
         current_page++;
-    if (!model->stock_value_enabled && current_page == PAGE_STOCK_VALUE)
+    if (!model->m_stock_value_enabled && current_page == PAGE_STOCK_VALUE)
         current_page++;
-    if (!model->cash_enabled && current_page == PAGE_CASH)
+    if (!model->m_cash_enabled && current_page == PAGE_CASH)
         current_page++;
-    if (!model->fees_enabled && current_page == PAGE_FEES)
+    if (!model->m_fees_enabled && current_page == PAGE_FEES)
         current_page++;
-    if (!model->dividend_enabled && current_page == PAGE_DIVIDEND)
+    if (!model->m_dividend_enabled && current_page == PAGE_DIVIDEND)
         current_page++;
-    if (!model->capgains_enabled && current_page == PAGE_CAPGAINS)
+    if (!model->m_capgains_enabled && current_page == PAGE_CAPGAINS)
         current_page++;
 
     return current_page;
@@ -1469,7 +1469,7 @@ void
 stock_assistant_finish_cb (GtkAssistant *assistant, gpointer user_data)
 {
     auto info = static_cast<StockAssistantController*>(user_data);
-    g_return_if_fail (info->model->txn_type);
+    g_return_if_fail (info->model->m_txn_type);
 
     gnc_suspend_gui_refresh ();
     gnc_resume_gui_refresh ();
@@ -1491,9 +1491,9 @@ refresh_handler (GHashTable *changes, gpointer user_data)
 {
     auto info = static_cast<StockAssistantController*>(user_data);
 
-    if (!GNC_IS_ACCOUNT (info->model->acct))
+    if (!GNC_IS_ACCOUNT (info->model->m_acct))
     {
-        PWARN ("account %p does not exist anymore. abort", info->model->acct);
+        PWARN ("account %p does not exist anymore. abort", info->model->m_acct);
         gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info);
     }
 }
@@ -1515,24 +1515,24 @@ static void connect_signals (gpointer data, GtkBuilder *builder)
     std::vector<SignalData> signals =
     {
         { view->transaction_type_combo  , "changed"            , G_CALLBACK (controller_transaction_type)      , info },
-        { view->transaction_date        , "date_changed"       , G_CALLBACK (controller_gde)                   , &model->transaction_date },
-        { view->transaction_description , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->transaction_description },
+        { view->transaction_date        , "date_changed"       , G_CALLBACK (controller_gde)                   , &model->m_transaction_date },
+        { view->transaction_description , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_transaction_description },
         { view->stock_amount_edit       , "changed"            , G_CALLBACK (controller_stock_amount)          , info },
         { view->stock_value_edit        , "changed"            , G_CALLBACK (controller_stock_value)           , info },
-        { view->stock_memo_edit         , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->stock_memo },
-        { view->cash_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->cash_account },
-        { view->cash_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->cash_memo },
-        { view->cash_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->cash_value },
+        { view->stock_memo_edit         , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_stock_memo },
+        { view->cash_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_cash_account },
+        { view->cash_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_cash_memo },
+        { view->cash_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_cash_value },
         { view->capitalize_fees_checkbox, "toggled"            , G_CALLBACK (controller_capitalize_fees)       , info },
-        { view->fees_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->fees_account },
-        { view->fees_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->fees_memo },
-        { view->fees_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->fees_value },
-        { view->dividend_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->dividend_account },
-        { view->dividend_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->dividend_memo },
-        { view->dividend_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->dividend_value },
-        { view->capgains_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->capgains_account },
-        { view->capgains_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->capgains_memo },
-        { view->capgains_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->capgains_value },
+        { view->fees_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_fees_account },
+        { view->fees_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_fees_memo },
+        { view->fees_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_fees_value },
+        { view->dividend_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_dividend_account },
+        { view->dividend_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_dividend_memo },
+        { view->dividend_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_dividend_value },
+        { view->capgains_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->m_capgains_account },
+        { view->capgains_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->m_capgains_memo },
+        { view->capgains_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->m_capgains_value },
         { view->window                  , "destroy"            , G_CALLBACK (stock_assistant_window_destroy_cb), info }
     };
     for (const auto& [widget, signal, callback, data] : signals)
diff --git a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
index 32c3266327..8ee30d853a 100644
--- a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
+++ b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
@@ -166,22 +166,22 @@ StockAssistantTest::StockAssistantTest() :
 void
 StockAssistantTest::instantiate_model(StockAssistantModel &model, const ASTTestCase &t)
 {
-    model.transaction_date = gnc_dmy2time64 (t.dd, t.mm, t.yy);
+    model.m_transaction_date = gnc_dmy2time64 (t.dd, t.mm, t.yy);
     model.maybe_reset_txn_types ();
 
     model.set_txn_type (t.type_idx);
-    model.transaction_description = t.desc;
-    model.stock_amount = gnc_numeric_create (t.stock_amt * 100, 100);
-    model.stock_value = gnc_numeric_create (t.stock_val, 100);
-    model.cash_value = gnc_numeric_create (t.cash_val, 100);
-    model.cash_account = cash_account;
-    model.fees_account = fees_account;
-    model.fees_capitalize = t.capitalize;
-    model.fees_value = gnc_numeric_create (t.fees_val, 100);
-    model.capgains_account = capgains_account;
-    model.capgains_value = gnc_numeric_create (t.capg_val, 100);
-    model.dividend_account = dividend_account;
-    model.dividend_value = gnc_numeric_create (t.divi_val, 100);
+    model.m_transaction_description = t.desc;
+    model.m_stock_amount = gnc_numeric_create (t.stock_amt * 100, 100);
+    model.m_stock_value = gnc_numeric_create (t.stock_val, 100);
+    model.m_cash_value = gnc_numeric_create (t.cash_val, 100);
+    model.m_cash_account = cash_account;
+    model.m_fees_account = fees_account;
+    model.m_fees_capitalize = t.capitalize;
+    model.m_fees_value = gnc_numeric_create (t.fees_val, 100);
+    model.m_capgains_account = capgains_account;
+    model.m_capgains_value = gnc_numeric_create (t.capg_val, 100);
+    model.m_dividend_account = dividend_account;
+    model.m_dividend_value = gnc_numeric_create (t.divi_val, 100);
 }
 
 class StockAssistantTestParameterized :
@@ -202,7 +202,7 @@ protected:
 TEST_F(StockAssistantTest, testFailureModes)
 {
     StockAssistantModel model (stock_account);
-    model.transaction_date = gnc_dmy2time64 (1, 1, 2022);
+    model.m_transaction_date = gnc_dmy2time64 (1, 1, 2022);
 
     // resetting txn_types will work the first time
     EXPECT_TRUE (model.maybe_reset_txn_types ());
@@ -211,7 +211,7 @@ TEST_F(StockAssistantTest, testFailureModes)
     EXPECT_FALSE (model.maybe_reset_txn_types ());
 
     // set transaction-date to a different date.
-    model.transaction_date = gnc_dmy2time64 (1, 2, 2022);
+    model.m_transaction_date = gnc_dmy2time64 (1, 2, 2022);
 
     // resetting txn_types will now work.
     EXPECT_TRUE (model.maybe_reset_txn_types ());

commit cebc7198f2e46bb954c56e71ae1cab683c98c773
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Jan 27 16:46:46 2023 -0800

    [stock-txn-asst] Reformat StockAssistantModel
    
    Moving the ctor and dtor to immediately after the variables and
    moving the longer function defs out of the class decl.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index 69101149a2..457d7887bb 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -580,363 +580,382 @@ struct StockAssistantModel
     const gchar* capgains_memo = nullptr;
     gnc_numeric capgains_value = gnc_numeric_create (1, 0);
 
-    // consider reset txn_types. return false if txn_types are still
-    // current (i.e. transaction_date hasn't changed).
-    bool maybe_reset_txn_types ()
+    StockAssistantModel (Account *account) :
+     acct (account), currency (gnc_account_get_currency_or_parent (account)),
+     curr_pinfo (gnc_commodity_print_info (this->currency, true)),
+     price_pinfo (gnc_price_print_info (this->currency, true)),
+     stock_pinfo (gnc_commodity_print_info (xaccAccountGetCommodity (account), true))
     {
-        auto new_bal = xaccAccountGetBalanceAsOfDate
-            (this->acct, gnc_time64_get_day_end (this->transaction_date));
-        if (this->txn_types_date && this->txn_types_date == this->transaction_date &&
-            gnc_numeric_equal (this->balance_at_date, new_bal))
-            return false;
-        this->balance_at_date = new_bal;
-        this->txn_types_date = this->transaction_date;
-        this->txn_types = gnc_numeric_zero_p (this->balance_at_date) ? starting_types
-            : gnc_numeric_positive_p (this->balance_at_date) ? long_types
-            : short_types;
-        return true;
+        DEBUG ("StockAssistantModel constructor\n");
     };
 
-    bool set_txn_type (guint type_idx)
+    ~StockAssistantModel()
     {
-        if (!this->txn_types_date || this->txn_types_date != this->transaction_date)
-        {
-            PERR ("transaction_date has changed. rerun maybe_reset_txn_types!");
-            return false;
-        }
-        try
-        {
-            this->txn_type = this->txn_types->at (type_idx);
-        }
-        catch (const std::out_of_range&)
-        {
-            PERR ("out of range type_idx=%d", type_idx);
-            return false;
-        }
-        this->input_new_balance = this->txn_type->stock_amount & FieldMask::INPUT_NEW_BALANCE;
-        this->stock_amount_enabled = this->txn_type->stock_amount != FieldMask::DISABLED;
-        this->stock_value_enabled = this->txn_type->stock_value != FieldMask::DISABLED;
-        this->fees_capitalize = this->txn_type->fees_value & FieldMask::CAPITALIZE_DEFAULT;
-        this->fees_enabled = this->txn_type->fees_value != FieldMask::DISABLED;
-        this->capgains_enabled = this->txn_type->capgains_value != FieldMask::DISABLED;
-        this->dividend_enabled = this->txn_type->dividend_value != FieldMask::DISABLED;
-        this->cash_enabled = this->txn_type->cash_value != FieldMask::DISABLED;
-        return true;
+        DEBUG ("StockAssistantModel destructor\n");
     };
 
+    // consider reset txn_types. return false if txn_types are still
+    // current (i.e. transaction_date hasn't changed).
+    bool maybe_reset_txn_types ();
+    bool set_txn_type (guint type_idx);
     std::string get_stock_balance_str ()
     {
         return xaccPrintAmount (this->balance_at_date, this->stock_pinfo);
     };
 
-    std::string get_new_amount_str ()
+    std::string get_new_amount_str ();
+    std::tuple<bool, gnc_numeric, const char*> calculate_price ();
+    std::tuple<bool, std::string, SplitInfoVec> generate_list_of_splits ();
+    std::tuple<bool, Transaction*> create_transaction ();
+
+private:
+    std::optional<time64>     txn_types_date;
+    bool ready_to_create = false;
+
+    SplitInfoVec list_of_splits;
+
+    void add_price (QofBook *book);
+};
+
+bool
+StockAssistantModel::maybe_reset_txn_types ()
+{
+    auto new_bal = xaccAccountGetBalanceAsOfDate
+        (this->acct, gnc_time64_get_day_end (this->transaction_date));
+    if (this->txn_types_date && this->txn_types_date == this->transaction_date &&
+        gnc_numeric_equal (this->balance_at_date, new_bal))
+        return false;
+    this->balance_at_date = new_bal;
+    this->txn_types_date = this->transaction_date;
+    this->txn_types = gnc_numeric_zero_p (this->balance_at_date) ? starting_types
+        : gnc_numeric_positive_p (this->balance_at_date) ? long_types
+        : short_types;
+    return true;
+};
+
+bool
+StockAssistantModel::set_txn_type (guint type_idx)
+{
+    if (!this->txn_types_date || this->txn_types_date != this->transaction_date)
     {
-        if (gnc_numeric_check (this->stock_amount))
-            return "";
+        PERR ("transaction_date has changed. rerun maybe_reset_txn_types!");
+        return false;
+    }
+    try
+    {
+        this->txn_type = this->txn_types->at (type_idx);
+    }
+    catch (const std::out_of_range&)
+    {
+        PERR ("out of range type_idx=%d", type_idx);
+        return false;
+    }
+    this->input_new_balance = this->txn_type->stock_amount & FieldMask::INPUT_NEW_BALANCE;
+    this->stock_amount_enabled = this->txn_type->stock_amount != FieldMask::DISABLED;
+    this->stock_value_enabled = this->txn_type->stock_value != FieldMask::DISABLED;
+    this->fees_capitalize = this->txn_type->fees_value & FieldMask::CAPITALIZE_DEFAULT;
+    this->fees_enabled = this->txn_type->fees_value != FieldMask::DISABLED;
+    this->capgains_enabled = this->txn_type->capgains_value != FieldMask::DISABLED;
+    this->dividend_enabled = this->txn_type->dividend_value != FieldMask::DISABLED;
+    this->cash_enabled = this->txn_type->cash_value != FieldMask::DISABLED;
+    return true;
+};
 
-        if (this->input_new_balance)
-        {
-            auto ratio = gnc_numeric_div (this->stock_amount, this->balance_at_date,
-                                          GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
-            if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
-                return "";
-
-            std::ostringstream ret;
-            ret << ratio.num << ':' << ratio.denom;
-            return ret.str();
-        }
-        else
-        {
-            auto amount = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT) ?
-                gnc_numeric_neg (this->stock_amount) : this->stock_amount;
-            amount = gnc_numeric_add_fixed (amount, this->balance_at_date);
-            return xaccPrintAmount (amount, stock_pinfo);
-        }
-    };
+std::string
+StockAssistantModel::get_new_amount_str ()
+{
+    if (gnc_numeric_check (this->stock_amount))
+        return "";
 
-    std::tuple<bool, gnc_numeric, const char*> calculate_price ()
+    if (this->input_new_balance)
+    {
+        auto ratio = gnc_numeric_div (this->stock_amount, this->balance_at_date,
+                                      GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+        if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
+            return "";
+
+        std::ostringstream ret;
+        ret << ratio.num << ':' << ratio.denom;
+        return ret.str();
+    }
+    else
     {
-        if (this->input_new_balance ||
-            !this->stock_amount_enabled || gnc_numeric_check (this->stock_amount) ||
-            !this->stock_value_enabled || gnc_numeric_check (this->stock_value) ||
-            gnc_numeric_zero_p (this->stock_amount) ||
-            gnc_numeric_zero_p (this->stock_value))
-            return { false, gnc_numeric_create (1, 0), nullptr };
-
-        auto price = gnc_numeric_div (this->stock_value, this->stock_amount,
-                                      GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
-        return {true, price, xaccPrintAmount (price, this->price_pinfo)};
+        auto amount = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT) ?
+            gnc_numeric_neg (this->stock_amount) : this->stock_amount;
+        amount = gnc_numeric_add_fixed (amount, this->balance_at_date);
+        return xaccPrintAmount (amount, stock_pinfo);
     }
+};
 
+std::tuple<bool, gnc_numeric, const char*>
+StockAssistantModel::calculate_price ()
+{
+    if (this->input_new_balance ||
+        !this->stock_amount_enabled || gnc_numeric_check (this->stock_amount) ||
+        !this->stock_value_enabled || gnc_numeric_check (this->stock_value) ||
+        gnc_numeric_zero_p (this->stock_amount) ||
+        gnc_numeric_zero_p (this->stock_value))
+        return { false, gnc_numeric_create (1, 0), nullptr };
+
+    auto price = gnc_numeric_div (this->stock_value, this->stock_amount,
+                                  GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
+    return {true, price, xaccPrintAmount (price, this->price_pinfo)};
+}
 
-    std::tuple<bool, std::string, SplitInfoVec> generate_list_of_splits ()
+std::tuple<bool, std::string, SplitInfoVec>
+StockAssistantModel::generate_list_of_splits ()
+{
+    if (!this->txn_types || !this->txn_type)
+        return { false, "Error: txn_type not initialized", {} };
+
+    this->list_of_splits.clear();
+
+    gnc_numeric debit = gnc_numeric_zero ();
+    gnc_numeric credit = gnc_numeric_zero ();
+    StringVec errors, warnings, infos;
+    StockTransactionSplitInfo line;
+    bool negative_in_red = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL,
+                                               GNC_PREF_NEGATIVE_IN_RED);
+    auto add_error_str = [&errors]
+        (const char* str) { errors.emplace_back (_(str)); };
+
+    // check the stock transaction date. If there are existing stock
+    // transactions dated after the date specified, it is very likely
+    // the later stock transactions will be invalidated. warn the user
+    // to review them.
+    auto last_split_node = g_list_last (xaccAccountGetSplitList (this->acct));
+    if (last_split_node)
     {
-        if (!this->txn_types || !this->txn_type)
-            return { false, "Error: txn_type not initialized", {} };
-
-        this->list_of_splits.clear();
-
-        gnc_numeric debit = gnc_numeric_zero ();
-        gnc_numeric credit = gnc_numeric_zero ();
-        StringVec errors, warnings, infos;
-        StockTransactionSplitInfo line;
-        bool negative_in_red = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL,
-                                                   GNC_PREF_NEGATIVE_IN_RED);
-        auto add_error_str = [&errors]
-            (const char* str) { errors.emplace_back (_(str)); };
-
-        // check the stock transaction date. If there are existing stock
-        // transactions dated after the date specified, it is very likely
-        // the later stock transactions will be invalidated. warn the user
-        // to review them.
-        auto last_split_node = g_list_last (xaccAccountGetSplitList (this->acct));
-        if (last_split_node)
+        auto last_split = static_cast<const Split*> (last_split_node->data);
+        auto last_split_date = xaccTransGetDate (xaccSplitGetParent (last_split));
+        if (this->transaction_date <= last_split_date)
         {
-            auto last_split = static_cast<const Split*> (last_split_node->data);
-            auto last_split_date = xaccTransGetDate (xaccSplitGetParent (last_split));
-            if (this->transaction_date <= last_split_date)
-            {
-                auto last_split_date_str = qof_print_date (last_split_date);
-                auto new_date_str = qof_print_date (this->transaction_date);
-                // Translators: the first %s is the new transaction date;
-                // the second %s is the current stock account's latest
-                // transaction date.
-                auto warn_txt =  g_strdup_printf (_("You will enter a transaction \
+            auto last_split_date_str = qof_print_date (last_split_date);
+            auto new_date_str = qof_print_date (this->transaction_date);
+            // Translators: the first %s is the new transaction date;
+            // the second %s is the current stock account's latest
+            // transaction date.
+            auto warn_txt =  g_strdup_printf (_("You will enter a transaction \
 with date %s which is earlier than the latest transaction in this account, \
 dated %s. Doing so may affect the cost basis, and therefore capital gains, \
 of transactions dated after the new entry. Please review all transactions \
 to ensure proper recording."), new_date_str, last_split_date_str);
-                warnings.push_back (warn_txt);
-                g_free (warn_txt);
-                g_free (new_date_str);
-                g_free (last_split_date_str);
-            }
+            warnings.push_back (warn_txt);
+            g_free (warn_txt);
+            g_free (new_date_str);
+            g_free (last_split_date_str);
         }
+    }
 
-        if (!this->stock_value_enabled)
-            line = StockTransactionSplitInfo (this->acct, gnc_numeric_zero());
-        else
-            line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->stock_value,
-                                             this->acct, this->stock_memo, this->stock_value,
-                                             NC_ ("Stock Assistant: Page name", "stock value"),
-                                             this->curr_pinfo);
+    if (!this->stock_value_enabled)
+        line = StockTransactionSplitInfo (this->acct, gnc_numeric_zero());
+    else
+        line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->stock_value,
+                                         this->acct, this->stock_memo, this->stock_value,
+                                         NC_ ("Stock Assistant: Page name", "stock value"),
+                                         this->curr_pinfo);
 
 
-        if (!this->stock_amount_enabled)
-            line.m_units_numeric = gnc_numeric_zero();
-        else if (gnc_numeric_check (this->stock_amount))
-        {
-            line.m_units_str = _("(missing)");
-            line.m_units_numeric = gnc_numeric_zero();
-            add_error_str (N_("Amount for stock units is missing"));
-        }
-        else if (this->input_new_balance)
-        {
-            auto stock_amount = this->stock_amount;
-            auto credit_side = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT);
-            auto delta = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
-            auto ratio = gnc_numeric_div (stock_amount, this->balance_at_date,
-                                          GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
-            stock_amount = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
-            line.m_units_numeric = stock_amount;
-            line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
-            line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
-            if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
-                add_error_str (N_("Invalid stock new balance."));
-            else if (gnc_numeric_negative_p (delta) && !credit_side)
-                add_error_str (N_("New balance must be higher than old balance."));
-            else if (gnc_numeric_positive_p (delta) && credit_side)
-                add_error_str (N_("New balance must be lower than old balance."));
-        }
-        else
-        {
-            auto stock_amount = this->stock_amount;
-            if (!gnc_numeric_positive_p (stock_amount))
-                add_error_str (N_("Stock amount must be positive."));
-            if (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
-                stock_amount = gnc_numeric_neg (stock_amount);
-            line.m_units_numeric = stock_amount;
-            line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
-            line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
-            auto new_bal = gnc_numeric_add_fixed (this->balance_at_date, stock_amount);
-            if (gnc_numeric_positive_p (this->balance_at_date) &&
-                gnc_numeric_negative_p (new_bal))
-                add_error_str (N_("Cannot sell more units than owned."));
-            else if (gnc_numeric_negative_p (this->balance_at_date) &&
-                     gnc_numeric_positive_p (new_bal))
-                add_error_str (N_("Cannot cover buy more units than owed."));
-        }
+    if (!this->stock_amount_enabled)
+        line.m_units_numeric = gnc_numeric_zero();
+    else if (gnc_numeric_check (this->stock_amount))
+    {
+        line.m_units_str = _("(missing)");
+        line.m_units_numeric = gnc_numeric_zero();
+        add_error_str (N_("Amount for stock units is missing"));
+    }
+    else if (this->input_new_balance)
+    {
+        auto stock_amount = this->stock_amount;
+        auto credit_side = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT);
+        auto delta = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
+        auto ratio = gnc_numeric_div (stock_amount, this->balance_at_date,
+                                      GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+        stock_amount = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
+        line.m_units_numeric = stock_amount;
+        line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+        line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
+        if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
+            add_error_str (N_("Invalid stock new balance."));
+        else if (gnc_numeric_negative_p (delta) && !credit_side)
+            add_error_str (N_("New balance must be higher than old balance."));
+        else if (gnc_numeric_positive_p (delta) && credit_side)
+            add_error_str (N_("New balance must be lower than old balance."));
+    }
+    else
+    {
+        auto stock_amount = this->stock_amount;
+        if (!gnc_numeric_positive_p (stock_amount))
+            add_error_str (N_("Stock amount must be positive."));
+        if (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
+            stock_amount = gnc_numeric_neg (stock_amount);
+        line.m_units_numeric = stock_amount;
+        line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+        line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
+        auto new_bal = gnc_numeric_add_fixed (this->balance_at_date, stock_amount);
+        if (gnc_numeric_positive_p (this->balance_at_date) &&
+            gnc_numeric_negative_p (new_bal))
+            add_error_str (N_("Cannot sell more units than owned."));
+        else if (gnc_numeric_negative_p (this->balance_at_date) &&
+                 gnc_numeric_positive_p (new_bal))
+            add_error_str (N_("Cannot cover buy more units than owed."));
+    }
 
-        this->list_of_splits.push_back (std::move (line));
+    this->list_of_splits.push_back (std::move (line));
 
-        auto [has_price, price, price_str] = this->calculate_price ();
-        if (has_price)
-        {
-            // Translators: %s refer to: stock mnemonic, broker currency,
-            // date of transaction.
-            auto tmpl = N_("A price of 1 %s = %s on %s will be recorded.");
-            auto date_str = qof_print_date (this->transaction_date);
-            auto price_msg = g_strdup_printf
-                (_(tmpl),
-                 gnc_commodity_get_mnemonic (xaccAccountGetCommodity (this->acct)),
-                 price_str, date_str);
-            infos.emplace_back (price_msg);
-            g_free (date_str);
-        }
+    auto [has_price, price, price_str] = this->calculate_price ();
+    if (has_price)
+    {
+        // Translators: %s refer to: stock mnemonic, broker currency,
+        // date of transaction.
+        auto tmpl = N_("A price of 1 %s = %s on %s will be recorded.");
+        auto date_str = qof_print_date (this->transaction_date);
+        auto price_msg = g_strdup_printf
+            (_(tmpl),
+             gnc_commodity_get_mnemonic (xaccAccountGetCommodity (this->acct)),
+             price_str, date_str);
+        infos.emplace_back (price_msg);
+        g_free (date_str);
+    }
 
-        if (this->cash_enabled)
-        {
-            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->cash_value,
-                                              this->cash_account, this->cash_memo, this->cash_value,
-                                              NC_ ("Stock Assistant: Page name", "cash"),
-                                              this->curr_pinfo);
-            this->list_of_splits.push_back (std::move (line));
-        }
+    if (this->cash_enabled)
+    {
+        line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->cash_value,
+                                          this->cash_account, this->cash_memo, this->cash_value,
+                                          NC_ ("Stock Assistant: Page name", "cash"),
+                                          this->curr_pinfo);
+        this->list_of_splits.push_back (std::move (line));
+    }
 
-        if (this->fees_enabled)
-        {
-            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->fees_value,
-                                              this->fees_capitalize ? this->acct : this->fees_account,
-                                              this->fees_memo, this->fees_value,
-                                              NC_ ("Stock Assistant: Page name", "fees"),
-                                              this->curr_pinfo);
-            if (this->fees_capitalize)
-                line.m_units_numeric = gnc_numeric_zero();
-            this->list_of_splits.push_back (std::move (line));
-        }
+    if (this->fees_enabled)
+    {
+        line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->fees_value,
+                                          this->fees_capitalize ? this->acct : this->fees_account,
+                                          this->fees_memo, this->fees_value,
+                                          NC_ ("Stock Assistant: Page name", "fees"),
+                                          this->curr_pinfo);
+        if (this->fees_capitalize)
+            line.m_units_numeric = gnc_numeric_zero();
+        this->list_of_splits.push_back (std::move (line));
+    }
 
-        if (this->dividend_enabled)
-        {
-            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->dividend_value,
-                                              this->dividend_account, this->dividend_memo,
-                                              this->dividend_value,
-                                              NC_ ("Stock Assistant: Page name", "dividend"),
-                                              this->curr_pinfo);
-            this->list_of_splits.push_back (std::move (line));
-        }
+    if (this->dividend_enabled)
+    {
+        line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->dividend_value,
+                                          this->dividend_account, this->dividend_memo,
+                                          this->dividend_value,
+                                          NC_ ("Stock Assistant: Page name", "dividend"),
+                                          this->curr_pinfo);
+        this->list_of_splits.push_back (std::move (line));
+    }
 
-        // 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 (this->capgains_enabled)
+    // 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 (this->capgains_enabled)
+    {
+        if (this->txn_type->capgains_value & FieldMask::CAPGAINS_IN_STOCK)
         {
-            if (this->txn_type->capgains_value & FieldMask::CAPGAINS_IN_STOCK)
-            {
-                line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->capgains_value ^
-                                                  (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
-                                                  this->acct, this->capgains_memo, this->capgains_value,
-                                                  NC_ ("Stock Assistant: Page name", "capital gains"),
-                                   this->curr_pinfo);
-                line.m_units_numeric = gnc_numeric_zero();
-                this->list_of_splits.push_back (std::move (line));
-            }
-
-            line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->capgains_value,
-                                             this->capgains_account, this->capgains_memo,
-                                             this->capgains_value,
-                                             NC_ ("Stock Assistant: Page name", "capital gains"),
-                                             this->curr_pinfo);
+            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->capgains_value ^
+                                              (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
+                                              this->acct, this->capgains_memo, this->capgains_value,
+                                              NC_ ("Stock Assistant: Page name", "capital gains"),
+                                              this->curr_pinfo);
+            line.m_units_numeric = gnc_numeric_zero();
             this->list_of_splits.push_back (std::move (line));
         }
 
-        if (!gnc_numeric_equal (debit, credit))
-        {
-            auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
-            auto debit_str = g_strdup (xaccPrintAmount (debit, this->curr_pinfo));
-            auto credit_str = g_strdup (xaccPrintAmount (credit, this->curr_pinfo));
-            auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
-            errors.emplace_back (error_str);
-            g_free (error_str);
-            g_free (credit_str);
-            g_free (debit_str);
-        }
-
-        // generate final summary message. Collates a header, the errors
-        // and warnings. Then allow completion if errors is empty.
-        std::ostringstream summary;
-        auto summary_add = [&summary](auto a) { summary << "\n• " << a; };
-        if (errors.empty())
-        {
-            summary << _("No errors found. Click Apply to create transaction.");
-            std::for_each (infos.begin(), infos.end(), summary_add);
-        }
-        else
-        {
-            summary << _("The following errors must be fixed:");
-            std::for_each (errors.begin(), errors.end(), summary_add);
-        }
-        if (!warnings.empty())
-        {
-            summary << "\n\n" << _("The following warnings exist:");
-            std::for_each (warnings.begin(), warnings.end(), summary_add);
-        }
-        this->ready_to_create = errors.empty();
-        return { this->ready_to_create, summary.str(), this->list_of_splits };
+        line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->capgains_value,
+                                         this->capgains_account, this->capgains_memo,
+                                         this->capgains_value,
+                                         NC_ ("Stock Assistant: Page name", "capital gains"),
+                                         this->curr_pinfo);
+        this->list_of_splits.push_back (std::move (line));
     }
 
-    std::tuple<bool, Transaction*> create_transaction ()
+    if (!gnc_numeric_equal (debit, credit))
     {
-        if (!this->ready_to_create)
-        {
-            PERR ("errors exist. cannot create transaction.");
-            return { false, nullptr };
-        }
-        auto book = qof_instance_get_book (acct);
-        auto trans = xaccMallocTransaction (book);
-        xaccTransBeginEdit (trans);
-        xaccTransSetCurrency (trans, this->currency);
-        xaccTransSetDescription (trans, this->transaction_description);
-        xaccTransSetDatePostedSecsNormalized (trans, this->transaction_date);
-        AccountVec accounts;
-        std::for_each (this->list_of_splits.begin(), this->list_of_splits.end(),
-                       [&](auto& line){ line.create_split (trans, accounts); });
-        this->add_price (book);
-        xaccTransCommitEdit (trans);
-        std::for_each (accounts.begin(), accounts.end(), xaccAccountCommitEdit);
-        this->ready_to_create = false;
-        return { true, trans };
+        auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
+        auto debit_str = g_strdup (xaccPrintAmount (debit, this->curr_pinfo));
+        auto credit_str = g_strdup (xaccPrintAmount (credit, this->curr_pinfo));
+        auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
+        errors.emplace_back (error_str);
+        g_free (error_str);
+        g_free (credit_str);
+        g_free (debit_str);
     }
 
-    StockAssistantModel (Account *account)
-        : acct (account)
-        , currency (gnc_account_get_currency_or_parent (account))
-        , curr_pinfo (gnc_commodity_print_info (this->currency, true))
-        , price_pinfo (gnc_price_print_info (this->currency, true))
-        , stock_pinfo (gnc_commodity_print_info (xaccAccountGetCommodity (account), true))
-    { DEBUG ("StockAssistantModel constructor\n"); };
-
-    ~StockAssistantModel(){ DEBUG ("StockAssistantModel destructor\n"); };
-
-private:
-    std::optional<time64>     txn_types_date;
-    bool ready_to_create = false;
-
-    SplitInfoVec list_of_splits;
+    // generate final summary message. Collates a header, the errors
+    // and warnings. Then allow completion if errors is empty.
+    std::ostringstream summary;
+    auto summary_add = [&summary](auto a) { summary << "\n• " << a; };
+    if (errors.empty())
+    {
+        summary << _("No errors found. Click Apply to create transaction.");
+        std::for_each (infos.begin(), infos.end(), summary_add);
+    }
+    else
+    {
+        summary << _("The following errors must be fixed:");
+        std::for_each (errors.begin(), errors.end(), summary_add);
+    }
+    if (!warnings.empty())
+    {
+        summary << "\n\n" << _("The following warnings exist:");
+        std::for_each (warnings.begin(), warnings.end(), summary_add);
+    }
+    this->ready_to_create = errors.empty();
+    return { this->ready_to_create, summary.str(), this->list_of_splits };
+}
 
-    void add_price (QofBook *book)
+std::tuple<bool, Transaction*>
+StockAssistantModel::create_transaction ()
+{
+    if (!this->ready_to_create)
     {
-        auto [has_price, p, price_str] = this->calculate_price ();
-        if (!has_price)
-            return;
-
-        auto price = gnc_price_create (book);
-        gnc_price_begin_edit (price);
-        gnc_price_set_commodity (price, xaccAccountGetCommodity (this->acct));
-        gnc_price_set_currency (price, this->currency);
-        gnc_price_set_time64 (price, this->transaction_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 pdb = gnc_pricedb_get_db (book);
-        if (!gnc_pricedb_add_price (pdb, price))
-            PWARN ("error adding price");
-
-        gnc_price_unref (price);
+        PERR ("errors exist. cannot create transaction.");
+        return { false, nullptr };
     }
-};
+    auto book = qof_instance_get_book (acct);
+    auto trans = xaccMallocTransaction (book);
+    xaccTransBeginEdit (trans);
+    xaccTransSetCurrency (trans, this->currency);
+    xaccTransSetDescription (trans, this->transaction_description);
+    xaccTransSetDatePostedSecsNormalized (trans, this->transaction_date);
+    AccountVec accounts;
+    std::for_each (this->list_of_splits.begin(), this->list_of_splits.end(),
+                   [&](auto& line){ line.create_split (trans, accounts); });
+    this->add_price (book);
+    xaccTransCommitEdit (trans);
+    std::for_each (accounts.begin(), accounts.end(), xaccAccountCommitEdit);
+    this->ready_to_create = false;
+    return { true, trans };
+}
+
+void
+StockAssistantModel::add_price (QofBook *book)
+{
+    auto [has_price, p, price_str] = this->calculate_price ();
+    if (!has_price)
+        return;
+
+    auto price = gnc_price_create (book);
+    gnc_price_begin_edit (price);
+    gnc_price_set_commodity (price, xaccAccountGetCommodity (this->acct));
+    gnc_price_set_currency (price, this->currency);
+    gnc_price_set_time64 (price, this->transaction_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 pdb = gnc_pricedb_get_db (book);
+    if (!gnc_pricedb_add_price (pdb, price))
+        PWARN ("error adding price");
+
+    gnc_price_unref (price);
+}
 
 
 struct StockAssistantView

commit 925999e070e93bd5602f5ae90a6522ca7442fb7e
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Jan 27 15:45:17 2023 -0800

    [stock-txn-asst] Refactor check_page into a constructor.
    
    Because it is one.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index ea78ef8e5e..69101149a2 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -413,48 +413,36 @@ struct StockTransactionSplitInfo
     gnc_numeric m_value_numeric = gnc_numeric_create (1, 0); // invalid gnc_numerics
     gnc_numeric m_units_numeric = gnc_numeric_create (1, 0);
 
+    static const char* s_missing_str;
+
     StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo constructor\n"); };
     StockTransactionSplitInfo (Account *acct, gnc_numeric val)
         : m_account_str{xaccAccountGetName (acct)} , m_account{acct} , m_value_numeric{val}
     { DEBUG ("StockTransactionSplitInfo constructor\n"); }
+    StockTransactionSplitInfo(gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
+                              FieldMask splitfield, Account *acct, const char *memo,
+                              gnc_numeric amount, const char* page, GNCPrintAmountInfo curr_pinfo);
     ~StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo destructor\n"); }
     void create_split(Transaction *trans, AccountVec &account_commits);
 };
 
-void
-StockTransactionSplitInfo::create_split(Transaction *trans, AccountVec &account_commits) {
-  g_return_if_fail(trans);
-  if (!m_account || gnc_numeric_check(m_value_numeric) ||
-      gnc_numeric_check(m_units_numeric))
-    return;
-  auto split = xaccMallocSplit(qof_instance_get_book(trans));
-  xaccSplitSetParent(split, trans);
-  xaccAccountBeginEdit(m_account);
-  account_commits.emplace_back(m_account);
-  xaccSplitSetAccount(split, m_account);
-  xaccSplitSetMemo(split, m_memo_str.c_str());
-  xaccSplitSetValue(split, m_value_numeric);
-  xaccSplitSetAmount(split, m_units_numeric);
-  DEBUG("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
-        m_action_str.c_str(), m_account_str.c_str(),
-        gnc_num_dbg_to_string(m_value_numeric),
-        gnc_num_dbg_to_string(m_units_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",
-                                  m_action_str.c_str()));
-}
+// Translators: (missing) denotes that the amount or account is
+// not provided, or incorrect, in the Stock Transaction Assistant.
+const char* StockTransactionSplitInfo::s_missing_str = N_("(missing)");
+
+StockTransactionSplitInfo::StockTransactionSplitInfo (gnc_numeric& debit, gnc_numeric& credit,
+                                                      StringVec& errors, FieldMask splitfield,
+                                                      Account *acct, const char *memo,
+                                                      gnc_numeric amount, const char* page,
+                                                      GNCPrintAmountInfo curr_pinfo) :
+    m_debit_side{splitfield & FieldMask::ENABLED_DEBIT},
+    m_account_str{acct ? xaccAccountGetName (acct) : ""},
+    m_memo_str{memo ? memo : ""},
+    m_action_str{page ? page : ""},
+    m_value_str{gnc_numeric_check(amount) ? "" : xaccPrintAmount (amount, curr_pinfo)},
+    m_account{acct}
 
-static StockTransactionSplitInfo
-check_page (gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
-            FieldMask splitfield, Account *acct, const char *memo,
-            gnc_numeric amount, const char* page, GNCPrintAmountInfo curr_pinfo)
 {
-    // Translators: (missing) denotes that the amount or account is
-    // not provided, or incorrect, in the Stock Transaction Assistant.
-    const char* missing_str = N_("(missing)");
-    auto line = StockTransactionSplitInfo ();
     auto add_error = [&errors](const char* format_str, const char* arg)
     {
         gchar *buf = g_strdup_printf (_(format_str),
@@ -465,20 +453,20 @@ check_page (gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
 
     DEBUG ("page=%s, amount=%s", page, gnc_num_dbg_to_string (amount));
     if (memo)
-        line.m_memo_str = memo;
-    line.m_debit_side = (splitfield & FieldMask::ENABLED_DEBIT);
+        m_memo_str = memo;
+    m_debit_side = (splitfield & FieldMask::ENABLED_DEBIT);
     if (page)
-        line.m_action_str = page;
+        m_action_str = page;
 
     if (gnc_numeric_check (amount))
     {
         if (splitfield & FieldMask::ALLOW_ZERO)
-            // line.value_numeric contains an invalid gnc_numeric
-            line.m_value_str = "";
+            // m_value_numeric contains an invalid gnc_numeric
+            m_value_str = "";
         else
         {
             add_error (N_("Amount for %s is missing."), page);
-            line.m_value_str = _(missing_str);
+            m_value_str = _(s_missing_str);
         }
     }
     else
@@ -493,31 +481,55 @@ check_page (gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
         if (gnc_numeric_negative_p (amount))
         {
             amount = gnc_numeric_neg (amount);
-            line.m_debit_side = !line.m_debit_side;
+            m_debit_side = !m_debit_side;
         }
-        if (line.m_debit_side)
+        if (m_debit_side)
             debit = gnc_numeric_add_fixed (debit, amount);
         else
             credit = gnc_numeric_add_fixed (credit, amount);
-        line.m_units_numeric = line.m_debit_side ? amount : gnc_numeric_neg (amount);
-        line.m_value_numeric = line.m_debit_side ? amount : gnc_numeric_neg (amount);
-        line.m_value_str = xaccPrintAmount (amount, curr_pinfo);
+        m_units_numeric = m_debit_side ? amount : gnc_numeric_neg (amount);
+        m_value_numeric = m_debit_side ? amount : gnc_numeric_neg (amount);
+        m_value_str = xaccPrintAmount (amount, curr_pinfo);
     }
 
     if (acct)
     {
-        line.m_account = acct;
-        line.m_account_str = xaccAccountGetName (acct);
+        m_account = acct;
+        m_account_str = xaccAccountGetName (acct);
     }
     else if ((splitfield & FieldMask::ALLOW_ZERO) &&
              (gnc_numeric_check (amount) || gnc_numeric_zero_p (amount)))
-        line.m_account_str = "";
+        m_account_str = "";
     else
     {
         add_error (N_("Account for %s is missing."), page);
-        line.m_account_str = _(missing_str);
+        m_account_str = _(s_missing_str);
     }
-    return line;
+}
+
+void
+StockTransactionSplitInfo::create_split(Transaction *trans, AccountVec &account_commits) {
+  g_return_if_fail(trans);
+  if (!m_account || gnc_numeric_check(m_value_numeric) ||
+      gnc_numeric_check(m_units_numeric))
+    return;
+  auto split = xaccMallocSplit(qof_instance_get_book(trans));
+  xaccSplitSetParent(split, trans);
+  xaccAccountBeginEdit(m_account);
+  account_commits.emplace_back(m_account);
+  xaccSplitSetAccount(split, m_account);
+  xaccSplitSetMemo(split, m_memo_str.c_str());
+  xaccSplitSetValue(split, m_value_numeric);
+  xaccSplitSetAmount(split, m_units_numeric);
+  DEBUG("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
+        m_action_str.c_str(), m_account_str.c_str(),
+        gnc_num_dbg_to_string(m_value_numeric),
+        gnc_num_dbg_to_string(m_units_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",
+                                  m_action_str.c_str()));
 }
 
 using SplitInfoVec = std::vector<StockTransactionSplitInfo>;
@@ -704,10 +716,10 @@ to ensure proper recording."), new_date_str, last_split_date_str);
         if (!this->stock_value_enabled)
             line = StockTransactionSplitInfo (this->acct, gnc_numeric_zero());
         else
-            line = check_page (debit, credit, errors, this->txn_type->stock_value,
-                               this->acct, this->stock_memo, this->stock_value,
-                               NC_ ("Stock Assistant: Page name", "stock value"),
-                               this->curr_pinfo);
+            line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->stock_value,
+                                             this->acct, this->stock_memo, this->stock_value,
+                                             NC_ ("Stock Assistant: Page name", "stock value"),
+                                             this->curr_pinfo);
 
 
         if (!this->stock_amount_enabled)
@@ -774,20 +786,20 @@ to ensure proper recording."), new_date_str, last_split_date_str);
 
         if (this->cash_enabled)
         {
-            line = check_page (debit, credit, errors, this->txn_type->cash_value,
-                               this->cash_account, this->cash_memo, this->cash_value,
-                               NC_ ("Stock Assistant: Page name", "cash"),
-                               this->curr_pinfo);
+            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->cash_value,
+                                              this->cash_account, this->cash_memo, this->cash_value,
+                                              NC_ ("Stock Assistant: Page name", "cash"),
+                                              this->curr_pinfo);
             this->list_of_splits.push_back (std::move (line));
         }
 
         if (this->fees_enabled)
         {
-            line = check_page (debit, credit, errors, this->txn_type->fees_value,
-                               this->fees_capitalize ? this->acct : this->fees_account,
-                               this->fees_memo, this->fees_value,
-                               NC_ ("Stock Assistant: Page name", "fees"),
-                               this->curr_pinfo);
+            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->fees_value,
+                                              this->fees_capitalize ? this->acct : this->fees_account,
+                                              this->fees_memo, this->fees_value,
+                                              NC_ ("Stock Assistant: Page name", "fees"),
+                                              this->curr_pinfo);
             if (this->fees_capitalize)
                 line.m_units_numeric = gnc_numeric_zero();
             this->list_of_splits.push_back (std::move (line));
@@ -795,11 +807,11 @@ to ensure proper recording."), new_date_str, last_split_date_str);
 
         if (this->dividend_enabled)
         {
-            line = check_page (debit, credit, errors, this->txn_type->dividend_value,
-                               this->dividend_account, this->dividend_memo,
-                               this->dividend_value,
-                               NC_ ("Stock Assistant: Page name", "dividend"),
-                               this->curr_pinfo);
+            line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->dividend_value,
+                                              this->dividend_account, this->dividend_memo,
+                                              this->dividend_value,
+                                              NC_ ("Stock Assistant: Page name", "dividend"),
+                                              this->curr_pinfo);
             this->list_of_splits.push_back (std::move (line));
         }
 
@@ -811,20 +823,20 @@ to ensure proper recording."), new_date_str, last_split_date_str);
         {
             if (this->txn_type->capgains_value & FieldMask::CAPGAINS_IN_STOCK)
             {
-                line = check_page (debit, credit, errors, this->txn_type->capgains_value ^
-                                   (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
-                                   this->acct, this->capgains_memo, this->capgains_value,
-                                   NC_ ("Stock Assistant: Page name", "capital gains"),
+                line = StockTransactionSplitInfo (debit, credit, errors, this->txn_type->capgains_value ^
+                                                  (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
+                                                  this->acct, this->capgains_memo, this->capgains_value,
+                                                  NC_ ("Stock Assistant: Page name", "capital gains"),
                                    this->curr_pinfo);
                 line.m_units_numeric = gnc_numeric_zero();
                 this->list_of_splits.push_back (std::move (line));
             }
 
-            line = check_page (debit, credit, errors, this->txn_type->capgains_value,
-                               this->capgains_account, this->capgains_memo,
-                               this->capgains_value,
-                               NC_ ("Stock Assistant: Page name", "capital gains"),
-                               this->curr_pinfo);
+            line = StockTransactionSplitInfo(debit, credit, errors, this->txn_type->capgains_value,
+                                             this->capgains_account, this->capgains_memo,
+                                             this->capgains_value,
+                                             NC_ ("Stock Assistant: Page name", "capital gains"),
+                                             this->curr_pinfo);
             this->list_of_splits.push_back (std::move (line));
         }
 

commit 4ebc028740e70ac760e4d2b105b9391b645be5b0
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Jan 27 15:23:25 2023 -0800

    [stock-txn-asst] Refactor gtest-assistant-stock-transaction.cpp
    
    For better readability. Removes gratuitous parameterized test
    suite because the StockAssistantModel is stateful and can't
    use easyTestCases when it and the book are re-created for
    each instance, as is the case when the easyTestCases is passed
    as a parameter set to a test suite.
    
    Also replaced the shared_ptr<QofBook> with a unique_ptr.

diff --git a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
index 089b4cc307..32c3266327 100644
--- a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
+++ b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
@@ -35,7 +35,7 @@ extern "C"
 #include <gnc-pricedb-p.h>
 }
 
-struct Tests
+struct ASTTestCase
 {
     unsigned int type_idx, dd, mm, yy;
     const char *desc;
@@ -44,123 +44,162 @@ struct Tests
     int fees_val, divi_val, capg_val, new_bal;
 };
 
-struct TestCase {
-    std::vector<Tests> tests;
+ASTTestCase easyTestCases[] = {
+    //t, dd, mm, yyyy, desc               , stk.amt, stk.val , cash    , capitalize, fees, divi , capg  , new_bal
+
+    // bal=0. next line is "open long".
+    { 0, 1 , 7 , 2019, "Buy"              , 100    , 2000000 , 2000995 , true      , 995 , 0    , 0     , 100 },
+
+    // bal>0. next lines are long_types
+    { 0, 11, 12, 2019, "Buy"              , 50     , 1600000 , 1600995 , true      , 995 , 0    , 0     , 150 },
+    { 1, 18, 3 , 2020, "Sell"             , 75     , 1200000 , 1199005 , false     , 995 , 0    ,-600995, 75 },
+    { 0, 1 , 4 , 2020, "Buy"              , 250    , 4200000 , 4200995 , true      , 995 , 0    , 0     , 325 },
+    { 3, 16, 4 , 2020, "ROC"              , 0      , 250000  , 250000  , true      , 0   , 0    , 0     , 325 },
+    { 0, 2 , 5 , 2020, "Buy"              , 125    , 4750000 , 4750000 , true      , 0   , 0    , 0     , 450 },
+    { 7, 11, 5 , 2020, "Split 2:1"        , 900    , 0       , 0       , true      , 0   , 0    , 0     , 900 },
+    { 1, 21, 5 , 2020, "Sell"             , 135    , 2150000 , 2149005 , false     , 995 , 0    , 574702, 765 },
+    { 0, 3 , 6 , 2020, "Buy"              , 150    , 2100000 , 2100000 , true      , 0   , 0    , 0     , 915 },
+    { 1, 10, 6 , 2020, "Sell"             , 915    , 12810000, 12809005, false     , 995 , 0    , 1783309, 0 },
+
+    // bal=0. next line is "open short".
+    { 1, 10, 6 , 2020, "Short Sell"       , 85     , 1190000 , 1189005 , true      , 995 , 0    , 0     , -85 },
+
+    // bal<0. next lines are short_types
+    { 0, 15, 6 , 2020, "Short Sell"       , 65     , 1105000 , 1104005 , true      , 995 , 0    , 0     , -150 },
+    { 1, 16, 6 , 2020, "Cover Buy"        , 50     , 500000  , 500995  , false     , 995 , 0    ,-264337, -100 },
+    { 7, 17, 6 , 2020, "Split 2:1"        , -200   , 0       , 0       , false     , 0   , 0    , 0     , -200 },
+    { 8, 18, 6 , 2020, "Reverse Split"    , -100   , 0       , 0       , false     , 0   , 0    , 0     , -100 },
+    { 2, 19, 6 , 2020, "Comp Dividend"    , 0      , 0       , 50000   , false     , 0   , 50000, 0     , -100 },
+    { 3, 19, 6 , 2020, "Comp ROC"         , 0      , 250000  , 250000  , false     , 0   , 0    , 0     , -100 },
+    { 5, 19, 6 , 2020, "Comp ND"          , 0      , 20000   , 0       , false     , 0   , 20000, 0     , -100 },
+    { 1, 20, 6 , 2020, "Cover Buy"        , 100    , 800000  , 800498  , false     , 498 , 0    ,-498673, 0 },
+
+    // bal=0. next line is "open long".
+    { 0, 20, 6 , 2020, "Buy"              , 100    , 800000  , 800498  , true      , 498 , 0    , 0     , 100 },
+
+    // bal>0. next lines are long_types
+    { 2, 21, 6 , 2020, "Dividend"         , 0      , 0       , 7000    , false     , 0   , 7000 , 0     , 100 },
+    { 2, 25, 6 , 2020, "Dividend"         , 0      , 0       , 11000   , false     , 0   , 11000, 0     , 100 },
+    { 0, 25, 6 , 2020, "+ Reinv"          , 1      , 10000   , 10000   , false     , 0   , 0    , 0     , 101 },
+    { 1, 26, 6 , 2020, "Sell remainder"   , 1      , 10000   , 10000   , false     , 0   , 0    , 1975  , 100 },
+    { 8, 26, 6 , 2020, "Reverse Split 1:2", 50     , 0       , 0       , false     , 0   , 0    , 0     , 50 },
+    { 5, 27, 6 , 2020, "ND"               , 0      , 10000   , 0       , false     , 0   , 10000, 0     , 50 }
 };
 
-TestCase easyTestCase = {
-    .tests =
+struct DestroyBook
+{
+    void operator()(QofBook* book)
     {
-        //t, dd, mm, yyyy, desc               , stk.amt, stk.val , cash    , capitalize, fees, divi , capg  , new_bal
-
-        // bal=0. next line is "open long".
-        { 0, 1 , 7 , 2019, "Buy"              , 100    , 2000000 , 2000995 , true      , 995 , 0    , 0     , 100 },
-
-        // bal>0. next lines are long_types
-        { 0, 11, 12, 2019, "Buy"              , 50     , 1600000 , 1600995 , true      , 995 , 0    , 0     , 150 },
-        { 1, 18, 3 , 2020, "Sell"             , 75     , 1200000 , 1199005 , false     , 995 , 0    ,-600995, 75 },
-        { 0, 1 , 4 , 2020, "Buy"              , 250    , 4200000 , 4200995 , true      , 995 , 0    , 0     , 325 },
-        { 3, 16, 4 , 2020, "ROC"              , 0      , 250000  , 250000  , true      , 0   , 0    , 0     , 325 },
-        { 0, 2 , 5 , 2020, "Buy"              , 125    , 4750000 , 4750000 , true      , 0   , 0    , 0     , 450 },
-        { 7, 11, 5 , 2020, "Split 2:1"        , 900    , 0       , 0       , true      , 0   , 0    , 0     , 900 },
-        { 1, 21, 5 , 2020, "Sell"             , 135    , 2150000 , 2149005 , false     , 995 , 0    , 574702, 765 },
-        { 0, 3 , 6 , 2020, "Buy"              , 150    , 2100000 , 2100000 , true      , 0   , 0    , 0     , 915 },
-        { 1, 10, 6 , 2020, "Sell"             , 915    , 12810000, 12809005, false     , 995 , 0    , 1783309, 0 },
-
-        // bal=0. next line is "open short".
-        { 1, 10, 6 , 2020, "Short Sell"       , 85     , 1190000 , 1189005 , true      , 995 , 0    , 0     , -85 },
-
-        // bal<0. next lines are short_types
-        { 0, 15, 6 , 2020, "Short Sell"       , 65     , 1105000 , 1104005 , true      , 995 , 0    , 0     , -150 },
-        { 1, 16, 6 , 2020, "Cover Buy"        , 50     , 500000  , 500995  , false     , 995 , 0    ,-264337, -100 },
-        { 7, 17, 6 , 2020, "Split 2:1"        , -200   , 0       , 0       , false     , 0   , 0    , 0     , -200 },
-        { 8, 18, 6 , 2020, "Reverse Split"    , -100   , 0       , 0       , false     , 0   , 0    , 0     , -100 },
-        { 2, 19, 6 , 2020, "Comp Dividend"    , 0      , 0       , 50000   , false     , 0   , 50000, 0     , -100 },
-        { 3, 19, 6 , 2020, "Comp ROC"         , 0      , 250000  , 250000  , false     , 0   , 0    , 0     , -100 },
-        { 5, 19, 6 , 2020, "Comp ND"          , 0      , 20000   , 0       , false     , 0   , 20000, 0     , -100 },
-        { 1, 20, 6 , 2020, "Cover Buy"        , 100    , 800000  , 800498  , false     , 498 , 0    ,-498673, 0 },
-
-        // bal=0. next line is "open long".
-        { 0, 20, 6 , 2020, "Buy"              , 100    , 800000  , 800498  , true      , 498 , 0    , 0     , 100 },
-
-        // bal>0. next lines are long_types
-        { 2, 21, 6 , 2020, "Dividend"         , 0      , 0       , 7000    , false     , 0   , 7000 , 0     , 100 },
-        { 2, 25, 6 , 2020, "Dividend"         , 0      , 0       , 11000   , false     , 0   , 11000, 0     , 100 },
-        { 0, 25, 6 , 2020, "+ Reinv"          , 1      , 10000   , 10000   , false     , 0   , 0    , 0     , 101 },
-        { 1, 26, 6 , 2020, "Sell remainder"   , 1      , 10000   , 10000   , false     , 0   , 0    , 1975  , 100 },
-        { 8, 26, 6 , 2020, "Reverse Split 1:2", 50     , 0       , 0       , false     , 0   , 0    , 0     , 50 },
-        { 5, 27, 6 , 2020, "ND"               , 0      , 10000   , 0       , false     , 0   , 10000, 0     , 50 }
+        qof_book_destroy(book);
     }
 };
 
-class Stock_AssistantTest : public ::testing::TestWithParam<TestCase *> {
+using QofBookPtr = std::unique_ptr<QofBook, DestroyBook>;
+
+class StockAssistantTest : public ::testing::Test{
 protected:
-    std::shared_ptr<QofBook> m_book;
+    QofBookPtr m_book;
     gnc_commodity *stock_commodity, *USD;
     Account *broker_account, *stock_account, *cash_account, *dividend_account,
         *capgains_account, *fees_account;
-    TestCase &m_testCase;
 
-public:
-    Stock_AssistantTest() :
-        m_book (qof_book_new (), qof_book_destroy),
+    StockAssistantTest();
+    void instantiate_model(StockAssistantModel &model, const ASTTestCase &t);
+};
+
+StockAssistantTest::StockAssistantTest() :
+        m_book (qof_book_new ()),
         broker_account (xaccMallocAccount (m_book.get ())),
         stock_account (xaccMallocAccount (m_book.get ())),
         cash_account (xaccMallocAccount (m_book.get ())),
         dividend_account (xaccMallocAccount (m_book.get ())),
         capgains_account (xaccMallocAccount (m_book.get ())),
-        fees_account (xaccMallocAccount (m_book.get ())),
-        m_testCase (*GetParam ())
+        fees_account (xaccMallocAccount (m_book.get ()))
+{
+    qof_init();
+    qof_book_register ();
+    gnc_pricedb_register();
+    gnc_commodity_table_register ();
+
+    stock_commodity = gnc_commodity_new (m_book.get(), "SPY", "", "SPY", "", 100);
+    USD = gnc_commodity_table_lookup (gnc_commodity_table_get_table (m_book.get()),
+                                      "CURRENCY", "USD");
+
+    xaccAccountBeginEdit (broker_account);
+    xaccAccountSetName (broker_account, "Broker Account");
+    xaccAccountSetType (broker_account, ACCT_TYPE_CASH);
+    xaccAccountSetCommodity (broker_account, USD);
+
+    xaccAccountBeginEdit (stock_account);
+    xaccAccountSetName (stock_account, "Stock Account");
+    xaccAccountSetType (stock_account, ACCT_TYPE_STOCK);
+    xaccAccountSetCommodity (stock_account, stock_commodity);
+    gnc_account_append_child (broker_account, stock_account);
+    xaccAccountCommitEdit (broker_account);
+    xaccAccountCommitEdit (stock_account);
+
+    xaccAccountBeginEdit (cash_account);
+    xaccAccountSetName (cash_account, "Cash Account");
+    xaccAccountSetType (cash_account, ACCT_TYPE_BANK);
+    xaccAccountSetCommodity (cash_account, USD);
+    xaccAccountCommitEdit (cash_account);
+
+    xaccAccountBeginEdit (dividend_account);
+    xaccAccountSetName (dividend_account, "Dividend Account");
+    xaccAccountSetType (dividend_account, ACCT_TYPE_INCOME);
+    xaccAccountSetCommodity (dividend_account, USD);
+    xaccAccountCommitEdit (dividend_account);
+
+    xaccAccountBeginEdit (capgains_account);
+    xaccAccountSetName (capgains_account, "Capgains Account");
+    xaccAccountSetType (capgains_account, ACCT_TYPE_INCOME);
+    xaccAccountSetCommodity (capgains_account, USD);
+    xaccAccountCommitEdit (capgains_account);
+
+    xaccAccountBeginEdit (fees_account);
+    xaccAccountSetName (fees_account, "Fees Account");
+    xaccAccountSetType (fees_account, ACCT_TYPE_EXPENSE);
+    xaccAccountSetCommodity (fees_account, USD);
+    xaccAccountCommitEdit (fees_account);
+}
+
+void
+StockAssistantTest::instantiate_model(StockAssistantModel &model, const ASTTestCase &t)
+{
+    model.transaction_date = gnc_dmy2time64 (t.dd, t.mm, t.yy);
+    model.maybe_reset_txn_types ();
+
+    model.set_txn_type (t.type_idx);
+    model.transaction_description = t.desc;
+    model.stock_amount = gnc_numeric_create (t.stock_amt * 100, 100);
+    model.stock_value = gnc_numeric_create (t.stock_val, 100);
+    model.cash_value = gnc_numeric_create (t.cash_val, 100);
+    model.cash_account = cash_account;
+    model.fees_account = fees_account;
+    model.fees_capitalize = t.capitalize;
+    model.fees_value = gnc_numeric_create (t.fees_val, 100);
+    model.capgains_account = capgains_account;
+    model.capgains_value = gnc_numeric_create (t.capg_val, 100);
+    model.dividend_account = dividend_account;
+    model.dividend_value = gnc_numeric_create (t.divi_val, 100);
+}
+
+class StockAssistantTestParameterized :
+    public StockAssistantTest,
+    public ::testing::WithParamInterface<ASTTestCase>
+{
+protected:
+    StockAssistantModel model;
+    const ASTTestCase &t;
+
+    StockAssistantTestParameterized() :
+        model(stock_account), t{GetParam()}
     {
-        qof_init();
-        qof_book_register ();
-        gnc_pricedb_register();
-        gnc_commodity_table_register ();
-
-        stock_commodity = gnc_commodity_new (m_book.get(), "SPY", "", "SPY", "", 100);
-        USD = gnc_commodity_table_lookup (gnc_commodity_table_get_table (m_book.get()),
-                                          "CURRENCY", "USD");
-
-        xaccAccountBeginEdit (broker_account);
-        xaccAccountSetName (broker_account, "Broker Account");
-        xaccAccountSetType (broker_account, ACCT_TYPE_CASH);
-        xaccAccountSetCommodity (broker_account, USD);
-
-        xaccAccountBeginEdit (stock_account);
-        xaccAccountSetName (stock_account, "Stock Account");
-        xaccAccountSetType (stock_account, ACCT_TYPE_STOCK);
-        xaccAccountSetCommodity (stock_account, stock_commodity);
-        gnc_account_append_child (broker_account, stock_account);
-        xaccAccountCommitEdit (broker_account);
-        xaccAccountCommitEdit (stock_account);
-
-        xaccAccountBeginEdit (cash_account);
-        xaccAccountSetName (cash_account, "Cash Account");
-        xaccAccountSetType (cash_account, ACCT_TYPE_BANK);
-        xaccAccountSetCommodity (cash_account, USD);
-        xaccAccountCommitEdit (cash_account);
-
-        xaccAccountBeginEdit (dividend_account);
-        xaccAccountSetName (dividend_account, "Dividend Account");
-        xaccAccountSetType (dividend_account, ACCT_TYPE_INCOME);
-        xaccAccountSetCommodity (dividend_account, USD);
-        xaccAccountCommitEdit (dividend_account);
-
-        xaccAccountBeginEdit (capgains_account);
-        xaccAccountSetName (capgains_account, "Capgains Account");
-        xaccAccountSetType (capgains_account, ACCT_TYPE_INCOME);
-        xaccAccountSetCommodity (capgains_account, USD);
-        xaccAccountCommitEdit (capgains_account);
-
-        xaccAccountBeginEdit (fees_account);
-        xaccAccountSetName (fees_account, "Fees Account");
-        xaccAccountSetType (fees_account, ACCT_TYPE_EXPENSE);
-        xaccAccountSetCommodity (fees_account, USD);
-        xaccAccountCommitEdit (fees_account);
+        instantiate_model (model, t);
     }
 };
 
-static void test_failure_modes (Account *stock_account)
+TEST_F(StockAssistantTest, testFailureModes)
 {
     StockAssistantModel model (stock_account);
     model.transaction_date = gnc_dmy2time64 (1, 1, 2022);
@@ -203,29 +242,12 @@ static void dump_acct (Account *acct)
     }
 }
 
-TEST_P(Stock_AssistantTest, DoesStock_Assistant)
+TEST_F(StockAssistantTest, testAggregateResults)
 {
-    test_failure_modes (stock_account);
-    for (const auto& t : m_testCase.tests)
+    for (const auto& t : easyTestCases)
     {
         StockAssistantModel model (stock_account);
-        model.transaction_date = gnc_dmy2time64 (t.dd, t.mm, t.yy);
-        model.maybe_reset_txn_types ();
-
-        model.set_txn_type (t.type_idx);
-        model.transaction_description = t.desc;
-        model.stock_amount = gnc_numeric_create (t.stock_amt * 100, 100);
-        model.stock_value = gnc_numeric_create (t.stock_val, 100);
-        model.cash_value = gnc_numeric_create (t.cash_val, 100);
-        model.cash_account = cash_account;
-        model.fees_account = fees_account;
-        model.fees_capitalize = t.capitalize;
-        model.fees_value = gnc_numeric_create (t.fees_val, 100);
-        model.capgains_account = capgains_account;
-        model.capgains_value = gnc_numeric_create (t.capg_val, 100);
-        model.dividend_account = dividend_account;
-        model.dividend_value = gnc_numeric_create (t.divi_val, 100);
-
+        instantiate_model (model, t);
         auto [success_splits, summary, splitinfos] = model.generate_list_of_splits ();
         EXPECT_TRUE (success_splits) << t.dd << '/' << t.mm << '/' << t.yy << ": "
                                      << t.desc << '='
@@ -248,20 +270,3 @@ TEST_P(Stock_AssistantTest, DoesStock_Assistant)
     EXPECT_EQ (xaccAccountGetBalance (fees_account).num, 4478);
     EXPECT_EQ (xaccAccountGetBalance (cash_account).num, 1663049);
 }
-
-#ifndef INSTANTIATE_TEST_SUITE_P
-// Silence "no previous declaration for" (treated as error due to -Werror) when building with GoogleTest < 1.8.1
-static testing::internal::ParamGenerator<TestCase*>
-gtest_InstantiationStock_AssistantTestStock_AssistantTest_EvalGenerator_();
-static std::string gtest_InstantiationStock_AssistantTestStock_AssistantTest_EvalGenerateName_(const testing::TestParamInfo<TestCase*>&);
-
-INSTANTIATE_TEST_CASE_P(
-#else // INSTANTIATE_TEST_SUITE_P
-INSTANTIATE_TEST_SUITE_P(
-#endif // INSTANTIATE_TEST_SUITE_P
-    InstantiationStock_AssistantTest,
-    Stock_AssistantTest,
-    ::testing::Values(
-        &easyTestCase
-    )
-);

commit 74f707bfa73b4fd0cc72968605d43d6e97bb7361
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Jan 26 13:54:21 2023 -0800

    [asst-stock-trans] StockAssistantModel instance variable names
    
    Should start with m_. Also designating them with this-> isn't
    idiomatic or necessary, and long functions shouldn't be defined
    in the class/struct body.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index 84d739cd4c..ea78ef8e5e 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -402,50 +402,49 @@ then record the reverse split.")
 
 struct StockTransactionSplitInfo
 {
-    bool debit_side;
-    std::string account_str;
-    std::string memo_str;
-    std::string action_str;
-    std::string value_str;
-    std::string units_str;
-    bool units_in_red = false;
-    Account* account = nullptr;
-    gnc_numeric value_numeric = gnc_numeric_create (1, 0); // invalid gnc_numerics
-    gnc_numeric units_numeric = gnc_numeric_create (1, 0);
-
-    void create_split(Transaction *trans, AccountVec& account_commits)
-    {
-        g_return_if_fail (trans);
-        if (!this->account ||
-            gnc_numeric_check (this->value_numeric) ||
-            gnc_numeric_check (this->units_numeric))
-            return;
-        auto split = xaccMallocSplit (qof_instance_get_book (trans));
-        xaccSplitSetParent (split, trans);
-        xaccAccountBeginEdit (this->account);
-        account_commits.emplace_back (this->account);
-        xaccSplitSetAccount (split, this->account);
-        xaccSplitSetMemo (split, this->memo_str.c_str());
-        xaccSplitSetValue (split, this->value_numeric);
-        xaccSplitSetAmount (split, this->units_numeric);
-        DEBUG ("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
-               this->action_str.c_str(), this->account_str.c_str(),
-               gnc_num_dbg_to_string (this->value_numeric),
-               gnc_num_dbg_to_string (this->units_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", this->action_str.c_str()));
-    }
+    bool m_debit_side;
+    std::string m_account_str;
+    std::string m_memo_str;
+    std::string m_action_str;
+    std::string m_value_str;
+    std::string m_units_str;
+    bool m_units_in_red = false;
+    Account* m_account = nullptr;
+    gnc_numeric m_value_numeric = gnc_numeric_create (1, 0); // invalid gnc_numerics
+    gnc_numeric m_units_numeric = gnc_numeric_create (1, 0);
+
     StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo constructor\n"); };
     StockTransactionSplitInfo (Account *acct, gnc_numeric val)
-        : account_str (xaccAccountGetName (acct))
-        , account (acct)
-        , value_numeric (val)
-    { DEBUG ("StockTransactionSplitInfo constructor\n"); };
-    ~StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo destructor\n"); };
+        : m_account_str{xaccAccountGetName (acct)} , m_account{acct} , m_value_numeric{val}
+    { DEBUG ("StockTransactionSplitInfo constructor\n"); }
+    ~StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo destructor\n"); }
+    void create_split(Transaction *trans, AccountVec &account_commits);
 };
 
+void
+StockTransactionSplitInfo::create_split(Transaction *trans, AccountVec &account_commits) {
+  g_return_if_fail(trans);
+  if (!m_account || gnc_numeric_check(m_value_numeric) ||
+      gnc_numeric_check(m_units_numeric))
+    return;
+  auto split = xaccMallocSplit(qof_instance_get_book(trans));
+  xaccSplitSetParent(split, trans);
+  xaccAccountBeginEdit(m_account);
+  account_commits.emplace_back(m_account);
+  xaccSplitSetAccount(split, m_account);
+  xaccSplitSetMemo(split, m_memo_str.c_str());
+  xaccSplitSetValue(split, m_value_numeric);
+  xaccSplitSetAmount(split, m_units_numeric);
+  DEBUG("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
+        m_action_str.c_str(), m_account_str.c_str(),
+        gnc_num_dbg_to_string(m_value_numeric),
+        gnc_num_dbg_to_string(m_units_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",
+                                  m_action_str.c_str()));
+}
 
 static StockTransactionSplitInfo
 check_page (gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
@@ -466,20 +465,20 @@ check_page (gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
 
     DEBUG ("page=%s, amount=%s", page, gnc_num_dbg_to_string (amount));
     if (memo)
-        line.memo_str = memo;
-    line.debit_side = (splitfield & FieldMask::ENABLED_DEBIT);
+        line.m_memo_str = memo;
+    line.m_debit_side = (splitfield & FieldMask::ENABLED_DEBIT);
     if (page)
-        line.action_str = page;
+        line.m_action_str = page;
 
     if (gnc_numeric_check (amount))
     {
         if (splitfield & FieldMask::ALLOW_ZERO)
             // line.value_numeric contains an invalid gnc_numeric
-            line.value_str = "";
+            line.m_value_str = "";
         else
         {
             add_error (N_("Amount for %s is missing."), page);
-            line.value_str = _(missing_str);
+            line.m_value_str = _(missing_str);
         }
     }
     else
@@ -494,29 +493,29 @@ check_page (gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
         if (gnc_numeric_negative_p (amount))
         {
             amount = gnc_numeric_neg (amount);
-            line.debit_side = !line.debit_side;
+            line.m_debit_side = !line.m_debit_side;
         }
-        if (line.debit_side)
+        if (line.m_debit_side)
             debit = gnc_numeric_add_fixed (debit, amount);
         else
             credit = gnc_numeric_add_fixed (credit, amount);
-        line.units_numeric = line.debit_side ? amount : gnc_numeric_neg (amount);
-        line.value_numeric = line.debit_side ? amount : gnc_numeric_neg (amount);
-        line.value_str = xaccPrintAmount (amount, curr_pinfo);
+        line.m_units_numeric = line.m_debit_side ? amount : gnc_numeric_neg (amount);
+        line.m_value_numeric = line.m_debit_side ? amount : gnc_numeric_neg (amount);
+        line.m_value_str = xaccPrintAmount (amount, curr_pinfo);
     }
 
     if (acct)
     {
-        line.account = acct;
-        line.account_str = xaccAccountGetName (acct);
+        line.m_account = acct;
+        line.m_account_str = xaccAccountGetName (acct);
     }
     else if ((splitfield & FieldMask::ALLOW_ZERO) &&
              (gnc_numeric_check (amount) || gnc_numeric_zero_p (amount)))
-        line.account_str = "";
+        line.m_account_str = "";
     else
     {
         add_error (N_("Account for %s is missing."), page);
-        line.account_str = _(missing_str);
+        line.m_account_str = _(missing_str);
     }
     return line;
 }
@@ -712,11 +711,11 @@ to ensure proper recording."), new_date_str, last_split_date_str);
 
 
         if (!this->stock_amount_enabled)
-            line.units_numeric = gnc_numeric_zero();
+            line.m_units_numeric = gnc_numeric_zero();
         else if (gnc_numeric_check (this->stock_amount))
         {
-            line.units_str = _("(missing)");
-            line.units_numeric = gnc_numeric_zero();
+            line.m_units_str = _("(missing)");
+            line.m_units_numeric = gnc_numeric_zero();
             add_error_str (N_("Amount for stock units is missing"));
         }
         else if (this->input_new_balance)
@@ -727,9 +726,9 @@ to ensure proper recording."), new_date_str, last_split_date_str);
             auto ratio = gnc_numeric_div (stock_amount, this->balance_at_date,
                                           GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
             stock_amount = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
-            line.units_numeric = stock_amount;
-            line.units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
-            line.units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
+            line.m_units_numeric = stock_amount;
+            line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+            line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
             if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
                 add_error_str (N_("Invalid stock new balance."));
             else if (gnc_numeric_negative_p (delta) && !credit_side)
@@ -744,9 +743,9 @@ to ensure proper recording."), new_date_str, last_split_date_str);
                 add_error_str (N_("Stock amount must be positive."));
             if (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
                 stock_amount = gnc_numeric_neg (stock_amount);
-            line.units_numeric = stock_amount;
-            line.units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
-            line.units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
+            line.m_units_numeric = stock_amount;
+            line.m_units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+            line.m_units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
             auto new_bal = gnc_numeric_add_fixed (this->balance_at_date, stock_amount);
             if (gnc_numeric_positive_p (this->balance_at_date) &&
                 gnc_numeric_negative_p (new_bal))
@@ -790,7 +789,7 @@ to ensure proper recording."), new_date_str, last_split_date_str);
                                NC_ ("Stock Assistant: Page name", "fees"),
                                this->curr_pinfo);
             if (this->fees_capitalize)
-                line.units_numeric = gnc_numeric_zero();
+                line.m_units_numeric = gnc_numeric_zero();
             this->list_of_splits.push_back (std::move (line));
         }
 
@@ -817,7 +816,7 @@ to ensure proper recording."), new_date_str, last_split_date_str);
                                    this->acct, this->capgains_memo, this->capgains_value,
                                    NC_ ("Stock Assistant: Page name", "capital gains"),
                                    this->curr_pinfo);
-                line.units_numeric = gnc_numeric_zero();
+                line.m_units_numeric = gnc_numeric_zero();
                 this->list_of_splits.push_back (std::move (line));
             }
 
@@ -1053,16 +1052,16 @@ struct StockAssistantView
         for (const auto& line : list_of_splits)
         {
             GtkTreeIter iter;
-            auto tooltip = g_markup_escape_text (line.memo_str.c_str(), -1);
+            auto tooltip = g_markup_escape_text (line.m_memo_str.c_str(), -1);
             gtk_list_store_append (list, &iter);
             gtk_list_store_set (list, &iter,
-                                SPLIT_COL_ACCOUNT, line.account_str.c_str(),
-                                SPLIT_COL_MEMO, line.memo_str.c_str(),
+                                SPLIT_COL_ACCOUNT, line.m_account_str.c_str(),
+                                SPLIT_COL_MEMO, line.m_memo_str.c_str(),
                                 SPLIT_COL_TOOLTIP, tooltip,
-                                SPLIT_COL_DEBIT, line.debit_side ? line.value_str.c_str() : nullptr,
-                                SPLIT_COL_CREDIT, line.debit_side ? nullptr : line.value_str.c_str(),
-                                SPLIT_COL_UNITS, line.units_str.c_str(),
-                                SPLIT_COL_UNITS_COLOR, line.units_in_red ? "red" : nullptr,
+                                SPLIT_COL_DEBIT, line.m_debit_side ? line.m_value_str.c_str() : nullptr,
+                                SPLIT_COL_CREDIT, line.m_debit_side ? nullptr : line.m_value_str.c_str(),
+                                SPLIT_COL_UNITS, line.m_units_str.c_str(),
+                                SPLIT_COL_UNITS_COLOR, line.m_units_in_red ? "red" : nullptr,
                                 -1);
             g_free (tooltip);
         }

commit 6d36d1d7eea8aad3182084448b00e8357e5f3aed
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Jan 26 13:27:43 2023 -0800

    [asst-stock-trans] Rename primary callbacks to clarify their purpose.

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index ec3e479b3f..84d739cd4c 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -48,10 +48,12 @@ static QofLogModule log_module = GNC_MOD_ASSISTANT;
 
 extern "C"
 {
-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);
+// These functions are the GtkAssistant primary button callbacks. They're
+// connected to their signals in assistant-stock-transaction.glade.
+void stock_assistant_prepare_cb (GtkAssistant  *assistant, GtkWidget *page,
+                                 gpointer user_data);
+void stock_assistant_finish_cb  (GtkAssistant *assistant, gpointer user_data);
+void stock_assistant_cancel_cb  (GtkAssistant *gtkassistant, gpointer user_data);
 }
 
 enum class FieldMask : unsigned;
@@ -1337,7 +1339,7 @@ controller_capitalize_fees (GtkWidget *widget, StockAssistantController* info)
 }
 
 void
-stock_assistant_prepare (GtkAssistant  *assistant, GtkWidget *page,
+stock_assistant_prepare_cb (GtkAssistant  *assistant, GtkWidget *page,
                          gpointer user_data)
 {
     auto info = static_cast<StockAssistantController*>(user_data);
@@ -1434,7 +1436,7 @@ forward_page_func (gint current_page, StockAssistantController* info)
 }
 
 void
-stock_assistant_finish (GtkAssistant *assistant, gpointer user_data)
+stock_assistant_finish_cb (GtkAssistant *assistant, gpointer user_data)
 {
     auto info = static_cast<StockAssistantController*>(user_data);
     g_return_if_fail (info->model->txn_type);
@@ -1447,7 +1449,7 @@ stock_assistant_finish (GtkAssistant *assistant, gpointer user_data)
 
 
 void
-stock_assistant_cancel (GtkAssistant *assistant, gpointer user_data)
+stock_assistant_cancel_cb (GtkAssistant *assistant, gpointer user_data)
 {
     auto info = static_cast<StockAssistantController*>(user_data);
     gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info);
diff --git a/gnucash/gtkbuilder/assistant-stock-transaction.glade b/gnucash/gtkbuilder/assistant-stock-transaction.glade
index 08370ccf4f..013a3f8c3e 100644
--- a/gnucash/gtkbuilder/assistant-stock-transaction.glade
+++ b/gnucash/gtkbuilder/assistant-stock-transaction.glade
@@ -9,9 +9,9 @@
     <property name="title" translatable="yes">Stock Transaction Assistant</property>
     <property name="window-position">center</property>
     <property name="default-width">500</property>
-    <signal name="cancel" handler="stock_assistant_cancel" swapped="no"/>
-    <signal name="close" handler="stock_assistant_finish" swapped="no"/>
-    <signal name="prepare" handler="stock_assistant_prepare" swapped="no"/>
+    <signal name="cancel" handler="stock_assistant_cancel_cb" swapped="no"/>
+    <signal name="close" handler="stock_assistant_finish_cb" swapped="no"/>
+    <signal name="prepare" handler="stock_assistant_prepare_cb" swapped="no"/>
     <child>
       <object class="GtkLabel" id="intro_page_label">
         <property name="visible">True</property>

commit 0d9dd3b789ac6a8bd9222d07715455760c39dd15
Author: Christopher Lam <christopher.lck at gmail.com>
Date:   Tue Jan 17 22:30:34 2023 +0800

    [assistant-stock-transaction] support additional txn_types

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index d6c9613eed..ec3e479b3f 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -99,6 +99,7 @@ enum class FieldMask : unsigned
     ALLOW_NEGATIVE = 8,
     INPUT_NEW_BALANCE = 16,     // stock_amt only: instead of amount, get new balance
     CAPITALIZE_DEFAULT = 32,    // fees only: capitalize by default into stock acct
+    CAPGAINS_IN_STOCK = 64,     // capg only: add a balancing split in stock acct
 };
 
 FieldMask operator |(FieldMask lhs, FieldMask rhs)
@@ -182,7 +183,7 @@ static const TxnTypeVec long_types
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
-        FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE, // capgains_amt
+        FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE | FieldMask::CAPGAINS_IN_STOCK, // capgains_amt
         // Translators: this is a stock transaction describing new
         // sale of stock, and recording capital gain/loss
         N_("Sell"),
@@ -213,6 +214,18 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Return of capital"),
         N_("Company returns capital, reducing the cost basis without affecting # units.")
     },
+    {
+        FieldMask::DISABLED,               // stock_amt
+        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::DISABLED,               // cash_amt
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
+        FieldMask::ENABLED_DEBIT,          // dividend_amt
+        FieldMask::DISABLED,               // capg_amt
+        // Translators: this is a stock transaction describing return
+        // of capital, reclassifying a dividend into return of capital
+        N_("Return of capital (reclassification)"),
+        N_("Company returns capital, reducing the cost basis without affecting # units. A distribution previously recorded as a dividend is reclassified to return of capital, often due to end-of-year tax information.")
+    },
     {
         FieldMask::DISABLED,               // stock_amt
         FieldMask::ENABLED_DEBIT,          // stock_val
@@ -221,10 +234,22 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         FieldMask::ENABLED_CREDIT,         // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing a
-        // notional distribution
-        N_("Notional distribution"),
+        // notional distribution recorded as dividend
+        N_("Notional distribution (dividend)"),
         N_("Company issues a notional distribution, which is recorded as dividend income and increases the cost basis without affecting # units.")
     },
+    {
+        FieldMask::DISABLED,               // stock_amt
+        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::DISABLED,               // cash_amt
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
+        FieldMask::DISABLED,               // dividend_amt
+        FieldMask::ENABLED_CREDIT,         // capg_amt
+        // Translators: this is a stock transaction describing a
+        // notional distribution recorded as capital gain
+        N_("Notional distribution (capital gain)"),
+        N_("Company issues a notional distribution, which is recorded as capital gain and increases the cost basis without affecting # units.")
+    },
     {
         FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
         FieldMask::DISABLED,               // stock_val
@@ -274,7 +299,7 @@ static const TxnTypeVec short_types
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
-        FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE,          // capg_amt
+        FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE | FieldMask::CAPGAINS_IN_STOCK,          // capg_amt
         // Translators: this is a stock transaction describing cover
         // buying stock, and recording capital gain/loss
         N_("Buy to cover short"),
@@ -304,6 +329,19 @@ static const TxnTypeVec short_types
         N_("Compensatory return of capital"),
         N_("Company returns capital, and the short stock holder must make a compensatory payment for the returned capital. This reduces the cost basis (less negative, towards 0.00 value) without affecting # units.")
     },
+    {
+        FieldMask::DISABLED,               // stock_amt
+        FieldMask::ENABLED_DEBIT,          // stock_val
+        FieldMask::DISABLED,               // cash_amt
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
+        FieldMask::ENABLED_CREDIT,         // dividend_amt
+        FieldMask::DISABLED,               // capg_amt
+        // Translators: this is a stock transaction describing
+        // reclassifying a compensatory dividend into compensatory
+        // return of capital when shorting stock
+        N_("Compensatory return of capital (reclassification)"),
+        N_("Company returns capital, and the short stock holder must make a compensatory payment for the returned capital. This reduces the cost basis (less negative, towards 0.00 value) without affecting # units. A distribution previously recorded as a compensatory dividend is reclassified to compensatory return of capital, often due to end-of-year tax information.")
+    },
     {
         FieldMask::DISABLED,               // stock_amt
         FieldMask::ENABLED_CREDIT,         // stock_val
@@ -312,10 +350,24 @@ static const TxnTypeVec short_types
         FieldMask::ENABLED_DEBIT,          // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing a
-        // notional distribution when shorting stock
-        N_("Compensatory notional distribution"),
+        // notional distribution recorded as dividend when shorting
+        // stock
+        N_("Compensatory notional distribution (dividend)"),
         N_("Company issues a notional distribution, and the short stock holder must make a compensatory payment for the notional distribution. This is recorded as a loss/negative dividend income amount, and increases the cost basis (more negative, away from 0.00 value) without affecting # units.")
     },
+    {
+        FieldMask::DISABLED,               // stock_amt
+        FieldMask::ENABLED_CREDIT,         // stock_val
+        FieldMask::DISABLED,               // cash_amt
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
+        FieldMask::DISABLED,               // dividend_amt
+        FieldMask::ENABLED_DEBIT,          // capg_amt
+        // Translators: this is a stock transaction describing a
+        // notional distribution recorded as capital gain when
+        // shorting stock
+        N_("Compensatory notional distribution (capital gain)"),
+        N_("Company issues a notional distribution, and the short stock holder must make a compensatory payment for the notional distribution. This is recorded as a capital loss amount, and increases the cost basis (more negative, away from 0.00 value) without affecting # units.")
+    },
     {
         FieldMask::ENABLED_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
         FieldMask::DISABLED,               // stock_val
@@ -756,20 +808,23 @@ to ensure proper recording."), new_date_str, last_split_date_str);
         // swap the debit/credit flags.
         if (this->capgains_enabled)
         {
+            if (this->txn_type->capgains_value & FieldMask::CAPGAINS_IN_STOCK)
+            {
+                line = check_page (debit, credit, errors, this->txn_type->capgains_value ^
+                                   (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
+                                   this->acct, this->capgains_memo, this->capgains_value,
+                                   NC_ ("Stock Assistant: Page name", "capital gains"),
+                                   this->curr_pinfo);
+                line.units_numeric = gnc_numeric_zero();
+                this->list_of_splits.push_back (std::move (line));
+            }
+
             line = check_page (debit, credit, errors, this->txn_type->capgains_value,
                                this->capgains_account, this->capgains_memo,
                                this->capgains_value,
                                NC_ ("Stock Assistant: Page name", "capital gains"),
                                this->curr_pinfo);
             this->list_of_splits.push_back (std::move (line));
-
-            line = check_page (debit, credit, errors, this->txn_type->capgains_value ^
-                               (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
-                               this->acct, this->capgains_memo, this->capgains_value,
-                               NC_ ("Stock Assistant: Page name", "capital gains"),
-                               this->curr_pinfo);
-            line.units_numeric = gnc_numeric_zero();
-            this->list_of_splits.push_back (std::move (line));
         }
 
         if (!gnc_numeric_equal (debit, credit))
diff --git a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
index a247d9928f..089b4cc307 100644
--- a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
+++ b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
@@ -62,7 +62,7 @@ TestCase easyTestCase = {
         { 0, 1 , 4 , 2020, "Buy"              , 250    , 4200000 , 4200995 , true      , 995 , 0    , 0     , 325 },
         { 3, 16, 4 , 2020, "ROC"              , 0      , 250000  , 250000  , true      , 0   , 0    , 0     , 325 },
         { 0, 2 , 5 , 2020, "Buy"              , 125    , 4750000 , 4750000 , true      , 0   , 0    , 0     , 450 },
-        { 5, 11, 5 , 2020, "Split 2:1"        , 900    , 0       , 0       , true      , 0   , 0    , 0     , 900 },
+        { 7, 11, 5 , 2020, "Split 2:1"        , 900    , 0       , 0       , true      , 0   , 0    , 0     , 900 },
         { 1, 21, 5 , 2020, "Sell"             , 135    , 2150000 , 2149005 , false     , 995 , 0    , 574702, 765 },
         { 0, 3 , 6 , 2020, "Buy"              , 150    , 2100000 , 2100000 , true      , 0   , 0    , 0     , 915 },
         { 1, 10, 6 , 2020, "Sell"             , 915    , 12810000, 12809005, false     , 995 , 0    , 1783309, 0 },
@@ -73,11 +73,11 @@ TestCase easyTestCase = {
         // bal<0. next lines are short_types
         { 0, 15, 6 , 2020, "Short Sell"       , 65     , 1105000 , 1104005 , true      , 995 , 0    , 0     , -150 },
         { 1, 16, 6 , 2020, "Cover Buy"        , 50     , 500000  , 500995  , false     , 995 , 0    ,-264337, -100 },
-        { 5, 17, 6 , 2020, "Split 2:1"        , -200   , 0       , 0       , false     , 0   , 0    , 0     , -200 },
-        { 6, 18, 6 , 2020, "Reverse Split"    , -100   , 0       , 0       , false     , 0   , 0    , 0     , -100 },
+        { 7, 17, 6 , 2020, "Split 2:1"        , -200   , 0       , 0       , false     , 0   , 0    , 0     , -200 },
+        { 8, 18, 6 , 2020, "Reverse Split"    , -100   , 0       , 0       , false     , 0   , 0    , 0     , -100 },
         { 2, 19, 6 , 2020, "Comp Dividend"    , 0      , 0       , 50000   , false     , 0   , 50000, 0     , -100 },
         { 3, 19, 6 , 2020, "Comp ROC"         , 0      , 250000  , 250000  , false     , 0   , 0    , 0     , -100 },
-        { 4, 19, 6 , 2020, "Comp ND"          , 0      , 20000   , 0       , false     , 0   , 20000, 0     , -100 },
+        { 5, 19, 6 , 2020, "Comp ND"          , 0      , 20000   , 0       , false     , 0   , 20000, 0     , -100 },
         { 1, 20, 6 , 2020, "Cover Buy"        , 100    , 800000  , 800498  , false     , 498 , 0    ,-498673, 0 },
 
         // bal=0. next line is "open long".
@@ -88,8 +88,8 @@ TestCase easyTestCase = {
         { 2, 25, 6 , 2020, "Dividend"         , 0      , 0       , 11000   , false     , 0   , 11000, 0     , 100 },
         { 0, 25, 6 , 2020, "+ Reinv"          , 1      , 10000   , 10000   , false     , 0   , 0    , 0     , 101 },
         { 1, 26, 6 , 2020, "Sell remainder"   , 1      , 10000   , 10000   , false     , 0   , 0    , 1975  , 100 },
-        { 6, 26, 6 , 2020, "Reverse Split 1:2", 50     , 0       , 0       , false     , 0   , 0    , 0     , 50 },
-        { 4, 27, 6 , 2020, "ND"               , 0      , 10000   , 0       , false     , 0   , 10000, 0     , 50 }
+        { 8, 26, 6 , 2020, "Reverse Split 1:2", 50     , 0       , 0       , false     , 0   , 0    , 0     , 50 },
+        { 5, 27, 6 , 2020, "ND"               , 0      , 10000   , 0       , false     , 0   , 10000, 0     , 50 }
     }
 };
 

commit 55046f22d26c71dd813d71b2da7fa62e2d2b2b82
Author: Christopher Lam <christopher.lck at gmail.com>
Date:   Tue Jan 17 21:58:25 2023 +0800

    reduce number of bools in txn_type info: fees_capitalize

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index e6304b10bf..d6c9613eed 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -98,6 +98,7 @@ enum class FieldMask : unsigned
     ALLOW_ZERO = 4,
     ALLOW_NEGATIVE = 8,
     INPUT_NEW_BALANCE = 16,     // stock_amt only: instead of amount, get new balance
+    CAPITALIZE_DEFAULT = 32,    // fees only: capitalize by default into stock acct
 };
 
 FieldMask operator |(FieldMask lhs, FieldMask rhs)
@@ -123,7 +124,6 @@ struct TxnTypeInfo
     FieldMask stock_value;
     FieldMask cash_value;
     FieldMask fees_value;
-    bool fees_capitalize;
     FieldMask dividend_value;
     FieldMask capgains_value;
     const char* friendly_name;
@@ -140,8 +140,7 @@ 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::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing an
@@ -153,8 +152,7 @@ static const TxnTypeVec starting_types
         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 | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing an
@@ -170,8 +168,7 @@ 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::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing
@@ -184,7 +181,6 @@ static const TxnTypeVec long_types
         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 new
@@ -197,7 +193,6 @@ static const TxnTypeVec long_types
         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
@@ -210,8 +205,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         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::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing return
@@ -224,7 +218,6 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         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
@@ -236,8 +229,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
-        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
-        true,                   // fees_capitalize
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing a stock
@@ -249,8 +241,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         FieldMask::ENABLED_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
-        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
-        true,                   // fees_capitalize
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing a reverse split
@@ -269,8 +260,7 @@ static const TxnTypeVec short_types
         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 | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing
@@ -283,7 +273,6 @@ static const TxnTypeVec short_types
         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_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE,          // capg_amt
         // Translators: this is a stock transaction describing cover
@@ -296,7 +285,6 @@ static const TxnTypeVec short_types
         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
@@ -308,8 +296,7 @@ static const TxnTypeVec short_types
         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::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing return
@@ -322,7 +309,6 @@ static const TxnTypeVec short_types
         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
@@ -334,8 +320,7 @@ static const TxnTypeVec short_types
         FieldMask::ENABLED_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
-        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
-        true,                   // fees_capitalize
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing a stock
@@ -347,8 +332,7 @@ static const TxnTypeVec short_types
         FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
-        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
-        true,                   // fees_capitalize
+        FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT,          // fees_amt
         FieldMask::DISABLED,               // dividend_amt
         FieldMask::DISABLED,               // capg_amt
         // Translators: this is a stock transaction describing a
@@ -567,7 +551,7 @@ struct StockAssistantModel
         this->input_new_balance = this->txn_type->stock_amount & FieldMask::INPUT_NEW_BALANCE;
         this->stock_amount_enabled = this->txn_type->stock_amount != FieldMask::DISABLED;
         this->stock_value_enabled = this->txn_type->stock_value != FieldMask::DISABLED;
-        this->fees_capitalize = this->txn_type->fees_capitalize;
+        this->fees_capitalize = this->txn_type->fees_value & FieldMask::CAPITALIZE_DEFAULT;
         this->fees_enabled = this->txn_type->fees_value != FieldMask::DISABLED;
         this->capgains_enabled = this->txn_type->capgains_value != FieldMask::DISABLED;
         this->dividend_enabled = this->txn_type->dividend_value != FieldMask::DISABLED;

commit d67f0b3c999aa5c2120aed6f2e1b94ed6c4bc4cc
Author: Christopher Lam <christopher.lck at gmail.com>
Date:   Tue Jan 17 21:51:01 2023 +0800

    reduce number of bools in txn_type info: input_new_balance
    
    input_new_balance merged into stock_amount FieldMask

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index abde72b1a7..e6304b10bf 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -96,7 +96,8 @@ enum class FieldMask : unsigned
     ENABLED_DEBIT,
     ENABLED_CREDIT,
     ALLOW_ZERO = 4,
-    ALLOW_NEGATIVE = 8
+    ALLOW_NEGATIVE = 8,
+    INPUT_NEW_BALANCE = 16,     // stock_amt only: instead of amount, get new balance
 };
 
 FieldMask operator |(FieldMask lhs, FieldMask rhs)
@@ -119,7 +120,6 @@ FieldMask operator ^(FieldMask lhs, FieldMask rhs)
 struct TxnTypeInfo
 {
     FieldMask stock_amount;
-    bool input_new_balance;
     FieldMask stock_value;
     FieldMask cash_value;
     FieldMask fees_value;
@@ -138,7 +138,6 @@ static const TxnTypeVec starting_types
 {
     {
         FieldMask::ENABLED_DEBIT,          // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_DEBIT,          // stock_val
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -152,7 +151,6 @@ static const TxnTypeVec starting_types
     },
     {
         FieldMask::ENABLED_CREDIT,         // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_CREDIT,         // stock_val
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -170,7 +168,6 @@ static const TxnTypeVec long_types
 {
     {
         FieldMask::ENABLED_DEBIT,          // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_DEBIT,          // stock_val
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -184,7 +181,6 @@ static const TxnTypeVec long_types
     },
     {
         FieldMask::ENABLED_CREDIT,         // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_CREDIT,         // stock_val
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -198,7 +194,6 @@ static const TxnTypeVec long_types
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        false,                             // input_new_balance
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -213,7 +208,6 @@ reinvested must be subsequently recorded as a regular stock purchase.")
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_CREDIT,         // stock_val
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -227,7 +221,6 @@ reinvested must be subsequently recorded as a regular stock purchase.")
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_DEBIT,          // stock_val
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -240,8 +233,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Company issues a notional distribution, which is recorded as dividend income and increases the cost basis without affecting # units.")
     },
     {
-        FieldMask::ENABLED_DEBIT,          // stock_amt
-        true,                              // input_new_balance
+        FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -254,8 +246,7 @@ reinvested must be subsequently recorded as a regular stock purchase.")
         N_("Company issues additional units, thereby reducing the stock price by a divisor, while keeping the total monetary value of the overall investment constant.")
     },
     {
-        FieldMask::ENABLED_CREDIT,         // stock_amt
-        true,                              // input_new_balance
+        FieldMask::ENABLED_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -276,7 +267,6 @@ static const TxnTypeVec short_types
 {
     {
         FieldMask::ENABLED_CREDIT,         // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_CREDIT,         // stock_val
         FieldMask::ENABLED_DEBIT,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -290,7 +280,6 @@ static const TxnTypeVec short_types
     },
     {
         FieldMask::ENABLED_DEBIT,          // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_DEBIT,          // stock_val
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -304,7 +293,6 @@ static const TxnTypeVec short_types
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        false,                             // input_new_balance
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -318,7 +306,6 @@ static const TxnTypeVec short_types
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_DEBIT,          // stock_val
         FieldMask::ENABLED_CREDIT,         // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -332,7 +319,6 @@ static const TxnTypeVec short_types
     },
     {
         FieldMask::DISABLED,               // stock_amt
-        false,                             // input_new_balance
         FieldMask::ENABLED_CREDIT,         // stock_val
         FieldMask::DISABLED,               // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -345,8 +331,7 @@ static const TxnTypeVec short_types
         N_("Company issues a notional distribution, and the short stock holder must make a compensatory payment for the notional distribution. This is recorded as a loss/negative dividend income amount, and increases the cost basis (more negative, away from 0.00 value) without affecting # units.")
     },
     {
-        FieldMask::ENABLED_CREDIT,         // stock_amt
-        true,                              // input_new_balance
+        FieldMask::ENABLED_CREDIT | FieldMask::INPUT_NEW_BALANCE,         // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -359,8 +344,7 @@ static const TxnTypeVec short_types
         N_("Company issues additional units, thereby reducing the stock price by a divisor, while keeping the total monetary value of the overall investment constant.")
     },
     {
-        FieldMask::ENABLED_DEBIT,          // stock_amt
-        true,                              // input_new_balance
+        FieldMask::ENABLED_DEBIT | FieldMask::INPUT_NEW_BALANCE,          // stock_amt
         FieldMask::DISABLED,               // stock_val
         FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO,          // cash_amt
         FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO,          // fees_amt
@@ -580,7 +564,7 @@ struct StockAssistantModel
             PERR ("out of range type_idx=%d", type_idx);
             return false;
         }
-        this->input_new_balance = this->txn_type->input_new_balance;
+        this->input_new_balance = this->txn_type->stock_amount & FieldMask::INPUT_NEW_BALANCE;
         this->stock_amount_enabled = this->txn_type->stock_amount != FieldMask::DISABLED;
         this->stock_value_enabled = this->txn_type->stock_value != FieldMask::DISABLED;
         this->fees_capitalize = this->txn_type->fees_capitalize;

commit d92d5b4bd213a0dc439418149696d1cefd2241ab
Author: Christopher Lam <christopher.lck at gmail.com>
Date:   Sun Oct 23 20:21:37 2022 +0800

    test suite for assistant-stock-transaction.cpp

diff --git a/gnucash/gnome/test/CMakeLists.txt b/gnucash/gnome/test/CMakeLists.txt
index 538ba5c2ed..81b77a8c16 100644
--- a/gnucash/gnome/test/CMakeLists.txt
+++ b/gnucash/gnome/test/CMakeLists.txt
@@ -5,6 +5,7 @@ set(GNOME_TEST_INCLUDE_DIRS
   ${CMAKE_SOURCE_DIR}/common/test-core
   ${CMAKE_SOURCE_DIR}/gnucash/gnome/
   ${CMAKE_SOURCE_DIR}/gnucash/gnome-utils/
+  ${CMAKE_SOURCE_DIR}/libgnucash/engine
   ${GUILE_INCLUDE_DIRS}
 )
 
@@ -24,9 +25,45 @@ gnc_add_test_with_guile(test-invoice-report-builtin-default test-invoice-report-
 
 set(GUILE_DEPENDS
   scm-core-utils
+  test-core
+  gnc-test-engine
+  gnc-app-utils
+  gnc-core-utils
+  gnc-engine
+)
+
+set (test_assistant-stock-transaction_SOURCES
+  gtest-assistant-stock-transaction.cpp
+)
+
+set (test_assistant-stock-transaction_INCLUDE_DIRS
+  ${CMAKE_BINARY_DIR}/common
+  ${CMAKE_SOURCE_DIR}/libgnucash/engine
+  ${GNOME_UTILS_GUI_TEST_INCLUDE_DIRS}
+  ${GNOME_UTILS_GUI_TEST_LIBS}
+  ${GLIB2_INCLUDE_DIRS}
+)
+
+set (test_assistant-stock-transaction_LIBS
+  gnc-engine
+  gnc-gnome-utils
+  gtest
+)
+
+gnc_add_test (test-assistant-stock-transaction
+  "${test_assistant-stock-transaction_SOURCES}"
+  test_assistant-stock-transaction_INCLUDE_DIRS
+  test_assistant-stock-transaction_LIBS
 )
 
 set_dist_list(test_gnome_DIST
   CMakeLists.txt
+<<<<<<< HEAD
   test-invoice-report-builtin-default.cpp
+=======
+  test-invoice-buitin-default.cpp
+  ${test_gnome_scheme_SOURCES}
+  ${test_gnome_SOURCES}
+  ${test_assistant-stock-transaction_SOURCES}
+>>>>>>> 711fe3671e (test suite for assistant-stock-transaction.cpp)
 )
diff --git a/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
new file mode 100644
index 0000000000..a247d9928f
--- /dev/null
+++ b/gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
@@ -0,0 +1,267 @@
+/********************************************************************
+ * test-assistant-stock-transaction.cpp:                            *
+ * Copyright 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, you can retrieve it from        *
+ * https://www.gnu.org/licenses/old-licenses/gpl-2.0.html            *
+ * or 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 at gnu.org                   *
+ ********************************************************************/
+#include "config.h"
+#include <glib.h>
+#include "../assistant-stock-transaction.cpp"
+#include <memory>
+#include <Account.h>
+#include <Split.h>
+#include <gnc-numeric.hpp>
+#include <gnc-datetime.hpp>
+#include <gtest/gtest.h>
+extern "C"
+{
+#include <gnc-pricedb-p.h>
+}
+
+struct Tests
+{
+    unsigned int type_idx, dd, mm, yy;
+    const char *desc;
+    int stock_amt, stock_val, cash_val;
+    bool capitalize;
+    int fees_val, divi_val, capg_val, new_bal;
+};
+
+struct TestCase {
+    std::vector<Tests> tests;
+};
+
+TestCase easyTestCase = {
+    .tests =
+    {
+        //t, dd, mm, yyyy, desc               , stk.amt, stk.val , cash    , capitalize, fees, divi , capg  , new_bal
+
+        // bal=0. next line is "open long".
+        { 0, 1 , 7 , 2019, "Buy"              , 100    , 2000000 , 2000995 , true      , 995 , 0    , 0     , 100 },
+
+        // bal>0. next lines are long_types
+        { 0, 11, 12, 2019, "Buy"              , 50     , 1600000 , 1600995 , true      , 995 , 0    , 0     , 150 },
+        { 1, 18, 3 , 2020, "Sell"             , 75     , 1200000 , 1199005 , false     , 995 , 0    ,-600995, 75 },
+        { 0, 1 , 4 , 2020, "Buy"              , 250    , 4200000 , 4200995 , true      , 995 , 0    , 0     , 325 },
+        { 3, 16, 4 , 2020, "ROC"              , 0      , 250000  , 250000  , true      , 0   , 0    , 0     , 325 },
+        { 0, 2 , 5 , 2020, "Buy"              , 125    , 4750000 , 4750000 , true      , 0   , 0    , 0     , 450 },
+        { 5, 11, 5 , 2020, "Split 2:1"        , 900    , 0       , 0       , true      , 0   , 0    , 0     , 900 },
+        { 1, 21, 5 , 2020, "Sell"             , 135    , 2150000 , 2149005 , false     , 995 , 0    , 574702, 765 },
+        { 0, 3 , 6 , 2020, "Buy"              , 150    , 2100000 , 2100000 , true      , 0   , 0    , 0     , 915 },
+        { 1, 10, 6 , 2020, "Sell"             , 915    , 12810000, 12809005, false     , 995 , 0    , 1783309, 0 },
+
+        // bal=0. next line is "open short".
+        { 1, 10, 6 , 2020, "Short Sell"       , 85     , 1190000 , 1189005 , true      , 995 , 0    , 0     , -85 },
+
+        // bal<0. next lines are short_types
+        { 0, 15, 6 , 2020, "Short Sell"       , 65     , 1105000 , 1104005 , true      , 995 , 0    , 0     , -150 },
+        { 1, 16, 6 , 2020, "Cover Buy"        , 50     , 500000  , 500995  , false     , 995 , 0    ,-264337, -100 },
+        { 5, 17, 6 , 2020, "Split 2:1"        , -200   , 0       , 0       , false     , 0   , 0    , 0     , -200 },
+        { 6, 18, 6 , 2020, "Reverse Split"    , -100   , 0       , 0       , false     , 0   , 0    , 0     , -100 },
+        { 2, 19, 6 , 2020, "Comp Dividend"    , 0      , 0       , 50000   , false     , 0   , 50000, 0     , -100 },
+        { 3, 19, 6 , 2020, "Comp ROC"         , 0      , 250000  , 250000  , false     , 0   , 0    , 0     , -100 },
+        { 4, 19, 6 , 2020, "Comp ND"          , 0      , 20000   , 0       , false     , 0   , 20000, 0     , -100 },
+        { 1, 20, 6 , 2020, "Cover Buy"        , 100    , 800000  , 800498  , false     , 498 , 0    ,-498673, 0 },
+
+        // bal=0. next line is "open long".
+        { 0, 20, 6 , 2020, "Buy"              , 100    , 800000  , 800498  , true      , 498 , 0    , 0     , 100 },
+
+        // bal>0. next lines are long_types
+        { 2, 21, 6 , 2020, "Dividend"         , 0      , 0       , 7000    , false     , 0   , 7000 , 0     , 100 },
+        { 2, 25, 6 , 2020, "Dividend"         , 0      , 0       , 11000   , false     , 0   , 11000, 0     , 100 },
+        { 0, 25, 6 , 2020, "+ Reinv"          , 1      , 10000   , 10000   , false     , 0   , 0    , 0     , 101 },
+        { 1, 26, 6 , 2020, "Sell remainder"   , 1      , 10000   , 10000   , false     , 0   , 0    , 1975  , 100 },
+        { 6, 26, 6 , 2020, "Reverse Split 1:2", 50     , 0       , 0       , false     , 0   , 0    , 0     , 50 },
+        { 4, 27, 6 , 2020, "ND"               , 0      , 10000   , 0       , false     , 0   , 10000, 0     , 50 }
+    }
+};
+
+class Stock_AssistantTest : public ::testing::TestWithParam<TestCase *> {
+protected:
+    std::shared_ptr<QofBook> m_book;
+    gnc_commodity *stock_commodity, *USD;
+    Account *broker_account, *stock_account, *cash_account, *dividend_account,
+        *capgains_account, *fees_account;
+    TestCase &m_testCase;
+
+public:
+    Stock_AssistantTest() :
+        m_book (qof_book_new (), qof_book_destroy),
+        broker_account (xaccMallocAccount (m_book.get ())),
+        stock_account (xaccMallocAccount (m_book.get ())),
+        cash_account (xaccMallocAccount (m_book.get ())),
+        dividend_account (xaccMallocAccount (m_book.get ())),
+        capgains_account (xaccMallocAccount (m_book.get ())),
+        fees_account (xaccMallocAccount (m_book.get ())),
+        m_testCase (*GetParam ())
+    {
+        qof_init();
+        qof_book_register ();
+        gnc_pricedb_register();
+        gnc_commodity_table_register ();
+
+        stock_commodity = gnc_commodity_new (m_book.get(), "SPY", "", "SPY", "", 100);
+        USD = gnc_commodity_table_lookup (gnc_commodity_table_get_table (m_book.get()),
+                                          "CURRENCY", "USD");
+
+        xaccAccountBeginEdit (broker_account);
+        xaccAccountSetName (broker_account, "Broker Account");
+        xaccAccountSetType (broker_account, ACCT_TYPE_CASH);
+        xaccAccountSetCommodity (broker_account, USD);
+
+        xaccAccountBeginEdit (stock_account);
+        xaccAccountSetName (stock_account, "Stock Account");
+        xaccAccountSetType (stock_account, ACCT_TYPE_STOCK);
+        xaccAccountSetCommodity (stock_account, stock_commodity);
+        gnc_account_append_child (broker_account, stock_account);
+        xaccAccountCommitEdit (broker_account);
+        xaccAccountCommitEdit (stock_account);
+
+        xaccAccountBeginEdit (cash_account);
+        xaccAccountSetName (cash_account, "Cash Account");
+        xaccAccountSetType (cash_account, ACCT_TYPE_BANK);
+        xaccAccountSetCommodity (cash_account, USD);
+        xaccAccountCommitEdit (cash_account);
+
+        xaccAccountBeginEdit (dividend_account);
+        xaccAccountSetName (dividend_account, "Dividend Account");
+        xaccAccountSetType (dividend_account, ACCT_TYPE_INCOME);
+        xaccAccountSetCommodity (dividend_account, USD);
+        xaccAccountCommitEdit (dividend_account);
+
+        xaccAccountBeginEdit (capgains_account);
+        xaccAccountSetName (capgains_account, "Capgains Account");
+        xaccAccountSetType (capgains_account, ACCT_TYPE_INCOME);
+        xaccAccountSetCommodity (capgains_account, USD);
+        xaccAccountCommitEdit (capgains_account);
+
+        xaccAccountBeginEdit (fees_account);
+        xaccAccountSetName (fees_account, "Fees Account");
+        xaccAccountSetType (fees_account, ACCT_TYPE_EXPENSE);
+        xaccAccountSetCommodity (fees_account, USD);
+        xaccAccountCommitEdit (fees_account);
+    }
+};
+
+static void test_failure_modes (Account *stock_account)
+{
+    StockAssistantModel model (stock_account);
+    model.transaction_date = gnc_dmy2time64 (1, 1, 2022);
+
+    // resetting txn_types will work the first time
+    EXPECT_TRUE (model.maybe_reset_txn_types ());
+
+    // trying to reset again shouldn't be necessary
+    EXPECT_FALSE (model.maybe_reset_txn_types ());
+
+    // set transaction-date to a different date.
+    model.transaction_date = gnc_dmy2time64 (1, 2, 2022);
+
+    // resetting txn_types will now work.
+    EXPECT_TRUE (model.maybe_reset_txn_types ());
+
+    // the Model is empty. generating list of splits should fail.
+    auto [success_splits, summary, splitinfos] = model.generate_list_of_splits ();
+    EXPECT_FALSE (success_splits); // no data!
+
+    auto [success_create, txn] = model.create_transaction();
+    EXPECT_FALSE (success_create); // no transaction created.
+}
+
+static void dump_acct (Account *acct)
+{
+    auto bal = GncNumeric(0);
+    std::cout << '\n' << std::setw(20) << std::right << xaccAccountGetName (acct)
+              << " Bal=" << std::setw(10) << std::right << GncNumeric (xaccAccountGetBalance (acct))
+              << std::endl;
+    for (auto n = xaccAccountGetSplitList (acct); n; n = n->next)
+    {
+        auto s = static_cast<Split*>(n->data);
+        bal += xaccSplitGetAmount (s);
+        std::cout << std::setw(20) << std::right << GncDateTime (xaccTransGetDate (xaccSplitGetParent (s))).format_iso8601()
+                  << " amt=" << std::setw(10) << std::right << GncNumeric (xaccSplitGetAmount (s))
+                  << " val=" << std::setw(10) << std::right << GncNumeric (xaccSplitGetValue (s))
+                  << " bal=" << std::setw(10) << std::right << bal
+                  << std::endl;
+    }
+}
+
+TEST_P(Stock_AssistantTest, DoesStock_Assistant)
+{
+    test_failure_modes (stock_account);
+    for (const auto& t : m_testCase.tests)
+    {
+        StockAssistantModel model (stock_account);
+        model.transaction_date = gnc_dmy2time64 (t.dd, t.mm, t.yy);
+        model.maybe_reset_txn_types ();
+
+        model.set_txn_type (t.type_idx);
+        model.transaction_description = t.desc;
+        model.stock_amount = gnc_numeric_create (t.stock_amt * 100, 100);
+        model.stock_value = gnc_numeric_create (t.stock_val, 100);
+        model.cash_value = gnc_numeric_create (t.cash_val, 100);
+        model.cash_account = cash_account;
+        model.fees_account = fees_account;
+        model.fees_capitalize = t.capitalize;
+        model.fees_value = gnc_numeric_create (t.fees_val, 100);
+        model.capgains_account = capgains_account;
+        model.capgains_value = gnc_numeric_create (t.capg_val, 100);
+        model.dividend_account = dividend_account;
+        model.dividend_value = gnc_numeric_create (t.divi_val, 100);
+
+        auto [success_splits, summary, splitinfos] = model.generate_list_of_splits ();
+        EXPECT_TRUE (success_splits) << t.dd << '/' << t.mm << '/' << t.yy << ": "
+                                     << t.desc << '='
+                                     << GncNumeric(xaccAccountGetBalance (stock_account))
+                                     << '\n' << summary;
+
+        auto [success_txn, txn] = model.create_transaction ();
+        EXPECT_TRUE (success_txn);
+
+        EXPECT_EQ (xaccAccountGetBalance (stock_account).num, t.new_bal * 100);
+    }
+
+    dump_acct (stock_account);
+    dump_acct (dividend_account);
+    dump_acct (capgains_account);
+    dump_acct (fees_account);
+    dump_acct (cash_account);
+    EXPECT_EQ (xaccAccountGetBalance (dividend_account).num, 42000);
+    EXPECT_EQ (xaccAccountGetBalance (capgains_account).num, -995981);
+    EXPECT_EQ (xaccAccountGetBalance (fees_account).num, 4478);
+    EXPECT_EQ (xaccAccountGetBalance (cash_account).num, 1663049);
+}
+
+#ifndef INSTANTIATE_TEST_SUITE_P
+// Silence "no previous declaration for" (treated as error due to -Werror) when building with GoogleTest < 1.8.1
+static testing::internal::ParamGenerator<TestCase*>
+gtest_InstantiationStock_AssistantTestStock_AssistantTest_EvalGenerator_();
+static std::string gtest_InstantiationStock_AssistantTestStock_AssistantTest_EvalGenerateName_(const testing::TestParamInfo<TestCase*>&);
+
+INSTANTIATE_TEST_CASE_P(
+#else // INSTANTIATE_TEST_SUITE_P
+INSTANTIATE_TEST_SUITE_P(
+#endif // INSTANTIATE_TEST_SUITE_P
+    InstantiationStock_AssistantTest,
+    Stock_AssistantTest,
+    ::testing::Values(
+        &easyTestCase
+    )
+);

commit f36e5d56ed09ad8a4effd867be8a71a59a8ec628
Author: Christopher Lam <christopher.lck at gmail.com>
Date:   Sat Oct 1 22:41:22 2022 +0800

    [assistant-stock-transaction] refactor using MVC

diff --git a/gnucash/gnome/assistant-stock-transaction.cpp b/gnucash/gnome/assistant-stock-transaction.cpp
index f2418e1d0b..abde72b1a7 100644
--- a/gnucash/gnome/assistant-stock-transaction.cpp
+++ b/gnucash/gnome/assistant-stock-transaction.cpp
@@ -24,12 +24,14 @@
 
 #include <gtk/gtk.h>
 #include <glib/gi18n.h>
+#include <memory>
 #include <vector>
 #include <string>
 #include <numeric>
 #include <algorithm>
 #include <optional>
 #include <stdexcept>
+#include <sstream>
 
 #include "Transaction.h"
 #include "engine-helpers.h"
@@ -57,6 +59,7 @@ bool operator &(FieldMask lhs, FieldMask rhs);
 FieldMask operator |(FieldMask lhs, FieldMask rhs);
 FieldMask operator ^(FieldMask lhs, FieldMask rhs);
 
+static const char* GNC_PREFS_GROUP = "dialogs.stock-assistant";
 static const char* ASSISTANT_STOCK_TRANSACTION_CM_CLASS = "assistant-stock-transaction";
 
 enum assistant_pages
@@ -375,31 +378,546 @@ then record the reverse split.")
     }
 };
 
-struct StockTransactionInfo
+struct StockTransactionSplitInfo
 {
-    GtkWidget * window;
+    bool debit_side;
+    std::string account_str;
+    std::string memo_str;
+    std::string action_str;
+    std::string value_str;
+    std::string units_str;
+    bool units_in_red = false;
+    Account* account = nullptr;
+    gnc_numeric value_numeric = gnc_numeric_create (1, 0); // invalid gnc_numerics
+    gnc_numeric units_numeric = gnc_numeric_create (1, 0);
+
+    void create_split(Transaction *trans, AccountVec& account_commits)
+    {
+        g_return_if_fail (trans);
+        if (!this->account ||
+            gnc_numeric_check (this->value_numeric) ||
+            gnc_numeric_check (this->units_numeric))
+            return;
+        auto split = xaccMallocSplit (qof_instance_get_book (trans));
+        xaccSplitSetParent (split, trans);
+        xaccAccountBeginEdit (this->account);
+        account_commits.emplace_back (this->account);
+        xaccSplitSetAccount (split, this->account);
+        xaccSplitSetMemo (split, this->memo_str.c_str());
+        xaccSplitSetValue (split, this->value_numeric);
+        xaccSplitSetAmount (split, this->units_numeric);
+        DEBUG ("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
+               this->action_str.c_str(), this->account_str.c_str(),
+               gnc_num_dbg_to_string (this->value_numeric),
+               gnc_num_dbg_to_string (this->units_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", this->action_str.c_str()));
+    }
+    StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo constructor\n"); };
+    StockTransactionSplitInfo (Account *acct, gnc_numeric val)
+        : account_str (xaccAccountGetName (acct))
+        , account (acct)
+        , value_numeric (val)
+    { DEBUG ("StockTransactionSplitInfo constructor\n"); };
+    ~StockTransactionSplitInfo () { DEBUG ("StockTransactionSplitInfo destructor\n"); };
+};
 
-    std::optional<TxnTypeVec> txn_types;
-    // the following stores date at which the txn_types were set. If
-    // the GNCDateEdit date is modified, it will trigger recreation of
-    // the txn_types above.
-    std::optional<time64>     txn_types_date;
+
+static StockTransactionSplitInfo
+check_page (gnc_numeric& debit, gnc_numeric& credit, StringVec& errors,
+            FieldMask splitfield, Account *acct, const char *memo,
+            gnc_numeric amount, const char* page, GNCPrintAmountInfo curr_pinfo)
+{
+    // Translators: (missing) denotes that the amount or account is
+    // not provided, or incorrect, in the Stock Transaction Assistant.
+    const char* missing_str = N_("(missing)");
+    auto line = StockTransactionSplitInfo ();
+    auto add_error = [&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);
+    };
+
+    DEBUG ("page=%s, amount=%s", page, gnc_num_dbg_to_string (amount));
+    if (memo)
+        line.memo_str = memo;
+    line.debit_side = (splitfield & FieldMask::ENABLED_DEBIT);
+    if (page)
+        line.action_str = page;
+
+    if (gnc_numeric_check (amount))
+    {
+        if (splitfield & FieldMask::ALLOW_ZERO)
+            // line.value_numeric contains an invalid gnc_numeric
+            line.value_str = "";
+        else
+        {
+            add_error (N_("Amount for %s is missing."), page);
+            line.value_str = _(missing_str);
+        }
+    }
+    else
+    {
+        if (!(splitfield & FieldMask::ALLOW_NEGATIVE))
+        {
+            if ((splitfield & FieldMask::ALLOW_ZERO) && gnc_numeric_negative_p (amount))
+                add_error (N_("Amount for %s must not be negative."), page);
+            else if (!(splitfield & FieldMask::ALLOW_ZERO) && !gnc_numeric_positive_p (amount))
+                add_error (N_("Amount for %s must be positive."), page);
+        }
+        if (gnc_numeric_negative_p (amount))
+        {
+            amount = gnc_numeric_neg (amount);
+            line.debit_side = !line.debit_side;
+        }
+        if (line.debit_side)
+            debit = gnc_numeric_add_fixed (debit, amount);
+        else
+            credit = gnc_numeric_add_fixed (credit, amount);
+        line.units_numeric = line.debit_side ? amount : gnc_numeric_neg (amount);
+        line.value_numeric = line.debit_side ? amount : gnc_numeric_neg (amount);
+        line.value_str = xaccPrintAmount (amount, curr_pinfo);
+    }
+
+    if (acct)
+    {
+        line.account = acct;
+        line.account_str = xaccAccountGetName (acct);
+    }
+    else if ((splitfield & FieldMask::ALLOW_ZERO) &&
+             (gnc_numeric_check (amount) || gnc_numeric_zero_p (amount)))
+        line.account_str = "";
+    else
+    {
+        add_error (N_("Account for %s is missing."), page);
+        line.account_str = _(missing_str);
+    }
+    return line;
+}
+
+using SplitInfoVec = std::vector<StockTransactionSplitInfo>;
+
+struct StockAssistantModel
+{
     Account   * acct;
+
     gnc_commodity * currency;
+    GNCPrintAmountInfo curr_pinfo;
+    GNCPrintAmountInfo price_pinfo;
+    GNCPrintAmountInfo stock_pinfo;
+
+    time64      transaction_date;
+    std::optional<TxnTypeVec> txn_types;
+
+    std::optional<TxnTypeInfo> txn_type;
+
+    const gchar *transaction_description;
+    gnc_numeric balance_at_date = gnc_numeric_create (1, 0);
+
+    bool input_new_balance;
+    bool stock_amount_enabled;
+    gnc_numeric stock_amount = gnc_numeric_create (1, 0);
+
+    bool stock_value_enabled;
+    gnc_numeric stock_value = gnc_numeric_create (1, 0);
+    const gchar* stock_memo = nullptr;
+
+    bool cash_enabled;
+    Account *cash_account = nullptr;
+    const gchar* cash_memo = nullptr;
+    gnc_numeric cash_value = gnc_numeric_create (1, 0);
+
+    bool fees_enabled;
+    bool fees_capitalize;
+    Account *fees_account = nullptr;
+    const gchar* fees_memo = nullptr;
+    gnc_numeric fees_value = gnc_numeric_create (1, 0);
+
+    bool dividend_enabled;
+    Account *dividend_account = nullptr;
+    const gchar* dividend_memo = nullptr;
+    gnc_numeric dividend_value = gnc_numeric_create (1, 0);
+
+    bool capgains_enabled;
+    Account *capgains_account = nullptr;
+    const gchar* capgains_memo = nullptr;
+    gnc_numeric capgains_value = gnc_numeric_create (1, 0);
+
+    // consider reset txn_types. return false if txn_types are still
+    // current (i.e. transaction_date hasn't changed).
+    bool maybe_reset_txn_types ()
+    {
+        auto new_bal = xaccAccountGetBalanceAsOfDate
+            (this->acct, gnc_time64_get_day_end (this->transaction_date));
+        if (this->txn_types_date && this->txn_types_date == this->transaction_date &&
+            gnc_numeric_equal (this->balance_at_date, new_bal))
+            return false;
+        this->balance_at_date = new_bal;
+        this->txn_types_date = this->transaction_date;
+        this->txn_types = gnc_numeric_zero_p (this->balance_at_date) ? starting_types
+            : gnc_numeric_positive_p (this->balance_at_date) ? long_types
+            : short_types;
+        return true;
+    };
+
+    bool set_txn_type (guint type_idx)
+    {
+        if (!this->txn_types_date || this->txn_types_date != this->transaction_date)
+        {
+            PERR ("transaction_date has changed. rerun maybe_reset_txn_types!");
+            return false;
+        }
+        try
+        {
+            this->txn_type = this->txn_types->at (type_idx);
+        }
+        catch (const std::out_of_range&)
+        {
+            PERR ("out of range type_idx=%d", type_idx);
+            return false;
+        }
+        this->input_new_balance = this->txn_type->input_new_balance;
+        this->stock_amount_enabled = this->txn_type->stock_amount != FieldMask::DISABLED;
+        this->stock_value_enabled = this->txn_type->stock_value != FieldMask::DISABLED;
+        this->fees_capitalize = this->txn_type->fees_capitalize;
+        this->fees_enabled = this->txn_type->fees_value != FieldMask::DISABLED;
+        this->capgains_enabled = this->txn_type->capgains_value != FieldMask::DISABLED;
+        this->dividend_enabled = this->txn_type->dividend_value != FieldMask::DISABLED;
+        this->cash_enabled = this->txn_type->cash_value != FieldMask::DISABLED;
+        return true;
+    };
+
+    std::string get_stock_balance_str ()
+    {
+        return xaccPrintAmount (this->balance_at_date, this->stock_pinfo);
+    };
+
+    std::string get_new_amount_str ()
+    {
+        if (gnc_numeric_check (this->stock_amount))
+            return "";
+
+        if (this->input_new_balance)
+        {
+            auto ratio = gnc_numeric_div (this->stock_amount, this->balance_at_date,
+                                          GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+            if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
+                return "";
+
+            std::ostringstream ret;
+            ret << ratio.num << ':' << ratio.denom;
+            return ret.str();
+        }
+        else
+        {
+            auto amount = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT) ?
+                gnc_numeric_neg (this->stock_amount) : this->stock_amount;
+            amount = gnc_numeric_add_fixed (amount, this->balance_at_date);
+            return xaccPrintAmount (amount, stock_pinfo);
+        }
+    };
+
+    std::tuple<bool, gnc_numeric, const char*> calculate_price ()
+    {
+        if (this->input_new_balance ||
+            !this->stock_amount_enabled || gnc_numeric_check (this->stock_amount) ||
+            !this->stock_value_enabled || gnc_numeric_check (this->stock_value) ||
+            gnc_numeric_zero_p (this->stock_amount) ||
+            gnc_numeric_zero_p (this->stock_value))
+            return { false, gnc_numeric_create (1, 0), nullptr };
+
+        auto price = gnc_numeric_div (this->stock_value, this->stock_amount,
+                                      GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
+        return {true, price, xaccPrintAmount (price, this->price_pinfo)};
+    }
+
+
+    std::tuple<bool, std::string, SplitInfoVec> generate_list_of_splits ()
+    {
+        if (!this->txn_types || !this->txn_type)
+            return { false, "Error: txn_type not initialized", {} };
+
+        this->list_of_splits.clear();
+
+        gnc_numeric debit = gnc_numeric_zero ();
+        gnc_numeric credit = gnc_numeric_zero ();
+        StringVec errors, warnings, infos;
+        StockTransactionSplitInfo line;
+        bool negative_in_red = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL,
+                                                   GNC_PREF_NEGATIVE_IN_RED);
+        auto add_error_str = [&errors]
+            (const char* str) { errors.emplace_back (_(str)); };
+
+        // check the stock transaction date. If there are existing stock
+        // transactions dated after the date specified, it is very likely
+        // the later stock transactions will be invalidated. warn the user
+        // to review them.
+        auto last_split_node = g_list_last (xaccAccountGetSplitList (this->acct));
+        if (last_split_node)
+        {
+            auto last_split = static_cast<const Split*> (last_split_node->data);
+            auto last_split_date = xaccTransGetDate (xaccSplitGetParent (last_split));
+            if (this->transaction_date <= last_split_date)
+            {
+                auto last_split_date_str = qof_print_date (last_split_date);
+                auto new_date_str = qof_print_date (this->transaction_date);
+                // Translators: the first %s is the new transaction date;
+                // the second %s is the current stock account's latest
+                // transaction date.
+                auto warn_txt =  g_strdup_printf (_("You will enter a transaction \
+with date %s which is earlier than the latest transaction in this account, \
+dated %s. Doing so may affect the cost basis, and therefore capital gains, \
+of transactions dated after the new entry. Please review all transactions \
+to ensure proper recording."), new_date_str, last_split_date_str);
+                warnings.push_back (warn_txt);
+                g_free (warn_txt);
+                g_free (new_date_str);
+                g_free (last_split_date_str);
+            }
+        }
+
+        if (!this->stock_value_enabled)
+            line = StockTransactionSplitInfo (this->acct, gnc_numeric_zero());
+        else
+            line = check_page (debit, credit, errors, this->txn_type->stock_value,
+                               this->acct, this->stock_memo, this->stock_value,
+                               NC_ ("Stock Assistant: Page name", "stock value"),
+                               this->curr_pinfo);
+
+
+        if (!this->stock_amount_enabled)
+            line.units_numeric = gnc_numeric_zero();
+        else if (gnc_numeric_check (this->stock_amount))
+        {
+            line.units_str = _("(missing)");
+            line.units_numeric = gnc_numeric_zero();
+            add_error_str (N_("Amount for stock units is missing"));
+        }
+        else if (this->input_new_balance)
+        {
+            auto stock_amount = this->stock_amount;
+            auto credit_side = (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT);
+            auto delta = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
+            auto ratio = gnc_numeric_div (stock_amount, this->balance_at_date,
+                                          GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+            stock_amount = gnc_numeric_sub_fixed (stock_amount, this->balance_at_date);
+            line.units_numeric = stock_amount;
+            line.units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+            line.units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
+            if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
+                add_error_str (N_("Invalid stock new balance."));
+            else if (gnc_numeric_negative_p (delta) && !credit_side)
+                add_error_str (N_("New balance must be higher than old balance."));
+            else if (gnc_numeric_positive_p (delta) && credit_side)
+                add_error_str (N_("New balance must be lower than old balance."));
+        }
+        else
+        {
+            auto stock_amount = this->stock_amount;
+            if (!gnc_numeric_positive_p (stock_amount))
+                add_error_str (N_("Stock amount must be positive."));
+            if (this->txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
+                stock_amount = gnc_numeric_neg (stock_amount);
+            line.units_numeric = stock_amount;
+            line.units_str = xaccPrintAmount (stock_amount, this->stock_pinfo);
+            line.units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
+            auto new_bal = gnc_numeric_add_fixed (this->balance_at_date, stock_amount);
+            if (gnc_numeric_positive_p (this->balance_at_date) &&
+                gnc_numeric_negative_p (new_bal))
+                add_error_str (N_("Cannot sell more units than owned."));
+            else if (gnc_numeric_negative_p (this->balance_at_date) &&
+                     gnc_numeric_positive_p (new_bal))
+                add_error_str (N_("Cannot cover buy more units than owed."));
+        }
+
+        this->list_of_splits.push_back (std::move (line));
+
+        auto [has_price, price, price_str] = this->calculate_price ();
+        if (has_price)
+        {
+            // Translators: %s refer to: stock mnemonic, broker currency,
+            // date of transaction.
+            auto tmpl = N_("A price of 1 %s = %s on %s will be recorded.");
+            auto date_str = qof_print_date (this->transaction_date);
+            auto price_msg = g_strdup_printf
+                (_(tmpl),
+                 gnc_commodity_get_mnemonic (xaccAccountGetCommodity (this->acct)),
+                 price_str, date_str);
+            infos.emplace_back (price_msg);
+            g_free (date_str);
+        }
+
+        if (this->cash_enabled)
+        {
+            line = check_page (debit, credit, errors, this->txn_type->cash_value,
+                               this->cash_account, this->cash_memo, this->cash_value,
+                               NC_ ("Stock Assistant: Page name", "cash"),
+                               this->curr_pinfo);
+            this->list_of_splits.push_back (std::move (line));
+        }
+
+        if (this->fees_enabled)
+        {
+            line = check_page (debit, credit, errors, this->txn_type->fees_value,
+                               this->fees_capitalize ? this->acct : this->fees_account,
+                               this->fees_memo, this->fees_value,
+                               NC_ ("Stock Assistant: Page name", "fees"),
+                               this->curr_pinfo);
+            if (this->fees_capitalize)
+                line.units_numeric = gnc_numeric_zero();
+            this->list_of_splits.push_back (std::move (line));
+        }
+
+        if (this->dividend_enabled)
+        {
+            line = check_page (debit, credit, errors, this->txn_type->dividend_value,
+                               this->dividend_account, this->dividend_memo,
+                               this->dividend_value,
+                               NC_ ("Stock Assistant: Page name", "dividend"),
+                               this->curr_pinfo);
+            this->list_of_splits.push_back (std::move (line));
+        }
+
+        // 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 (this->capgains_enabled)
+        {
+            line = check_page (debit, credit, errors, this->txn_type->capgains_value,
+                               this->capgains_account, this->capgains_memo,
+                               this->capgains_value,
+                               NC_ ("Stock Assistant: Page name", "capital gains"),
+                               this->curr_pinfo);
+            this->list_of_splits.push_back (std::move (line));
+
+            line = check_page (debit, credit, errors, this->txn_type->capgains_value ^
+                               (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
+                               this->acct, this->capgains_memo, this->capgains_value,
+                               NC_ ("Stock Assistant: Page name", "capital gains"),
+                               this->curr_pinfo);
+            line.units_numeric = gnc_numeric_zero();
+            this->list_of_splits.push_back (std::move (line));
+        }
+
+        if (!gnc_numeric_equal (debit, credit))
+        {
+            auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
+            auto debit_str = g_strdup (xaccPrintAmount (debit, this->curr_pinfo));
+            auto credit_str = g_strdup (xaccPrintAmount (credit, this->curr_pinfo));
+            auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
+            errors.emplace_back (error_str);
+            g_free (error_str);
+            g_free (credit_str);
+            g_free (debit_str);
+        }
+
+        // generate final summary message. Collates a header, the errors
+        // and warnings. Then allow completion if errors is empty.
+        std::ostringstream summary;
+        auto summary_add = [&summary](auto a) { summary << "\n• " << a; };
+        if (errors.empty())
+        {
+            summary << _("No errors found. Click Apply to create transaction.");
+            std::for_each (infos.begin(), infos.end(), summary_add);
+        }
+        else
+        {
+            summary << _("The following errors must be fixed:");
+            std::for_each (errors.begin(), errors.end(), summary_add);
+        }
+        if (!warnings.empty())
+        {
+            summary << "\n\n" << _("The following warnings exist:");
+            std::for_each (warnings.begin(), warnings.end(), summary_add);
+        }
+        this->ready_to_create = errors.empty();
+        return { this->ready_to_create, summary.str(), this->list_of_splits };
+    }
+
+    std::tuple<bool, Transaction*> create_transaction ()
+    {
+        if (!this->ready_to_create)
+        {
+            PERR ("errors exist. cannot create transaction.");
+            return { false, nullptr };
+        }
+        auto book = qof_instance_get_book (acct);
+        auto trans = xaccMallocTransaction (book);
+        xaccTransBeginEdit (trans);
+        xaccTransSetCurrency (trans, this->currency);
+        xaccTransSetDescription (trans, this->transaction_description);
+        xaccTransSetDatePostedSecsNormalized (trans, this->transaction_date);
+        AccountVec accounts;
+        std::for_each (this->list_of_splits.begin(), this->list_of_splits.end(),
+                       [&](auto& line){ line.create_split (trans, accounts); });
+        this->add_price (book);
+        xaccTransCommitEdit (trans);
+        std::for_each (accounts.begin(), accounts.end(), xaccAccountCommitEdit);
+        this->ready_to_create = false;
+        return { true, trans };
+    }
+
+    StockAssistantModel (Account *account)
+        : acct (account)
+        , currency (gnc_account_get_currency_or_parent (account))
+        , curr_pinfo (gnc_commodity_print_info (this->currency, true))
+        , price_pinfo (gnc_price_print_info (this->currency, true))
+        , stock_pinfo (gnc_commodity_print_info (xaccAccountGetCommodity (account), true))
+    { DEBUG ("StockAssistantModel constructor\n"); };
+
+    ~StockAssistantModel(){ DEBUG ("StockAssistantModel destructor\n"); };
+
+private:
+    std::optional<time64>     txn_types_date;
+    bool ready_to_create = false;
+
+    SplitInfoVec list_of_splits;
+
+    void add_price (QofBook *book)
+    {
+        auto [has_price, p, price_str] = this->calculate_price ();
+        if (!has_price)
+            return;
+
+        auto price = gnc_price_create (book);
+        gnc_price_begin_edit (price);
+        gnc_price_set_commodity (price, xaccAccountGetCommodity (this->acct));
+        gnc_price_set_currency (price, this->currency);
+        gnc_price_set_time64 (price, this->transaction_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 pdb = gnc_pricedb_get_db (book);
+        if (!gnc_pricedb_add_price (pdb, price))
+            PWARN ("error adding price");
+
+        gnc_price_unref (price);
+    }
+};
+
+
+struct StockAssistantView
+{
+    GtkWidget * window;
 
     // transaction type page
     GtkWidget * transaction_type_page;
     GtkWidget * transaction_type_combo;
     GtkWidget * transaction_type_explanation;
-    std::optional<TxnTypeInfo> txn_type;
 
     // transaction details page
     GtkWidget * transaction_details_page;
-    GtkWidget * date_edit;
-    GtkWidget * transaction_description_entry;
+    GtkWidget * transaction_date;
+    GtkWidget * transaction_description;
 
     // stock amount page
-    gnc_numeric balance_at_date;
     GtkWidget * stock_amount_page;
     GtkWidget * stock_amount_title;
     GtkWidget * prev_amount;
@@ -443,737 +961,462 @@ struct StockTransactionInfo
     GtkWidget * finish_page;
     GtkWidget * finish_split_view;
     GtkWidget * finish_summary;
-};
 
+    void set_focus (GtkWidget *widget) { gtk_widget_grab_focus (widget); }
+    void set_focus_gae (GtkWidget *gae) { set_focus (GTK_WIDGET (gnc_amount_edit_gtk_entry (GNC_AMOUNT_EDIT (gae)))); }
 
-/******* implementations ***********************************************/
-static void
-stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data)
-{
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-    gnc_unregister_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info);
-    info->txn_types_date = std::nullopt;
-    info->txn_types = std::nullopt;
-    info->txn_type = std::nullopt;
-    g_free (info);
-}
-
-static void
-refresh_page_transaction_type (GtkWidget *widget, gpointer user_data)
-{
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-
-    auto type_idx = gtk_combo_box_get_active (GTK_COMBO_BOX (widget));
-    if (type_idx < 0)           // combo isn't initialized yet.
-        return;
+    int get_transaction_type_index ()
+    { return gtk_combo_box_get_active (GTK_COMBO_BOX (transaction_type_combo)); }
 
-    if (!info->txn_types)
-        return;
-
-    try
+    void set_transaction_types (const TxnTypeVec& txn_types)
     {
-        info->txn_type = info->txn_types->at (type_idx);
-    }
-    catch (const std::out_of_range&)
+        auto combo = GTK_COMBO_BOX_TEXT (this->transaction_type_combo);
+        gtk_combo_box_text_remove_all (combo);
+        std::for_each (txn_types.begin(), txn_types.end(),
+                       [&combo](const auto& it)
+                       { gtk_combo_box_text_append_text (combo, _(it.friendly_name)); });
+        gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
+    };
+
+    void set_txn_type_explanation (const gchar *txt)
+    { gtk_label_set_text (GTK_LABEL (this->transaction_type_explanation), txt); };
+
+    void
+    prepare_stock_amount_page (bool input_new_balance, const std::string prev_balance)
     {
-        PERR ("out of range type_idx=%d", type_idx);
-        return;
-    }
-
-    g_return_if_fail (info->txn_type);
-
-    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 std::optional<gnc_numeric>
-calculate_price (StockTransactionInfo* info)
-{
-    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))
-        return std::nullopt;
+        gtk_label_set_text_with_mnemonic
+            (GTK_LABEL (this->stock_amount_label),
+             input_new_balance ? _("Ne_w Balance") : _("_Shares"));
+        gtk_label_set_text
+            (GTK_LABEL (this->next_amount_label),
+             input_new_balance ? _("Ratio") : _("Next Balance"));
+        gtk_label_set_text
+            (GTK_LABEL (this->stock_amount_title),
+             input_new_balance ?
+             _("Enter the new balance of shares after the stock split.") :
+             _("Enter the number of shares you gained or lost in the transaction."));
+        gtk_label_set_text (GTK_LABEL (this->prev_amount), prev_balance.c_str());
+    };
 
-    if (gnc_numeric_zero_p (amount) || gnc_numeric_zero_p (value))
-        return std::nullopt;
+    void set_stock_amount (std::string new_amount_str)
+    {
+        gtk_label_set_text (GTK_LABEL(this->next_amount), new_amount_str.c_str());
+    };
 
-    return gnc_numeric_div (value, amount, GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT);
-}
+    void set_price_value (const gchar *val)
+    { gtk_label_set_text (GTK_LABEL (this->price_value), val); };
 
-static void
-refresh_page_stock_amount (GtkWidget *widget, gpointer user_data)
-{
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-    g_return_if_fail (info->txn_type);
+    bool get_capitalize_fees ()
+    { return gtk_toggle_button_get_active
+            (GTK_TOGGLE_BUTTON (this->capitalize_fees_checkbox)); }
 
-    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));
+    void set_capitalize_fees (bool state)
+    {
+        gtk_toggle_button_set_active
+            (GTK_TOGGLE_BUTTON (this->capitalize_fees_checkbox), state);
+    }
 
-    gnc_numeric stock_amount;
+    void update_fees_acct_sensitive (bool sensitive)
+    { gtk_widget_set_sensitive (this->fees_account, sensitive); }
 
-    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->input_new_balance)
+    void prepare_finish_page (bool success, const std::string& summary,
+                              const SplitInfoVec& list_of_splits)
     {
-        gnc_numeric ratio = gnc_numeric_div (stock_amount, bal,
-                                             GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
-        if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
-            gtk_label_set_text (GTK_LABEL(info->next_amount), nullptr);
-        else
+        auto gtv = GTK_TREE_VIEW (this->finish_split_view);
+        auto list = GTK_LIST_STORE (gtk_tree_view_get_model (gtv));
+        gtk_list_store_clear (list);
+        for (const auto& line : list_of_splits)
         {
-            auto str = gnc_numeric_to_string (ratio);
-            auto p = str ? strchr (str, '/') : nullptr;
-            if (p)
-                *p = ':';
-            auto lbl = g_strdup_printf (_("%s Split"), str);
-            gtk_label_set_text (GTK_LABEL(info->next_amount), lbl);
-            g_free (lbl);
-            g_free (str);
+            GtkTreeIter iter;
+            auto tooltip = g_markup_escape_text (line.memo_str.c_str(), -1);
+            gtk_list_store_append (list, &iter);
+            gtk_list_store_set (list, &iter,
+                                SPLIT_COL_ACCOUNT, line.account_str.c_str(),
+                                SPLIT_COL_MEMO, line.memo_str.c_str(),
+                                SPLIT_COL_TOOLTIP, tooltip,
+                                SPLIT_COL_DEBIT, line.debit_side ? line.value_str.c_str() : nullptr,
+                                SPLIT_COL_CREDIT, line.debit_side ? nullptr : line.value_str.c_str(),
+                                SPLIT_COL_UNITS, line.units_str.c_str(),
+                                SPLIT_COL_UNITS_COLOR, line.units_in_red ? "red" : nullptr,
+                                -1);
+            g_free (tooltip);
         }
+        gtk_assistant_set_page_complete (GTK_ASSISTANT (this->window),
+                                         this->finish_page, success);
+        gtk_label_set_text (GTK_LABEL (this->finish_summary), summary.c_str());
     }
-    else
+
+    StockAssistantView (GtkBuilder *builder, gnc_commodity *stock_commodity,
+                        gnc_commodity *currency, GtkWidget *parent)
+        : window (get_widget (builder, "stock_transaction_assistant"))
+        , transaction_type_page (get_widget (builder, "transaction_type_page"))
+        , transaction_type_combo (get_widget (builder, "transaction_type_page_combobox"))
+        , transaction_type_explanation (get_widget (builder, "transaction_type_page_explanation"))
+        , transaction_details_page (get_widget (builder, "transaction_details_page"))
+        , transaction_date (create_date (builder, 0, "transaction_date_label", "transaction_details_table"))
+        , transaction_description (get_widget (builder, "transaction_description_entry"))
+        , stock_amount_page (get_widget (builder, "stock_amount_page"))
+        , stock_amount_title (get_widget (builder, "stock_amount_title"))
+        , prev_amount (get_widget (builder, "prev_balance_amount"))
+        , next_amount (get_widget (builder, "next_balance_amount"))
+        , next_amount_label (get_widget (builder, "next_balance_label"))
+        , stock_amount_edit (create_gae (builder, 1, stock_commodity, "stock_amount_table", "stock_amount_label"))
+        , stock_amount_label (get_widget (builder, "stock_amount_label"))
+        , stock_value_page (get_widget (builder, "stock_value_page"))
+        , stock_value_edit (create_gae (builder, 0, currency, "stock_value_table", "stock_value_label"))
+        , price_value (get_widget (builder, "stock_price_amount"))
+        , stock_memo_edit (get_widget (builder, "stock_memo_entry"))
+        , cash_page (get_widget (builder, "cash_details_page"))
+        , cash_account (create_gas (builder, 0, { ACCT_TYPE_ASSET, ACCT_TYPE_BANK }, currency,  "cash_table", "cash_account_label"))
+        , cash_memo_edit (get_widget (builder, "cash_memo_entry"))
+        , cash_value (create_gae (builder, 1, currency, "cash_table", "cash_label"))
+        , fees_page (get_widget (builder, "fees_details_page"))
+        , capitalize_fees_checkbox (get_widget (builder, "capitalize_fees_checkbutton"))
+        , fees_account (create_gas (builder, 1, { ACCT_TYPE_EXPENSE }, currency, "fees_table", "fees_account_label"))
+        , fees_memo_edit (get_widget (builder, "fees_memo_entry"))
+        , fees_value (create_gae (builder, 2, currency, "fees_table", "fees_label"))
+        , dividend_page (get_widget (builder, "dividend_details_page"))
+        , dividend_account (create_gas (builder, 0, { ACCT_TYPE_INCOME }, currency, "dividend_table", "dividend_account_label"))
+        , dividend_memo_edit (get_widget (builder, "dividend_memo_entry"))
+        , dividend_value (create_gae (builder, 1, currency, "dividend_table", "dividend_label"))
+        , capgains_page (get_widget (builder, "capgains_details_page"))
+        , capgains_account (create_gas (builder, 0, { ACCT_TYPE_INCOME }, currency, "capgains_table", "capgains_account_label"))
+        , capgains_memo_edit (get_widget (builder, "capgains_memo_entry"))
+        , capgains_value (create_gae (builder, 1, currency, "capgains_table", "capgains_label"))
+        , finish_page (get_widget (builder, "finish_page"))
+        , finish_split_view (get_treeview (builder, "transaction_view"))
+        , finish_summary (get_widget (builder, "finish_summary"))
+    {
+        // Set the name for this assistant so it can be easily manipulated with css
+        gtk_widget_set_name (GTK_WIDGET(this->window), "gnc-id-assistant-stock-transaction");
+        gtk_tree_view_set_tooltip_column (GTK_TREE_VIEW (this->finish_split_view),
+                                          SPLIT_COL_TOOLTIP);
+        gtk_window_set_transient_for (GTK_WINDOW (this->window), GTK_WINDOW(parent));
+        gnc_window_adjust_for_screen (GTK_WINDOW(this->window));
+        gnc_restore_window_size (GNC_PREFS_GROUP, GTK_WINDOW(this->window),
+                                 GTK_WINDOW(parent));
+        gtk_widget_show_all (this->window);
+        DEBUG ("StockAssistantView constructor\n");
+    };
+    ~StockAssistantView(){
+        gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(this->window));
+        DEBUG ("StockAssistantView destructor\n");
+    };
+
+private:
+    GtkWidget* get_widget (GtkBuilder *builder, const gchar * ID)
     {
-        if (info->txn_type->stock_amount == FieldMask::ENABLED_CREDIT)
-            stock_amount = gnc_numeric_neg (stock_amount);
-        bal = gnc_numeric_add_fixed (bal, stock_amount);
+        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);
+    }
 
-        gtk_label_set_text (GTK_LABEL(info->next_amount),
-                            xaccPrintAmount (bal, pinfo));
+    GtkWidget* create_gas (GtkBuilder *builder, gint row,
+                           std::vector<GNCAccountType> 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 ();
+        auto accum = [](auto a, auto b){ return g_list_prepend (a, (gpointer)b); };
+        auto null_glist = static_cast<GList*>(nullptr);
+        auto acct_list = std::accumulate (type.begin(), type.end(), null_glist, accum);
+        auto curr_list = accum (null_glist, 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;
     }
-}
 
+    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 void
-refresh_page_stock_value (GtkWidget *widget, gpointer user_data)
-{
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-    g_return_if_fail (info->txn_type);
+    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;
+    }
 
-    auto price = calculate_price (info);
-    if (!price.has_value())
+    GtkWidget* get_treeview (GtkBuilder *builder, const gchar *treeview_label)
     {
-        // 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 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,
+                                         G_TYPE_STRING, G_TYPE_STRING);
+        gtk_tree_view_set_model(view, GTK_TREE_MODEL(store));
+        gtk_tree_selection_set_mode (gtk_tree_view_get_selection (view),
+                                     GTK_SELECTION_NONE);
+        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();
+        g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, nullptr);
+        column = gtk_tree_view_column_new_with_attributes
+            (_("Memo"), renderer, "text", SPLIT_COL_MEMO, nullptr);
+        gtk_tree_view_column_set_expand (column, true);
+        gtk_tree_view_append_column(view, column);
+
+        renderer = gtk_cell_renderer_text_new();
+        gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
+        gtk_cell_renderer_set_padding (renderer, 5, 0);
+        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();
+        gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
+        gtk_cell_renderer_set_padding (renderer, 5, 0);
+        column = gtk_tree_view_column_new_with_attributes
+            (_("Credit"), renderer, "text", SPLIT_COL_CREDIT, nullptr);
+        gtk_tree_view_append_column(view, column);
+
+        renderer = gtk_cell_renderer_text_new();
+        gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
+        gtk_cell_renderer_set_padding (renderer, 5, 0);
+        column = gtk_tree_view_column_new_with_attributes
+            (_("Units"), renderer,
+             "text", SPLIT_COL_UNITS,
+             "foreground", SPLIT_COL_UNITS_COLOR,
+             nullptr);
+        gtk_tree_view_append_column(view, column);
+
+        return GTK_WIDGET (view);
     }
+};
 
-    auto pinfo = gnc_price_print_info (info->currency, true);
-    gtk_label_set_text (GTK_LABEL (info->price_value), xaccPrintAmount (*price, pinfo));
-}
+static void connect_signals (gpointer, GtkBuilder*);
 
-static void
-refresh_page_cash (GtkWidget *widget, gpointer user_data)
+struct StockAssistantController
 {
-    return;
-}
+    std::unique_ptr<StockAssistantModel> model;
+    std::unique_ptr<StockAssistantView> view;
+    StockAssistantController (GtkWidget *parent, Account* acct)
+        : model (std::make_unique<StockAssistantModel>(acct))
+    {
+        auto builder = gtk_builder_new();
+        gnc_builder_add_from_file (builder, "assistant-stock-transaction.glade",
+                                   "stock_transaction_assistant");
+        this->view = std::make_unique<StockAssistantView>
+            (builder, xaccAccountGetCommodity (acct), this->model->currency, parent);
+        connect_signals (this, builder);
+        g_object_unref (builder);
+        DEBUG ("StockAssistantController constructor\n");
+    };
+    ~StockAssistantController (){ DEBUG ("StockAssistantController destructor\n"); };
+};
 
+/******* implementations ***********************************************/
 static void
-refresh_page_fees (GtkWidget *widget, gpointer user_data)
+stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data)
 {
-    auto info = static_cast<StockTransactionInfo*>(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);
+    auto info = static_cast<StockAssistantController*>(user_data);
+    gnc_unregister_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info);
+    delete info;
 }
 
 static void
-refresh_page_dividend (GtkWidget *widget, gpointer user_data)
+controller_transaction_type (GtkWidget *widget, StockAssistantController* info)
 {
-    return;
-}
+    if (!info->model->txn_types)
+        return;
 
-static void
-refresh_page_capgains (GtkWidget *widget, gpointer user_data)
-{
-    return;
+    auto type_idx = info->view->get_transaction_type_index();
+    if (type_idx < 0)           // combo isn't initialized yet.
+        return;
+
+    if (!info->model->set_txn_type (type_idx))
+        return;
+
+    info->view->set_txn_type_explanation (info->model->txn_type->explanation);
+    info->view->set_capitalize_fees (info->model->fees_capitalize);
 }
 
-static void
-add_error (StringVec& errors, const char* format_str, const char* arg)
+static void controller_gde (GtkWidget *widget, time64* date)
 {
-    gchar *buf = g_strdup_printf (_(format_str),
-                                  g_strcmp0("Cash", arg) ?
-                                  _(arg) :
-                                  g_dpgettext2 (nullptr, "Stock Assistant", arg));
-    errors.emplace_back (buf);
-    g_free (buf);
+    *date = gnc_date_edit_get_date_end (GNC_DATE_EDIT (widget));
 }
 
-static void
-add_error_str (StringVec& errors, const char* str)
+static void controller_gtk_entry (GtkWidget *widget, const gchar **model_text)
 {
-    errors.emplace_back (_(str));
+    *model_text = gtk_entry_get_text (GTK_ENTRY (widget));
 }
 
-struct SummaryLineInfo
+static void controller_gae (GtkWidget *widget, gnc_numeric *num)
 {
-    bool debit_side;
-    bool value_is_zero;
-    std::string account;
-    std::string memo;
-    std::string value;
-    std::string units;
-    bool units_in_red;
-};
+    gnc_numeric amt;
+    if (!gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT(widget), &amt, true, nullptr))
+        *num = amt;
+    else
+        num->denom = 0;
+}
 
-static void
-add_to_summary_table (GtkListStore *list, SummaryLineInfo line)
+static void controller_gas (GtkWidget *widget, Account **acct)
 {
-    GtkTreeIter iter;
-    auto tooltip = g_markup_escape_text (line.memo.c_str(), -1);
-    gtk_list_store_append (list, &iter);
-    gtk_list_store_set (list, &iter,
-                        SPLIT_COL_ACCOUNT, line.account.c_str(),
-                        SPLIT_COL_MEMO, line.memo.c_str(),
-                        SPLIT_COL_TOOLTIP, tooltip,
-                        SPLIT_COL_DEBIT, line.debit_side ? line.value.c_str() : "",
-                        SPLIT_COL_CREDIT, !line.debit_side ? line.value.c_str() : "",
-                        SPLIT_COL_UNITS, line.units.c_str(),
-                        SPLIT_COL_UNITS_COLOR, line.units_in_red ? "red" : nullptr,
-                        -1);
-    g_free (tooltip);
+    *acct = gnc_account_sel_get_account (GNC_ACCOUNT_SEL (widget));
 }
 
-
 static void
-check_page (SummaryLineInfo& line, gnc_numeric& debit, gnc_numeric& credit,
-            FieldMask splitfield, Account *acct, GtkWidget *memo, GtkWidget *gae,
-            gnc_commodity *comm, const char* page, StringVec& errors)
+controller_stock_amount (GtkWidget *widget, StockAssistantController* info)
 {
-    // Translators: (missing) denotes that the amount or account is
-    // not provided, or incorrect, in the Stock Transaction Assistant.
-    const char* missing_str = N_("(missing)");
-    gnc_numeric amount;
-
-    line.memo = gtk_entry_get_text (GTK_ENTRY (memo));
-    line.units = "";
-    line.units_in_red = false;
-    line.debit_side = (splitfield & FieldMask::ENABLED_DEBIT);
-
-    if (gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT (gae), &amount, true, nullptr))
-    {
-        line.value_is_zero = false;
-        if (splitfield & FieldMask::ALLOW_ZERO)
-            line.value = "";
-        else
-        {
-            add_error (errors, N_("Amount for %s is missing."), page);
-            line.value = _(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);
-            line.debit_side = !line.debit_side;
-        }
-        if (line.debit_side)
-            debit = gnc_numeric_add_fixed (debit, amount);
-        else
-            credit = gnc_numeric_add_fixed (credit, amount);
-        line.value = xaccPrintAmount (amount, gnc_commodity_print_info (comm, true));
-        line.value_is_zero = gnc_numeric_zero_p (amount);
-    }
+    g_return_if_fail (info && info->model->txn_type);
 
-    if (acct)
-        line.account = xaccAccountGetName (acct);
-    else if ((splitfield & FieldMask::ALLOW_ZERO) && gnc_numeric_zero_p (amount))
-        line.account = "";
-    else
-    {
-        add_error (errors, N_("Account for %s is missing."), page);
-        line.account = _(missing_str);
-    }
+    controller_gae (widget, &info->model->stock_amount);
+    info->view->set_stock_amount (info->model->get_new_amount_str());
 }
 
-static inline Account*
-gas_account (GtkWidget *gas)
+static void
+controller_stock_value (GtkWidget *widget, StockAssistantController* info)
 {
-    return gnc_account_sel_get_account (GNC_ACCOUNT_SEL (gas));
+    g_return_if_fail (info && info->model->txn_type);
+
+    controller_gae (widget, &info->model->stock_value);
+    auto [has_price, price, price_str] = info->model->calculate_price ();
+    // Translators: StockAssistant: N/A denotes stock price is not computable
+    info->view->set_price_value (has_price ? price_str : _("N/A"));
 }
 
 static void
-refresh_page_finish (StockTransactionInfo *info)
+controller_capitalize_fees (GtkWidget *widget, StockAssistantController* info)
 {
-    g_return_if_fail (info->txn_type);
-    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, warnings, infos;
-    SummaryLineInfo line;
-    bool negative_in_red = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL,
-                                               GNC_PREF_NEGATIVE_IN_RED);
-
-    // check the stock transaction date. If there are existing stock
-    // transactions dated after the date specified, it is very likely
-    // the later stock transactions will be invalidated. warn the user
-    // to review them.
-    auto new_date = gnc_date_edit_get_date_end (GNC_DATE_EDIT (info->date_edit));
-    auto last_split_node = g_list_last (xaccAccountGetSplitList (info->acct));
-    if (last_split_node)
-    {
-        auto last_split = static_cast<const Split*> (last_split_node->data);
-        auto last_split_date = xaccTransGetDate (xaccSplitGetParent (last_split));
-        if (new_date <= last_split_date)
-        {
-            auto last_split_date_str = qof_print_date (last_split_date);
-            auto new_date_str = qof_print_date (new_date);
-            // Translators: the first %s is the new transaction date;
-            // the second %s is the current stock account's latest
-            // transaction date.
-            auto warn_txt =  g_strdup_printf (_("You will enter a transaction \
-with date %s which is earlier than the latest transaction in this account, \
-dated %s. Doing so may affect the cost basis, and therefore capital gains, \
-of transactions dated after the new entry. Please review all transactions \
-to ensure proper recording."), new_date_str, last_split_date_str);
-            warnings.push_back (warn_txt);
-            g_free (warn_txt);
-            g_free (new_date_str);
-            g_free (last_split_date_str);
-        }
-    }
-
-    if (info->txn_type->stock_value == FieldMask::DISABLED)
-        line = { false, false, xaccAccountGetName (info->acct), "", "", "", false };
-    else
-        check_page (line, debit, credit, info->txn_type->stock_value, info->acct,
-                    info->stock_memo_edit, info->stock_value_edit, info->currency,
-        // Translators: Designates the page in the Stock Assistant for entering
-        // the currency value of a non-currency asset.
-                    N_ ("Stock Value"), errors);
-
-
-    if (info->txn_type->stock_amount == FieldMask::DISABLED)
-        ;
-    else if (info->txn_type->input_new_balance)
-    {
-        auto stock_amount = gnc_amount_edit_get_amount
-            (GNC_AMOUNT_EDIT(info->stock_amount_edit));
-        auto credit_side = (info->txn_type->stock_amount & FieldMask::ENABLED_CREDIT);
-        auto delta = gnc_numeric_sub_fixed (stock_amount, info->balance_at_date);
-        auto ratio = gnc_numeric_div (stock_amount, info->balance_at_date,
-                                      GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
-        auto stock_pinfo = gnc_commodity_print_info
-            (xaccAccountGetCommodity (info->acct), true);
-        stock_amount = gnc_numeric_sub_fixed (stock_amount, info->balance_at_date);
-        line.units = xaccPrintAmount (stock_amount, stock_pinfo);
-        line.units_in_red = negative_in_red && gnc_numeric_negative_p (stock_amount);
-        if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
-            add_error_str (errors, N_("Invalid stock new balance."));
-        else if (gnc_numeric_negative_p (delta) && !credit_side)
-            add_error_str (errors, N_("New balance must be higher than old balance."));
-        else if (gnc_numeric_positive_p (delta) && credit_side)
-            add_error_str (errors, N_("New balance must be lower than old balance."));
-    }
-    else
-    {
-        auto stock_amount = gnc_amount_edit_get_amount
-            (GNC_AMOUNT_EDIT(info->stock_amount_edit));
-        auto stock_pinfo = gnc_commodity_print_info
-            (xaccAccountGetCommodity (info->acct), true);
-        if (!gnc_numeric_positive_p (stock_amount))
-            add_error_str (errors, N_("Stock amount must be positive."));
-        if (info->txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
-            stock_amount = gnc_numeric_neg (stock_amount);
-        line.units = xaccPrintAmount (stock_amount, stock_pinfo);
-        line.units_in_red = negative_in_red && gnc_numeric_negative_p (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."));
-    }
-
-    add_to_summary_table (list, line);
-
-    auto price = calculate_price (info);
-    if (price.has_value())
-    {
-        auto curr_pinfo = gnc_price_print_info (info->currency, true);
-        // Translators: %s refer to: stock mnemonic, broker currency,
-        // date of transaction.
-        auto tmpl = N_("A price of 1 %s = %s on %s will be recorded.");
-        auto date_str = qof_print_date (new_date);
-        auto price_str = g_strdup_printf
-            (_(tmpl),
-             gnc_commodity_get_mnemonic (xaccAccountGetCommodity (info->acct)),
-             xaccPrintAmount (*price, curr_pinfo), date_str);
-        infos.emplace_back (price_str);
-        g_free (price_str);
-        g_free (date_str);
-    }
-
-    if (info->txn_type->cash_value != FieldMask::DISABLED)
-    {
-        check_page (line, debit, credit, info->txn_type->cash_value,
-                    gas_account (info->cash_account), info->cash_memo_edit,
-                    info->cash_value, info->currency,
-// Translators: Designates a page in the stock assistant or inserts the value
-// into the non-currency asset split of an investment transaction.
-                    NC_ ("Stock Assistant", "Cash"), errors);
-        add_to_summary_table (list, line);
-    }
-
-    if (info->txn_type->fees_value != FieldMask::DISABLED)
-    {
-        auto capitalize_fees = gtk_toggle_button_get_active
-            (GTK_TOGGLE_BUTTON (info->capitalize_fees_checkbox));
-        check_page (line, debit, credit, info->txn_type->fees_value,
-                    capitalize_fees ? info->acct : gas_account (info->fees_account),
-                    info->fees_memo_edit, info->fees_value, info->currency,
-// Translators: Designates a page in the stock assistant or inserts the value
-// into the fees split of an investment transaction.
-                    N_ ("Fees"), errors);
-        if (!line.value_is_zero)
-            add_to_summary_table (list, line);
-    }
-
-    if (info->txn_type->dividend_value != FieldMask::DISABLED)
-    {
-        check_page (line, debit, credit, info->txn_type->dividend_value,
-                    gas_account (info->dividend_account),
-                    info->dividend_memo_edit, info->dividend_value, info->currency,
-// Translators: Designates a page in the stock assistant or inserts the value
-// into the income split of an investment dividend transaction.
-                    N_ ("Dividend"), errors);
-        add_to_summary_table (list, line);
-    }
-
-    // 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 (line, debit, credit, info->txn_type->capgains_value,
-                    gas_account (info->capgains_account),
-                    info->capgains_memo_edit, info->capgains_value, info->currency,
-// Translators: Designates a page in the stock assistant or inserts the value
-// into the capital gain/loss income split of an investment transaction.
-                    N_ ("Capital Gain"), errors);
-        add_to_summary_table (list, line);
-
-        check_page (line, debit, credit,
-                    info->txn_type->capgains_value ^ (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT),
-                    info->acct, info->capgains_memo_edit, info->capgains_value,
-                    info->currency,
-                    N_ ("Capital Gain"), errors);
-        add_to_summary_table (list, line);
-    }
-
-    if (!gnc_numeric_equal (debit, credit))
-    {
-        auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
-        auto print_info = gnc_commodity_print_info (info->currency, true);
-        auto debit_str = g_strdup (xaccPrintAmount (debit, print_info));
-        auto credit_str = g_strdup (xaccPrintAmount (credit, print_info));
-        auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
-        errors.emplace_back (error_str);
-        g_free (error_str);
-        g_free (credit_str);
-        g_free (debit_str);
-    }
-
-    // generate final summary message. Collates a header, the errors
-    // and warnings. Then allow completion if errors is empty.
-    auto add_bullet_item = [](std::string& a, std::string& b)->std::string { return std::move(a) + "\n• " + b; };
-    auto summary = std::string{};
-    if (errors.empty())
-    {
-        summary = _("No errors found. Click Apply to create transaction.");
-        summary = std::accumulate (infos.begin(), infos.end(), std::move (summary), add_bullet_item);
-    }
-    else
-    {
-        summary = _("The following errors must be fixed:");
-        summary = std::accumulate (errors.begin(), errors.end(), std::move (summary), add_bullet_item);
-    }
-
-    if (!warnings.empty())
-    {
-        summary += "\n\n";
-        summary += _("The following warnings exist:");
-        summary = std::accumulate (warnings.begin(), warnings.end(), std::move (summary), add_bullet_item);
-    }
-    gtk_label_set_text (GTK_LABEL (info->finish_summary), summary.c_str());
-    gtk_assistant_set_page_complete (GTK_ASSISTANT (info->window),
-                                     info->finish_page, errors.empty());
+    g_return_if_fail (info && info->model->txn_type);
+    info->model->fees_capitalize = info->view->get_capitalize_fees ();
+    info->view->update_fees_acct_sensitive (!info->model->fees_capitalize);
 }
 
 void
 stock_assistant_prepare (GtkAssistant  *assistant, GtkWidget *page,
                          gpointer user_data)
 {
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-    gint currentpage = gtk_assistant_get_current_page(assistant);
+    auto info = static_cast<StockAssistantController*>(user_data);
+    g_return_if_fail (info && info->model);
+    auto model = info->model.get();
+    auto view = info->view.get();
+
+    auto currentpage = gtk_assistant_get_current_page(assistant);
 
     switch (currentpage)
     {
     case PAGE_TRANSACTION_TYPE:
-        // initialize transaction types.
-        gnc_numeric balance;
-        time64 date;
-        date = gnc_date_edit_get_date_end (GNC_DATE_EDIT (info->date_edit));
-        if (info->txn_types_date && (info->txn_types_date == date))
+        if (!model->maybe_reset_txn_types())
             break;
-        info->txn_types_date = date;
-        balance = xaccAccountGetBalanceAsOfDate (info->acct, date);
-        info->txn_types = gnc_numeric_zero_p (balance) ? starting_types
-            : gnc_numeric_positive_p (balance) ? long_types
-            : short_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),
-                                            _(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);
+        view->set_transaction_types (model->txn_types.value());
+        controller_transaction_type (view->transaction_type_combo, info);
+        view->set_focus (view->transaction_type_combo);
+        break;
+    case PAGE_TRANSACTION_DETAILS:
+        controller_gde (view->transaction_date, &model->transaction_date);
+        controller_gtk_entry (view->transaction_description, &model->transaction_description);
+        view->set_focus (view->transaction_description);
         break;
     case PAGE_STOCK_AMOUNT:
-        info->balance_at_date = xaccAccountGetBalanceAsOfDate
-            (info->acct, gnc_date_edit_get_date_end (GNC_DATE_EDIT (info->date_edit)));
-        gtk_label_set_text_with_mnemonic
-            (GTK_LABEL (info->stock_amount_label),
-             info->txn_type->input_new_balance ? _("Ne_w Balance") : _("_Shares"));
-        gtk_label_set_text
-            (GTK_LABEL (info->next_amount_label),
-             info->txn_type->input_new_balance ? _("Ratio") : _("Next Balance"));
-        gtk_label_set_text
-            (GTK_LABEL (info->stock_amount_title),
-             info->txn_type->input_new_balance ?
-             _("Enter the new balance of shares after the stock split.") :
-             _("Enter the number of shares you gained or lost in the transaction."));
-        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)));
+        view->prepare_stock_amount_page (model->input_new_balance,
+                                         model->get_stock_balance_str());
+        controller_stock_amount (view->stock_amount_edit, info);
+        view->set_focus_gae (view->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)));
+        controller_gtk_entry (view->stock_memo_edit, &model->stock_memo);
+        controller_stock_value (view->stock_value_edit, info);
+        view->set_focus_gae (view->stock_value_edit);
         break;
     case PAGE_CASH:
-        refresh_page_cash (info->cash_value, info);
+        controller_gtk_entry (view->cash_memo_edit, &model->cash_memo);
+        controller_gae (view->cash_value, &model->cash_value);
+        controller_gas (view->cash_account, &model->cash_account);
+        view->set_focus_gae (view->cash_value);
         break;
     case PAGE_FEES:
-        refresh_page_fees (info->fees_value, info);
+        controller_capitalize_fees (view->capitalize_fees_checkbox, info);
+        controller_gtk_entry (view->fees_memo_edit, &model->fees_memo);
+        controller_gae (view->fees_value, &model->fees_value);
+        controller_gas (view->fees_account, &model->fees_account);
+        view->set_focus_gae (view->fees_value);
         break;
     case PAGE_DIVIDEND:
-        refresh_page_dividend (info->fees_value, info);
+        controller_gtk_entry (view->dividend_memo_edit, &model->dividend_memo);
+        controller_gae (view->dividend_value, &model->dividend_value);
+        controller_gas (view->dividend_account, &model->dividend_account);
+        view->set_focus_gae (view->dividend_value);
         break;
     case PAGE_CAPGAINS:
-        refresh_page_capgains (info->capgains_value, info);
+        controller_gtk_entry (view->capgains_memo_edit, &model->capgains_memo);
+        controller_gae (view->capgains_value, &model->capgains_value);
+        controller_gas (view->capgains_account, &model->capgains_account);
+        view->set_focus_gae (view->capgains_value);
         break;
     case PAGE_FINISH:
-        refresh_page_finish (info);
+    {
+        auto [success, summary, list_of_splits] = model->generate_list_of_splits ();
+        view->prepare_finish_page (success, summary, list_of_splits);
+        break;
+    }
+    default:
         break;
     }
 }
 
 static gint
-forward_page_func (gint current_page, gpointer user_data)
+forward_page_func (gint current_page, StockAssistantController* info)
 {
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-    auto& txn_type = info->txn_type;
+    auto model = info->model.get();
 
     current_page++;
-    if (!txn_type)
+    if (!model->txn_type)
         return current_page;
 
-    if (txn_type->stock_amount == FieldMask::DISABLED && current_page == PAGE_STOCK_AMOUNT)
+    if (!model->stock_amount_enabled && current_page == PAGE_STOCK_AMOUNT)
         current_page++;
-    if (txn_type->stock_value == FieldMask::DISABLED && current_page == PAGE_STOCK_VALUE)
+    if (!model->stock_value_enabled && current_page == PAGE_STOCK_VALUE)
         current_page++;
-    if (txn_type->cash_value == FieldMask::DISABLED && current_page == PAGE_CASH)
+    if (!model->cash_enabled && current_page == PAGE_CASH)
         current_page++;
-    if (txn_type->fees_value == FieldMask::DISABLED && current_page == PAGE_FEES)
+    if (!model->fees_enabled && current_page == PAGE_FEES)
         current_page++;
-    if (txn_type->dividend_value == FieldMask::DISABLED && current_page == PAGE_DIVIDEND)
+    if (!model->dividend_enabled && current_page == PAGE_DIVIDEND)
         current_page++;
-    if (txn_type->capgains_value == FieldMask::DISABLED && current_page == PAGE_CAPGAINS)
+    if (!model->capgains_enabled && current_page == PAGE_CAPGAINS)
         current_page++;
 
     return current_page;
 }
 
-static void
-create_split (Transaction *trans, const gchar *action, Account *account,
-              AccountVec& account_commits, GtkWidget *memo_entry,
-              gnc_numeric amount_numeric, gnc_numeric 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)));
-    auto action_str{ g_strcmp0(action, "Cash") ?
-        _(action) :
-        g_dpgettext2 (nullptr, "Stock Assistant: Action field", action)};
-    gnc_set_num_action (nullptr, split, nullptr, action_str);
-}
-
-static void
-add_price (StockTransactionInfo* info, time64 date)
-{
-    auto p = calculate_price (info);
-
-    if (!p.has_value())
-        return;
-
-    auto price = gnc_price_create (gnc_get_current_book ());
-
-    gnc_price_begin_edit (price);
-    gnc_price_set_commodity (price, xaccAccountGetCommodity (info->acct));
-    gnc_price_set_currency (price, info->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);
-}
-
-static gnc_numeric gae_amount (GtkWidget *widget)
-{
-    return gnc_amount_edit_get_amount (GNC_AMOUNT_EDIT (widget));
-}
-
 void
 stock_assistant_finish (GtkAssistant *assistant, gpointer user_data)
 {
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-    AccountVec account_commits;
-    auto book = gnc_get_current_book ();
-    g_return_if_fail (info->txn_type);
+    auto info = static_cast<StockAssistantController*>(user_data);
+    g_return_if_fail (info->model->txn_type);
 
     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);
-
-    auto stock_amount = info->txn_type->stock_amount != FieldMask::DISABLED ?
-        gae_amount (info->stock_amount_edit) : gnc_numeric_zero ();
-    auto stock_value = info->txn_type->stock_value != FieldMask::DISABLED ?
-        gae_amount (info->stock_value_edit) : gnc_numeric_zero ();
-    if (info->txn_type->input_new_balance)
-        stock_amount = gnc_numeric_sub_fixed (stock_amount, info->balance_at_date);
-    else
-    {
-        if (info->txn_type->stock_amount & FieldMask::ENABLED_CREDIT)
-            stock_amount = gnc_numeric_neg (stock_amount);
-        if (info->txn_type->stock_value & FieldMask::ENABLED_CREDIT)
-            stock_value = gnc_numeric_neg (stock_value);
-    }
-
-// Translators: Inserts the value into action field of the non-currency asset split of
-// an investment transaction.
-    create_split (trans, N_ ("Stock"),
-                  info->acct, account_commits, info->stock_memo_edit,
-                  stock_amount, stock_value);
-
-    if (info->txn_type->cash_value != FieldMask::DISABLED)
-    {
-        auto cash = gae_amount (info->cash_value);
-        if (info->txn_type->cash_value & FieldMask::ENABLED_CREDIT)
-            cash = gnc_numeric_neg (cash);
-
-        create_split (trans, NC_ ("Stock Assistant:", "Cash"),
-                      gas_account (info->cash_account), account_commits,
-                      info->cash_memo_edit, cash, cash);
-    }
-
-    if (info->txn_type->fees_value != FieldMask::DISABLED)
-    {
-        auto fees = gae_amount (info->fees_value);
-        if (!gnc_numeric_zero_p (fees))
-        {
-            auto capitalize = gtk_toggle_button_get_active
-                (GTK_TOGGLE_BUTTON (info->capitalize_fees_checkbox));
-
-            create_split (trans, N_ ("Fees"),
-                          capitalize ? info->acct : gas_account (info->fees_account),
-                          account_commits, info->fees_memo_edit,
-                          capitalize ? gnc_numeric_zero () : fees, fees);
-        }
-    }
-
-    if (info->txn_type->dividend_value != FieldMask::DISABLED)
-    {
-        auto dividend = gae_amount (info->dividend_value);
-        if (info->txn_type->dividend_value & FieldMask::ENABLED_CREDIT)
-            dividend = gnc_numeric_neg (dividend);
-
-        create_split (trans, N_ ("Dividend"),
-                      gas_account (info->dividend_account), account_commits,
-                      info->dividend_memo_edit, dividend, dividend);
-    }
-
-    if (info->txn_type->capgains_value != FieldMask::DISABLED)
-    {
-        auto capgains = gae_amount (info->capgains_value);
-        create_split (trans, N_ ("Capital Gain"),
-                      info->acct, account_commits, info->capgains_memo_edit,
-                      gnc_numeric_zero (), capgains);
-
-        capgains = gnc_numeric_neg (capgains);
-        create_split (trans, N_ ("Capital Gain"),
-                      gas_account (info->capgains_account), account_commits,
-                      info->capgains_memo_edit, capgains, capgains);
-    }
-
-    add_price (info, 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);
@@ -1183,220 +1426,19 @@ stock_assistant_finish (GtkAssistant *assistant, gpointer user_data)
 void
 stock_assistant_cancel (GtkAssistant *assistant, gpointer user_data)
 {
-    auto info = static_cast<StockTransactionInfo*>(user_data);
+    auto info = static_cast<StockAssistantController*>(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<GNCAccountType> 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,
-                                     G_TYPE_STRING, G_TYPE_STRING);
-    gtk_tree_view_set_model(view, GTK_TREE_MODEL(store));
-    gtk_tree_selection_set_mode (gtk_tree_view_get_selection (view),
-                                 GTK_SELECTION_NONE);
-    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();
-    g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, nullptr);
-    column = gtk_tree_view_column_new_with_attributes
-        (_("Memo"), renderer, "text", SPLIT_COL_MEMO, nullptr);
-    gtk_tree_view_column_set_expand (column, true);
-    gtk_tree_view_append_column(view, column);
-
-    renderer = gtk_cell_renderer_text_new();
-    gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
-    gtk_cell_renderer_set_padding (renderer, 5, 0);
-    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();
-    gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
-    gtk_cell_renderer_set_padding (renderer, 5, 0);
-    column = gtk_tree_view_column_new_with_attributes
-        (_("Credit"), renderer, "text", SPLIT_COL_CREDIT, nullptr);
-    gtk_tree_view_append_column(view, column);
-
-    renderer = gtk_cell_renderer_text_new();
-    gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
-    gtk_cell_renderer_set_padding (renderer, 5, 0);
-    column = gtk_tree_view_column_new_with_attributes
-        (_("Units"), renderer,
-         "text", SPLIT_COL_UNITS,
-         "foreground", SPLIT_COL_UNITS_COLOR,
-         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");
-
-    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->stock_amount_title = get_widget (builder, "stock_amount_title");
-    info->prev_amount = get_widget (builder, "prev_balance_amount");
-    info->stock_amount_label = get_widget (builder, "stock_amount_label");
-    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");
-    info->next_amount_label = get_widget (builder, "next_balance_label");
-    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_tree_view_set_tooltip_column (GTK_TREE_VIEW (info->finish_split_view),
-                                      SPLIT_COL_TOOLTIP);
-
-    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<StockTransactionInfo*>(user_data);
+    auto info = static_cast<StockAssistantController*>(user_data);
 
-    if (!GNC_IS_ACCOUNT (info->acct))
+    if (!GNC_IS_ACCOUNT (info->model->acct))
     {
-        PWARN ("account %p does not exist anymore. abort", info->acct);
+        PWARN ("account %p does not exist anymore. abort", info->model->acct);
         gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, info);
     }
 }
@@ -1404,8 +1446,51 @@ refresh_handler (GHashTable *changes, gpointer user_data)
 static void
 close_handler (gpointer user_data)
 {
-    auto info = static_cast<StockTransactionInfo*>(user_data);
-    gtk_widget_destroy (info->window);
+    auto info = static_cast<StockAssistantController*>(user_data);
+    gtk_widget_destroy (info->view->window);
+}
+
+static void connect_signals (gpointer data, GtkBuilder *builder)
+{
+    auto info = static_cast<StockAssistantController*>(data);
+    auto model = info->model.get();
+    auto view = info->view.get();
+
+    struct SignalData { GtkWidget* widget; const char* signal; GCallback callback; gpointer data; };
+    std::vector<SignalData> signals =
+    {
+        { view->transaction_type_combo  , "changed"            , G_CALLBACK (controller_transaction_type)      , info },
+        { view->transaction_date        , "date_changed"       , G_CALLBACK (controller_gde)                   , &model->transaction_date },
+        { view->transaction_description , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->transaction_description },
+        { view->stock_amount_edit       , "changed"            , G_CALLBACK (controller_stock_amount)          , info },
+        { view->stock_value_edit        , "changed"            , G_CALLBACK (controller_stock_value)           , info },
+        { view->stock_memo_edit         , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->stock_memo },
+        { view->cash_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->cash_account },
+        { view->cash_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->cash_memo },
+        { view->cash_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->cash_value },
+        { view->capitalize_fees_checkbox, "toggled"            , G_CALLBACK (controller_capitalize_fees)       , info },
+        { view->fees_account            , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->fees_account },
+        { view->fees_memo_edit          , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->fees_memo },
+        { view->fees_value              , "changed"            , G_CALLBACK (controller_gae)                   , &model->fees_value },
+        { view->dividend_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->dividend_account },
+        { view->dividend_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->dividend_memo },
+        { view->dividend_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->dividend_value },
+        { view->capgains_account        , "account_sel_changed", G_CALLBACK (controller_gas)                   , &model->capgains_account },
+        { view->capgains_memo_edit      , "changed"            , G_CALLBACK (controller_gtk_entry)             , &model->capgains_memo },
+        { view->capgains_value          , "changed"            , G_CALLBACK (controller_gae)                   , &model->capgains_value },
+        { view->window                  , "destroy"            , G_CALLBACK (stock_assistant_window_destroy_cb), info }
+    };
+    for (const auto& [widget, signal, callback, data] : signals)
+        g_signal_connect (widget, signal, callback, data);
+    gtk_assistant_set_forward_page_func (GTK_ASSISTANT(view->window),
+                                         (GtkAssistantPageFunc)forward_page_func,
+                                         info, nullptr);
+    gtk_builder_connect_signals (builder, 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);
 }
 
 /********************************************************************\
@@ -1419,15 +1504,5 @@ close_handler (gpointer user_data)
 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));
+    [[maybe_unused]]auto info = new StockAssistantController (parent, account);
 }
diff --git a/gnucash/gschemas/org.gnucash.GnuCash.dialogs.gschema.xml.in b/gnucash/gschemas/org.gnucash.GnuCash.dialogs.gschema.xml.in
index 8442f4f437..8554d2d3a2 100644
--- a/gnucash/gschemas/org.gnucash.GnuCash.dialogs.gschema.xml.in
+++ b/gnucash/gschemas/org.gnucash.GnuCash.dialogs.gschema.xml.in
@@ -15,6 +15,7 @@
     <child name="open-save" schema="org.gnucash.GnuCash.dialogs.open-save"/>
     <child name="report" schema="org.gnucash.GnuCash.dialogs.report"/>
     <child name="report-saved-configs" schema="org.gnucash.GnuCash.dialogs.report-saved-configs"/>
+    <child name="stock-assistant" schema="org.gnucash.GnuCash.dialogs.stock-assistant"/>
     <child name="lot-viewer" schema="org.gnucash.GnuCash.dialogs.lot-viewer"/>
     <child name="new-user" schema="org.gnucash.GnuCash.dialogs.new-user"/>
     <child name="new-hierarchy" schema="org.gnucash.GnuCash.dialogs.new-hierarchy"/>
@@ -178,6 +179,16 @@
     </key>
   </schema>
 
+  <schema id="org.gnucash.GnuCash.dialogs.stock-assistant" path="/org/gnucash/GnuCash/dialogs/stock-assistant/">
+    <key name="last-geometry" type="(iiii)">
+      <default>(-1,-1,-1,-1)</default>
+      <summary>Last window position and size</summary>
+      <description>This setting describes the size and position of the window when it was last closed.
+        The numbers are the X and Y coordinates of the top left corner of the window
+        followed by the width and height of the window.</description>
+    </key>
+  </schema>
+
   <schema id="org.gnucash.GnuCash.dialogs.lot-viewer" path="/org/gnucash/GnuCash/dialogs/lot-viewer/">
     <key name="hpane-position" type="i">
       <default>200</default>

commit f7bfd0bcf8dea7f8e0510cde98acf3a266404efc
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Sun May 28 14:07:23 2023 +0100

    Add a don't complete entry to the top of the list

diff --git a/gnucash/register/register-gnome/completioncell-gnome.c b/gnucash/register/register-gnome/completioncell-gnome.c
index 57d63cc2cf..3fdf827f06 100644
--- a/gnucash/register/register-gnome/completioncell-gnome.c
+++ b/gnucash/register/register-gnome/completioncell-gnome.c
@@ -71,6 +71,8 @@ typedef struct _PopBox
 
 } PopBox;
 
+#define DONT_TEXT N_("Don't autocomplete")
+
 /** Enumeration for the list-store */
 enum GncCompletionColumn
 {
@@ -599,6 +601,11 @@ populate_list_store (CompletionCell* cell, const gchar* str)
 
     gtk_list_store_clear (box->item_store);
 
+    // add the don't first entry
+    gchar *markup = g_markup_printf_escaped ("<i>%s</i>", DONT_TEXT);
+    list_store_append (box->item_store, DONT_TEXT, markup, 0);
+    g_free (markup);
+
     // add to the list store
     g_hash_table_foreach (box->item_hash, add_item, box);
 
@@ -642,6 +649,8 @@ gnc_completion_cell_modify_verify (BasicCell* bcell,
 
     if (box->in_list_select)
     {
+        if (g_strcmp0 (newval, DONT_TEXT) == 0)
+            return;
         gnc_basic_cell_set_value_internal (bcell, newval);
         *cursor_position = -1;
         *start_selection = 0;

commit 3c81b4c85c08ffec1650e35d61cac26fd15c7f4e
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Sun May 28 14:06:16 2023 +0100

    Use new completion cell for register description field

diff --git a/gnucash/register/ledger-core/split-register-layout.c b/gnucash/register/ledger-core/split-register-layout.c
index 93a6abcd25..bb6c3f76e9 100644
--- a/gnucash/register/ledger-core/split-register-layout.c
+++ b/gnucash/register/ledger-core/split-register-layout.c
@@ -677,7 +677,7 @@ gnc_split_register_layout_add_cells (SplitRegister* reg,
 
     gnc_register_add_cell (layout,
                            DESC_CELL,
-                           COMBO_CELL_TYPE_NAME,
+                           COMPLETION_CELL_TYPE_NAME,
                            C_ ("sample", "Description of a transaction"),
                            CELL_ALIGN_LEFT,
                            TRUE,
diff --git a/gnucash/register/ledger-core/split-register-load.c b/gnucash/register/ledger-core/split-register-load.c
index 8874ddacba..25a51facfe 100644
--- a/gnucash/register/ledger-core/split-register-load.c
+++ b/gnucash/register/ledger-core/split-register-load.c
@@ -32,6 +32,7 @@
 #include "Transaction.h"
 #include "account-quickfill.h"
 #include "combocell.h"
+#include "completioncell.h"
 #include "gnc-component-manager.h"
 #include "qof.h"
 #include "gnc-ui-util.h"
@@ -54,7 +55,6 @@ static QofLogModule log_module = GNC_MOD_LEDGER;
 static void gnc_split_register_load_xfer_cells (SplitRegister* reg,
                                                 Account* base_account);
 
-static void gnc_split_register_load_desc_cells (SplitRegister* reg);
 static void
 gnc_split_register_load_recn_cells (SplitRegister* reg)
 {
@@ -113,6 +113,21 @@ gnc_split_register_load_type_cells (SplitRegister* reg)
     gnc_recn_cell_set_read_only (cell, TRUE);
 }
 
+static void
+gnc_split_register_load_desc_cells (SplitRegister* reg)
+{
+    CompletionCell *cell;
+
+    if (!reg) return;
+
+    cell = (CompletionCell *)
+           gnc_table_layout_get_cell (reg->table->layout, DESC_CELL);
+
+    if (!cell) return;
+
+    gnc_completion_cell_set_sort_enabled (cell, TRUE);
+}
+
 /** Add a transaction to the register.
  *
  *  Virtual cells are set up to hold the data, beginning at @a vcell_loc and
@@ -609,6 +624,13 @@ gnc_split_register_load (SplitRegister* reg, GList* slist,
         added_blank_trans = TRUE;
     }
 
+    gnc_completion_cell_clear_menu (
+        (CompletionCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL));
+
+    gnc_completion_cell_reverse_sort (
+        (CompletionCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL),
+          table->model->reverse_sort);
+
     /* populate the table */
     for (node = slist; node; node = node->next)
     {
@@ -729,9 +751,10 @@ gnc_split_register_load (SplitRegister* reg, GList* slist,
         if (info->first_pass)
             add_quickfill_completions (reg->table->layout, trans, split, has_last_num);
 
-        gnc_combo_cell_add_menu_item_unique (
-            (ComboCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL),
-            xaccTransGetDescription (trans));
+        gnc_completion_cell_add_menu_item (
+            (CompletionCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL),
+             xaccTransGetDescription (trans));
+
 
         if (trans == find_trans)
             new_trans_row = vcell_loc.virt_row;
@@ -936,17 +959,4 @@ gnc_split_register_load_xfer_cells (SplitRegister* reg, Account* base_account)
     gnc_combo_cell_use_list_store_cache (cell, store);
 }
 
-static void
-gnc_split_register_load_desc_cells (SplitRegister* reg)
-{
-    ComboCell* cell;
-    GtkListStore* store = gtk_list_store_new (1, G_TYPE_STRING);
-
-    cell = (ComboCell*)
-           gnc_table_layout_get_cell (reg->table->layout, DESC_CELL);
-
-    gnc_combo_cell_use_type_ahead_only (cell);
-
-    gnc_combo_cell_use_list_store_cache (cell, store);
-}
 /* ====================== END OF FILE ================================== */
diff --git a/gnucash/register/ledger-core/split-register.c b/gnucash/register/ledger-core/split-register.c
index 3d017d4ca4..e26671098d 100644
--- a/gnucash/register/ledger-core/split-register.c
+++ b/gnucash/register/ledger-core/split-register.c
@@ -29,6 +29,7 @@
 #include <glib/gi18n.h>
 
 #include "combocell.h"
+#include "completioncell.h"
 #include "datecell.h"
 #include "dialog-utils.h"
 #include "gnc-component-manager.h"
@@ -2665,10 +2666,10 @@ gnc_split_register_config_cells (SplitRegister* reg)
         ((ComboCell*)
          gnc_table_layout_get_cell (reg->table->layout, ACTN_CELL), TRUE);
 
-    /* the description cell */
-    gnc_combo_cell_set_autosize
-        ((ComboCell*)
-         gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), TRUE);
+     /* the description cell */
+    gnc_completion_cell_set_autosize
+        ((CompletionCell*)
+          gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), TRUE);
 
     /* Use GNC_COMMODITY_MAX_FRACTION for prices and "exchange rates"  */
     gnc_price_cell_set_fraction
@@ -2697,10 +2698,10 @@ gnc_split_register_config_cells (SplitRegister* reg)
         ((ComboCell*)
          gnc_table_layout_get_cell (reg->table->layout, ACTN_CELL), FALSE);
 
-    /* The description cell should accept strings not in the list */
-    gnc_combo_cell_set_strict
-        ((ComboCell*)
-         gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), FALSE);
+     /* The description cell should accept strings not in the list */
+    gnc_completion_cell_set_strict
+        ((CompletionCell*)
+          gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), FALSE);
 
     /* number format for share quantities in stock ledgers */
     switch (reg->type)
diff --git a/gnucash/register/register-core/combocell.h b/gnucash/register/register-core/combocell.h
index ebdb0340a7..ceeb7b7dd0 100644
--- a/gnucash/register/register-core/combocell.h
+++ b/gnucash/register/register-core/combocell.h
@@ -63,10 +63,6 @@ void         gnc_combo_cell_clear_menu (ComboCell* cell);
 void         gnc_combo_cell_add_menu_item (ComboCell* cell,
                                            const char* menustr);
 
-/** Add a unique menu item to the list. */
-void gnc_combo_cell_add_menu_item_unique (ComboCell* cell,
-                                          const char* menustr);
-
 /** Add a 'account name' menu item to the list. When testing for
  *  equality with the currently selected item, this function will
  *  ignore the characters normally used to separate account names. */
@@ -108,9 +104,5 @@ void gnc_combo_cell_use_quickfill_cache (ComboCell* cell,
                                          QuickFill* shared_qf);
 void gnc_combo_cell_use_list_store_cache (ComboCell* cell, gpointer data);
 
-/** Set the combocell to use only type ahead search. This will make the
- *  search to be more like a modified entry completion. */
-void gnc_combo_cell_use_type_ahead_only (ComboCell* cell);
-
 /** @} */
 #endif
diff --git a/gnucash/register/register-gnome/combocell-gnome.c b/gnucash/register/register-gnome/combocell-gnome.c
index 14b1f5d6d4..e55681cd05 100644
--- a/gnucash/register/register-gnome/combocell-gnome.c
+++ b/gnucash/register/register-gnome/combocell-gnome.c
@@ -75,9 +75,6 @@ typedef struct _PopBox
 
     GList* ignore_strings;
 
-    GHashTable *item_hash;
-
-    gboolean use_type_ahead_only;
 } PopBox;
 
 
@@ -168,10 +165,6 @@ gnc_combo_cell_init (ComboCell* cell)
     box->complete_char = '\0';
 
     box->ignore_strings = NULL;
-
-    box->item_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
-
-    box->use_type_ahead_only = FALSE;
 }
 
 static void
@@ -340,9 +333,6 @@ gnc_combo_cell_destroy (BasicCell* bcell)
             box->qf = NULL;
         }
 
-        if (box->item_hash)
-           g_hash_table_destroy (box->item_hash);
-
         g_list_free_full (box->ignore_strings, g_free);
         box->ignore_strings = NULL;
 
@@ -468,70 +458,6 @@ gnc_combo_cell_add_menu_item (ComboCell* cell, const char* menustr)
     }
 }
 
-void
-gnc_combo_cell_add_menu_item_unique (ComboCell* cell, const char* menustr)
-{
-    PopBox* box;
-
-    if (cell == NULL)
-        return;
-    if (menustr == NULL)
-        return;
-
-    box = cell->cell.gui_private;
-
-    if (box->item_list != NULL)
-    {
-        block_list_signals (cell);
-
-        /* check if menustr has already been added. */
-        if (g_hash_table_lookup_extended (box->item_hash, menustr, NULL, NULL))
-            return;
-
-        g_hash_table_insert (box->item_hash, g_strdup (menustr), NULL);
-
-        gchar *menustr_temp = g_strdup (menustr);
-        gnc_utf8_strip_invalid_and_controls (menustr_temp);
-        gnc_item_list_append (box->item_list, menustr_temp);
-        if (cell->cell.value &&
-            (strcmp (menustr_temp, cell->cell.value) == 0))
-            gnc_item_list_select (box->item_list, menustr_temp);
-        g_free (menustr_temp);
-        unblock_list_signals (cell);
-    }
-    else
-    {
-        GtkTreeIter iter;
-
-        // add a blank entry as the first entry in store
-        if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL(cell->shared_store), NULL) == 0)
-        {
-            gtk_list_store_append (cell->shared_store, &iter);
-            gtk_list_store_set (cell->shared_store, &iter, 0, "", -1);
-            g_hash_table_insert (box->item_hash, g_strdup (""), NULL);
-        }
-
-        /* check if menustr has already been added. */
-        if (g_hash_table_lookup_extended (box->item_hash, menustr, NULL, NULL))
-            return;
-
-        g_hash_table_insert (box->item_hash, g_strdup (menustr), NULL);
-
-        gchar *menustr_temp = g_strdup (menustr);
-        gnc_utf8_strip_invalid_and_controls (menustr_temp);
-        gtk_list_store_append (cell->shared_store, &iter);
-        gtk_list_store_set (cell->shared_store, &iter, 0, menustr_temp, -1);
-        g_free (menustr_temp);
-    }
-
-    /* If we're going to be using a pre-fab quickfill,
-     * then don't fill it in here */
-    if (FALSE == box->use_quickfill_cache)
-    {
-        gnc_quickfill_insert (box->qf, menustr, QUICKFILL_ALPHA);
-    }
-}
-
 void
 gnc_combo_cell_add_account_menu_item (ComboCell* cell, char* menustr)
 {
@@ -713,8 +639,7 @@ gnc_combo_cell_modify_verify (BasicCell* _cell,
             return;
         }
 
-        if (!box->use_type_ahead_only) // Do we only want to use type-ahead
-            match_str = quickfill_match (box->qf, newval);
+        match_str = quickfill_match (box->qf, newval);
 
         if (match_str != NULL) // Do we have a quickfill match
         {
@@ -937,19 +862,6 @@ gnc_combo_cell_direct_update (BasicCell* bcell,
     return TRUE;
 }
 
-void
-gnc_combo_cell_use_type_ahead_only (ComboCell* cell)
-{
-    PopBox* box;
-
-    if (cell == NULL) return;
-
-    box = cell->cell.gui_private;
-
-    box->use_type_ahead_only = TRUE;
-
-}
-
 static void
 gnc_combo_cell_gui_realize (BasicCell* bcell, gpointer data)
 {
diff --git a/gnucash/register/register-gnome/gnucash-register.c b/gnucash/register/register-gnome/gnucash-register.c
index 1324581307..9a09ae89f4 100644
--- a/gnucash/register/register-gnome/gnucash-register.c
+++ b/gnucash/register/register-gnome/gnucash-register.c
@@ -47,6 +47,7 @@
 #include "gnc-state.h"
 
 #include "combocell.h"
+#include "completioncell.h"
 #include "datecell.h"
 #include "formulacell-gnome.h"
 #include "pricecell-gnome.h"
@@ -100,6 +101,7 @@ void
 gnucash_register_add_cell_types (void)
 {
     gnc_register_add_cell_type (COMBO_CELL_TYPE_NAME, gnc_combo_cell_new);
+    gnc_register_add_cell_type (COMPLETION_CELL_TYPE_NAME, gnc_completion_cell_new);
     gnc_register_add_cell_type (DATE_CELL_TYPE_NAME, gnc_date_cell_new);
     gnc_register_add_cell_type (PRICE_CELL_TYPE_NAME,
                                 gnc_price_cell_gnome_new);

commit f46a958da21e35c3256e5fdb8e620d92a5f45529
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Sun May 28 14:05:10 2023 +0100

    Modify GncItemList by adding a couple of functions.
    
    It is more efficient to disconnect the list store from the tree store
    and then reconnect it before doing large number additions to the list
    store so add a couple of functions to do this.

diff --git a/gnucash/register/register-gnome/combocell-gnome.c b/gnucash/register/register-gnome/combocell-gnome.c
index fd6c039d0b..14b1f5d6d4 100644
--- a/gnucash/register/register-gnome/combocell-gnome.c
+++ b/gnucash/register/register-gnome/combocell-gnome.c
@@ -367,7 +367,7 @@ gnc_combo_cell_set_sort_enabled (ComboCell* cell, gboolean enabled)
         return;
 
     block_list_signals (cell);
-    gnc_item_list_set_sort_enabled (box->item_list, enabled);
+    gnc_item_list_set_sort_column (box->item_list, 0);
     unblock_list_signals (cell);
 }
 
diff --git a/gnucash/register/register-gnome/gnucash-item-list.c b/gnucash/register/register-gnome/gnucash-item-list.c
index febc0b2189..0b1eee4296 100644
--- a/gnucash/register/register-gnome/gnucash-item-list.c
+++ b/gnucash/register/register-gnome/gnucash-item-list.c
@@ -98,22 +98,14 @@ gnc_item_list_append (GncItemList* item_list, const char* string)
 
 
 void
-gnc_item_list_set_sort_enabled (GncItemList* item_list, gboolean enabled)
+gnc_item_list_set_sort_column (GncItemList* item_list, gint column_id)
 {
-    if (enabled)
-    {
-        gtk_tree_sortable_set_sort_column_id
-        (GTK_TREE_SORTABLE (item_list->list_store),
-         0,
-         GTK_SORT_ASCENDING);
-    }
-    else
-    {
-        gtk_tree_sortable_set_sort_column_id
+    g_return_if_fail (IS_GNC_ITEM_LIST (item_list));
+
+    gtk_tree_sortable_set_sort_column_id
         (GTK_TREE_SORTABLE (item_list->list_store),
-         GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
+         column_id,
          GTK_SORT_ASCENDING);
-    }
 }
 
 
@@ -203,6 +195,7 @@ gnc_item_list_select (GncItemList* item_list, const char* string)
     g_free (to_find_data);
 }
 
+
 char*
 gnc_item_list_get_selection (GncItemList *item_list)
 {
@@ -277,6 +270,29 @@ gnc_item_list_using_temp (GncItemList *item_list)
     return item_list && item_list->temp_store;
 }
 
+GtkListStore *
+gnc_item_list_disconnect_store (GncItemList *item_list)
+{
+    GtkListStore *store;
+
+    g_return_val_if_fail (item_list != NULL, NULL);
+
+    store = GTK_LIST_STORE(gtk_tree_view_get_model (item_list->tree_view));
+
+    gtk_tree_view_set_model (item_list->tree_view, NULL);
+
+    return store;
+}
+
+void
+gnc_item_list_connect_store (GncItemList *item_list, GtkListStore *list_store)
+{
+    g_return_if_fail (item_list != 0);
+
+    gtk_tree_view_set_model (item_list->tree_view,
+                             GTK_TREE_MODEL (list_store));
+}
+
 static void
 gnc_item_list_init (GncItemList* item_list)
 {
@@ -493,9 +509,9 @@ gnc_item_list_new (GtkListStore* list_store)
     g_object_unref (list_store);
 
     gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE);
-    gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (
-            tree_view)),
-                                 GTK_SELECTION_BROWSE);
+    gtk_tree_selection_set_mode (
+        gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)),
+                                     GTK_SELECTION_BROWSE);
     gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (list_store),
                                           0, GTK_SORT_ASCENDING);
 
diff --git a/gnucash/register/register-gnome/gnucash-item-list.h b/gnucash/register/register-gnome/gnucash-item-list.h
index 31ab1eaf3a..4cf903c339 100644
--- a/gnucash/register/register-gnome/gnucash-item-list.h
+++ b/gnucash/register/register-gnome/gnucash-item-list.h
@@ -77,7 +77,7 @@ void gnc_item_list_clear (GncItemList *item_list);
 
 void gnc_item_list_append (GncItemList *item_list, const char *string);
 
-void gnc_item_list_set_sort_enabled(GncItemList *item_list, gboolean enabled);
+void gnc_item_list_set_sort_column (GncItemList *item_list, gint column_id);
 
 gboolean gnc_item_in_list (GncItemList *item_list, const char *string);
 
@@ -98,5 +98,9 @@ void gnc_item_list_set_temp_store (GncItemList *item_list, GtkListStore *store);
 
 gboolean gnc_item_list_using_temp (GncItemList *item_list);
 
+GtkListStore * gnc_item_list_disconnect_store (GncItemList *item_list);
+
+void gnc_item_list_connect_store (GncItemList *item_list, GtkListStore *store);
+
 /** @} */
 #endif /* GNUCASH_ITEM_LIST_H */

commit 7fb03b1be29d1aef8859100f549d12fc0658636d
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue May 2 14:48:00 2023 +0100

    Create a new register cell type 'completioncell'
    
    Used to present a pop up list of text entries that has been filtered by
    entered keystrokes anywhere in the text entries.

diff --git a/gnucash/register/register-core/CMakeLists.txt b/gnucash/register/register-core/CMakeLists.txt
index 7ec7f63450..8c1afeee44 100644
--- a/gnucash/register/register-core/CMakeLists.txt
+++ b/gnucash/register/register-core/CMakeLists.txt
@@ -24,6 +24,7 @@ set (register_core_HEADERS
   cell-factory.h
   cellblock.h
   combocell.h
+  completioncell.h
   datecell.h
   formulacell.h
   gtable.h
diff --git a/gnucash/register/register-core/completioncell.h b/gnucash/register/register-core/completioncell.h
new file mode 100644
index 0000000000..8484eb428f
--- /dev/null
+++ b/gnucash/register/register-core/completioncell.h
@@ -0,0 +1,79 @@
+/********************************************************************\
+ * completion.h -- combo-box used for completion cell               *
+ *                                                                  *
+ * 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 at gnu.org                   *
+ *                                                                  *
+\********************************************************************/
+
+/** @addtogroup Cell Cell
+ * @{
+ * @file completion.h
+ * @struct CompletionCell
+ * @brief The CompletionCell object implements a cell handler with a
+ *        "combination-box" pull-down menu in it.
+ *
+ * On output, the currently selected menu item is displayed.
+ * On input, the user can select from a list in the pull-down menu,
+ * or use the keyboard to select a menu entry by typing the first
+ * few menu characters.
+ *
+ * @author Copyright (c) 2023 Robert Fewell
+ */
+
+#ifndef COMPLETION_CELL_H
+#define COMPLETION_CELL_H
+
+#include <glib.h>
+
+#include "basiccell.h"
+
+typedef struct
+{
+    BasicCell cell;
+} CompletionCell;
+
+
+BasicCell * gnc_completion_cell_new (void);
+void gnc_completion_cell_init (CompletionCell* cell);
+
+void gnc_completion_cell_set_value (CompletionCell* cell, const char* value);
+
+void gnc_completion_cell_clear_menu (CompletionCell* cell);
+
+/** Add a menu item to the hash table list. */
+void gnc_completion_cell_add_menu_item (CompletionCell* cell,
+                                        const char* menustr);
+
+/** Enable sorting of the menu item's contents. */
+void gnc_completion_cell_set_sort_enabled (CompletionCell* cell,
+                                           gboolean enabled);
+
+/** Determines whether the cell will accept strings not in the
+ * menu. Defaults to strict, i.e., only menu items are accepted. */
+void gnc_completion_cell_set_strict (CompletionCell* cell, gboolean strict);
+
+/** Determines whether the popup list autosizes itself or uses
+ * all available space. FALSE by default. */
+void gnc_completion_cell_set_autosize (CompletionCell* cell, gboolean autosize);
+
+/** Register the sort direction. Used to determine in what order the completion should
+ * present the list. FALSE by default */
+void gnc_completion_cell_reverse_sort (CompletionCell* cell, gboolean is_reversed);
+
+/** @} */
+#endif
diff --git a/gnucash/register/register-core/register-common.c b/gnucash/register/register-core/register-common.c
index 9ea9708b48..a1c11f455a 100644
--- a/gnucash/register/register-core/register-common.c
+++ b/gnucash/register/register-core/register-common.c
@@ -27,6 +27,7 @@
 #include "basiccell.h"
 #include "cell-factory.h"
 #include "combocell.h"
+#include "completioncell.h"
 #include "datecell.h"
 #include "formulacell.h"
 #include "numcell.h"
diff --git a/gnucash/register/register-core/register-common.h b/gnucash/register/register-core/register-common.h
index 31c427469a..9bb2d4007d 100644
--- a/gnucash/register/register-core/register-common.h
+++ b/gnucash/register/register-core/register-common.h
@@ -71,6 +71,7 @@
 #define QUICKFILL_CELL_TYPE_NAME "quickfill-cell"
 #define FORMULA_CELL_TYPE_NAME   "formula-cell"
 #define CHECKBOX_CELL_TYPE_NAME	 "checkbox-cell"
+#define COMPLETION_CELL_TYPE_NAME "completion-cell"
 
 void gnc_register_init (void);
 void gnc_register_shutdown (void);
diff --git a/gnucash/register/register-gnome/CMakeLists.txt b/gnucash/register/register-gnome/CMakeLists.txt
index 51a288680c..6d893fe2aa 100644
--- a/gnucash/register/register-gnome/CMakeLists.txt
+++ b/gnucash/register/register-gnome/CMakeLists.txt
@@ -2,6 +2,7 @@ include(CheckSymbolExists)
 
 set (register_gnome_SOURCES
   combocell-gnome.c
+  completioncell-gnome.c
   datecell-gnome.c
   formulacell-gnome.c
   gnucash-color.c
diff --git a/gnucash/register/register-gnome/completioncell-gnome.c b/gnucash/register/register-gnome/completioncell-gnome.c
new file mode 100644
index 0000000000..57d63cc2cf
--- /dev/null
+++ b/gnucash/register/register-gnome/completioncell-gnome.c
@@ -0,0 +1,951 @@
+/********************************************************************\
+ * completioncell-gnome.c -- completion combobox cell for gnome     *
+ *                                                                  *
+ * 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 at gnu.org                   *
+ *                                                                  *
+\********************************************************************/
+
+/*
+ * FILE: completioncell-gnome.c
+ *
+ * FUNCTION: Implement gnome portion of a entry completion combo widget
+ *           embedded in a table cell.
+ *
+ * HISTORY:
+ * @author Copyright (c) 2023 Robert Fewell
+ */
+
+#include <config.h>
+
+#include <string.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "completioncell.h"
+#include "gnc-prefs.h"
+#include "gnucash-item-edit.h"
+#include "gnucash-item-list.h"
+#include "gnucash-sheet.h"
+#include "gnucash-sheetP.h"
+#include "table-allgui.h"
+#include "gnc-glib-utils.h"
+
+typedef struct _PopBox
+{
+    GnucashSheet* sheet;
+    GncItemEdit*  item_edit;
+    GncItemList*  item_list;
+
+    GHashTable*   item_hash; // the item hash table
+    GtkListStore* item_store; // the item list store
+
+    gchar*        newval; // string value to find
+
+    gboolean      signals_connected; // list signals connected
+    gboolean      list_popped;  // list is popped up
+
+    gboolean      autosize; // autosize the popup width
+
+    gboolean      sort_enabled; // sort of list store enabled
+    gboolean      register_is_reversed; // whether the register is reversed
+    gboolean      stop_searching; // set when there are no results
+
+    gboolean      strict; // text entry must be in the list
+    gboolean      in_list_select; // item selected in the list
+
+    gint          occurrence; // the position in the list
+
+} PopBox;
+
+/** Enumeration for the list-store */
+enum GncCompletionColumn
+{
+    TEXT_COL,        //0
+    TEXT_MARKUP_COL, //1
+    WEIGHT_COL,      //2
+};
+
+static void gnc_completion_cell_gui_realize (BasicCell* bcell, gpointer w);
+static void gnc_completion_cell_gui_move (BasicCell* bcell);
+static void gnc_completion_cell_gui_destroy (BasicCell* bcell);
+static gboolean gnc_completion_cell_enter (BasicCell* bcell,
+                                           int* cursor_position,
+                                           int* start_selection,
+                                           int* end_selection);
+static void gnc_completion_cell_leave (BasicCell* bcell);
+static void gnc_completion_cell_destroy (BasicCell* bcell);
+
+BasicCell*
+gnc_completion_cell_new (void)
+{
+    CompletionCell* cell = g_new0 (CompletionCell, 1);
+    gnc_completion_cell_init (cell);
+    return &cell->cell;
+}
+
+void
+gnc_completion_cell_init (CompletionCell* cell)
+{
+    gnc_basic_cell_init (& (cell->cell));
+
+    cell->cell.is_popup = TRUE;
+
+    cell->cell.destroy = gnc_completion_cell_destroy;
+
+    cell->cell.gui_realize = gnc_completion_cell_gui_realize;
+    cell->cell.gui_destroy = gnc_completion_cell_gui_destroy;
+
+    PopBox* box = g_new0 (PopBox, 1);
+
+    box->sheet = NULL;
+    box->item_edit = NULL;
+    box->item_list = NULL;
+    box->item_store = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING,
+                                             G_TYPE_INT);
+    box->signals_connected = FALSE;
+    box->list_popped = FALSE;
+    box->autosize = FALSE;
+    box->register_is_reversed = FALSE;
+
+    box->sort_enabled = FALSE;
+
+    cell->cell.gui_private = box;
+
+    box->stop_searching = FALSE;
+
+    box->strict = FALSE;
+    box->in_list_select = FALSE;
+    box->occurrence = 0;
+
+    box->item_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+static void
+hide_popup (PopBox* box)
+{
+    gnc_item_edit_hide_popup (box->item_edit);
+    box->list_popped = FALSE;
+}
+
+static void
+select_item_cb (GncItemList* item_list, char* item_string, gpointer user_data)
+{
+    CompletionCell* cell = user_data;
+    PopBox* box = cell->cell.gui_private;
+
+    box->in_list_select = TRUE;
+    gnucash_sheet_modify_current_cell (box->sheet, item_string);
+    box->in_list_select = FALSE;
+
+    hide_popup (box);
+}
+
+static void
+change_item_cb (GncItemList* item_list, char* item_string, gpointer user_data)
+{
+    CompletionCell* cell = user_data;
+    PopBox* box = cell->cell.gui_private;
+
+    box->in_list_select = TRUE;
+    gnucash_sheet_modify_current_cell (box->sheet, item_string);
+    box->in_list_select = FALSE;
+}
+
+static void
+activate_item_cb (GncItemList* item_list, char* item_string, gpointer user_data)
+{
+    CompletionCell* cell = user_data;
+    PopBox* box = cell->cell.gui_private;
+    hide_popup (box);
+}
+
+static void
+block_list_signals (CompletionCell* cell)
+{
+    PopBox* box = cell->cell.gui_private;
+
+    if (!box->signals_connected)
+        return;
+
+    g_signal_handlers_block_matched (G_OBJECT(box->item_list),
+                                     G_SIGNAL_MATCH_DATA,
+                                     0, 0, NULL, NULL, cell);
+}
+
+static void
+unblock_list_signals (CompletionCell* cell)
+{
+    PopBox* box = cell->cell.gui_private;
+
+    if (!box->signals_connected)
+        return;
+
+    g_signal_handlers_unblock_matched (G_OBJECT(box->item_list),
+                                       G_SIGNAL_MATCH_DATA,
+                                       0, 0, NULL, NULL, cell);
+}
+
+static void
+key_press_item_cb (GncItemList* item_list, GdkEventKey* event, gpointer user_data)
+{
+    CompletionCell* cell = user_data;
+    PopBox* box = cell->cell.gui_private;
+
+    switch (event->keyval)
+    {
+    case GDK_KEY_Escape:
+        block_list_signals (cell); // Prevent recursion, unselect all
+        gnc_item_list_select (box->item_list, NULL);
+        unblock_list_signals (cell);
+        hide_popup (box);
+        break;
+
+    default:
+        gtk_widget_event (GTK_WIDGET (box->sheet),
+                          (GdkEvent*) event);
+        break;
+    }
+}
+
+static void
+completion_disconnect_signals (CompletionCell* cell)
+{
+    PopBox* box = cell->cell.gui_private;
+
+    if (!box->signals_connected)
+        return;
+
+    g_signal_handlers_disconnect_matched (G_OBJECT(box->item_list),
+                                          G_SIGNAL_MATCH_DATA,
+                                          0, 0, NULL, NULL, cell);
+
+    box->signals_connected = FALSE;
+}
+
+static void
+completion_connect_signals (CompletionCell* cell)
+{
+    PopBox* box = cell->cell.gui_private;
+
+    if (box->signals_connected)
+        return;
+
+    g_signal_connect (G_OBJECT(box->item_list), "select_item",
+                      G_CALLBACK(select_item_cb), cell);
+
+    g_signal_connect (G_OBJECT(box->item_list), "change_item",
+                      G_CALLBACK(change_item_cb), cell);
+
+    g_signal_connect (G_OBJECT(box->item_list), "activate_item",
+                      G_CALLBACK(activate_item_cb), cell);
+
+    g_signal_connect (G_OBJECT(box->item_list), "key_press_event",
+                      G_CALLBACK(key_press_item_cb), cell);
+
+    box->signals_connected = TRUE;
+}
+
+static void
+gnc_completion_cell_gui_destroy (BasicCell* bcell)
+{
+    CompletionCell* cell = (CompletionCell*) bcell;
+
+    if (cell->cell.gui_realize)
+    {
+        PopBox* box = bcell->gui_private;
+        if (box && box->item_list)
+        {
+            completion_disconnect_signals (cell);
+            g_object_unref (box->item_list);
+            box->item_list = NULL;
+        }
+        /* allow the widget to be shown again */
+        cell->cell.gui_realize = gnc_completion_cell_gui_realize;
+        cell->cell.gui_move = NULL;
+        cell->cell.enter_cell = NULL;
+        cell->cell.leave_cell = NULL;
+        cell->cell.gui_destroy = NULL;
+    }
+}
+
+static void
+gnc_completion_cell_destroy (BasicCell* bcell)
+{
+    CompletionCell* cell = (CompletionCell*) bcell;
+    PopBox* box = cell->cell.gui_private;
+
+    gnc_completion_cell_gui_destroy (& (cell->cell));
+
+    if (box)
+    {
+        if (box->item_hash)
+            g_hash_table_destroy (box->item_hash);
+
+        g_free (box);
+        cell->cell.gui_private = NULL;
+    }
+    cell->cell.gui_private = NULL;
+    cell->cell.gui_realize = NULL;
+}
+
+static gint
+sort_func (GtkTreeModel* model, GtkTreeIter* iter_a, GtkTreeIter* iter_b, gpointer user_data)
+{
+    gint a_weight, b_weight;
+    gint ret = 0;
+
+    gtk_tree_model_get (model, iter_a, WEIGHT_COL, &a_weight, -1);
+    gtk_tree_model_get (model, iter_b, WEIGHT_COL, &b_weight, -1);
+
+    if (a_weight < b_weight)
+        ret = -1;
+    else if (a_weight > b_weight)
+        ret = 1;
+
+    return ret;
+}
+
+void
+gnc_completion_cell_set_sort_enabled (CompletionCell* cell,
+                                      gboolean enabled)
+{
+    if (!cell)
+        return;
+
+    PopBox* box = cell->cell.gui_private;
+    box->sort_enabled = enabled;
+}
+
+static void
+set_sort_column_enabled (PopBox* box, gboolean enable)
+{
+    if (enable)
+    {
+        gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE(box->item_list->list_store),
+                                         WEIGHT_COL, sort_func, box->item_list, NULL);
+
+        gnc_item_list_set_sort_column (box->item_list, WEIGHT_COL);
+    }
+    else
+        gnc_item_list_set_sort_column (box->item_list, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
+}
+
+static void
+item_store_clear (CompletionCell* cell)
+{
+    if (!cell)
+        return;
+
+    PopBox* box = cell->cell.gui_private;
+
+    // disconnect list store from tree view
+    GtkListStore *store = gnc_item_list_disconnect_store (box->item_list);
+
+    block_list_signals (cell);
+
+    if (box->sort_enabled) // if sorting, disable it
+        set_sort_column_enabled (box, FALSE);
+
+    box->stop_searching = FALSE;
+    gtk_list_store_clear (box->item_store);
+
+    if (box->sort_enabled) // if sorting, enable it
+        set_sort_column_enabled (box, TRUE);
+
+    unblock_list_signals (cell);
+
+    // reconect list store to tree view
+    gnc_item_list_connect_store (box->item_list, store);
+
+    hide_popup (box);
+}
+
+void
+gnc_completion_cell_clear_menu (CompletionCell* cell)
+{
+    if (!cell)
+        return;
+
+    PopBox* box = cell->cell.gui_private;
+    if (!box)
+        return;
+
+    if (box->item_list)
+    {
+        g_hash_table_remove_all (box->item_hash);
+        item_store_clear (cell);
+        box->occurrence = 0;
+    }
+}
+
+void
+gnc_completion_cell_add_menu_item (CompletionCell* cell,
+                                   const char* menustr)
+{
+    if (!cell || !menustr)
+        return;
+
+    PopBox* box = cell->cell.gui_private;
+
+    if (box->item_hash)
+    {
+        gpointer value = g_hash_table_lookup (box->item_hash, menustr);
+        gboolean update = FALSE;
+        if (value)
+        {
+            if (!box->register_is_reversed)
+                update = TRUE;
+        }
+        else
+            update = TRUE;
+
+        if (update)
+        {
+            g_hash_table_insert (box->item_hash, g_strdup (menustr),
+                                 GINT_TO_POINTER(box->occurrence));
+        }
+        box->occurrence++;
+    }
+}
+
+void
+gnc_completion_cell_set_value (CompletionCell* cell, const char* str)
+{
+    if (!cell || !str)
+
+    gnc_basic_cell_set_value (&cell->cell, str);
+}
+
+static inline void
+list_store_append (GtkListStore *store, char* string,
+                   char* markup, gint weight)
+{
+    GtkTreeIter iter;
+
+    g_return_if_fail (store);
+    g_return_if_fail (string);
+    g_return_if_fail (markup);
+
+    gtk_list_store_append (store, &iter);
+
+    gtk_list_store_set (store, &iter, TEXT_COL, string,
+                                      TEXT_MARKUP_COL, markup,
+                                      WEIGHT_COL, weight, -1);
+}
+
+static char*
+normalize_and_fold (char* utf8_string)
+{
+    char *normalized = g_utf8_normalize (utf8_string, -1, G_NORMALIZE_NFC);
+    if (!normalized)
+        return NULL;
+
+    char *folded = g_utf8_casefold (normalized, -1);
+    g_free (normalized);
+    return folded;
+}
+
+static gint
+test_and_add (PopBox* box, const gchar *text, gint start_pos,
+              gpointer key, gint occurrence_difference)
+{
+    gint ret_value = -1;
+    gint text_length = g_utf8_strlen (text, -1);
+
+    if (start_pos > text_length)
+       return ret_value;
+
+    gchar *sub_text = g_utf8_substring (text, start_pos, text_length);
+    gchar *sub_text_norm_fold = normalize_and_fold (sub_text);
+    gchar *found_text_ptr = g_strstr_len (sub_text_norm_fold, -1, box->newval);
+
+    if (found_text_ptr)
+    {
+        gchar *markup = NULL, *prefix = NULL, *match = NULL, *suffix = NULL;
+        glong newval_length = g_utf8_strlen (box->newval, -1);
+        gulong found_location = g_utf8_pointer_to_offset (sub_text_norm_fold,
+                                                          found_text_ptr) + start_pos;
+        gboolean have_boundary = FALSE;
+        gint prefix_length;
+        gint weight;
+
+        if (found_location > 0)
+            prefix = g_utf8_substring (text, 0, found_location);
+        else
+            prefix = g_strdup ("");
+
+        prefix_length = g_utf8_strlen (prefix, -1);
+
+        match = g_utf8_substring (text, found_location, found_location + newval_length);
+
+        if (found_location >= 1)
+        {
+            gunichar prev = g_utf8_get_char (g_utf8_offset_to_pointer (sub_text, found_location - start_pos - 1));
+            if (prev && (g_unichar_isspace (prev) || g_unichar_ispunct (prev)))
+                have_boundary = TRUE;
+            else
+                ret_value = found_location + 1;
+        }
+
+        suffix = g_utf8_substring (text, found_location + newval_length, text_length);
+
+        markup = g_markup_printf_escaped ("%s<b>%s</b>%s%s", prefix, match, suffix, " ");
+
+        if ((prefix_length == 0 ) || have_boundary)
+        {
+            weight = occurrence_difference; // sorted by recent first
+
+            if (g_strcmp0 (sub_text_norm_fold, box->newval) == 0) // exact match
+                weight = 1;
+
+            list_store_append (box->item_store, key, markup, weight);
+        }
+        g_free (markup);
+        g_free (prefix);
+        g_free (match);
+        g_free (suffix);
+    }
+    g_free (sub_text_norm_fold);
+    g_free (sub_text);
+    return ret_value;
+}
+
+static void
+add_item (gpointer key, gpointer value, gpointer user_data)
+{
+    PopBox* box = user_data;
+    gchar *hash_entry = g_strdup (key);
+
+    if (hash_entry && *hash_entry)
+    {
+        gint start_pos = 0;
+        gint occurrence_difference;
+        gnc_utf8_strip_invalid_and_controls (hash_entry);
+
+        if (box->register_is_reversed)
+            occurrence_difference = GPOINTER_TO_INT(value) + 1;
+        else
+            occurrence_difference = box->occurrence - GPOINTER_TO_INT(value);
+
+        do
+        {
+            start_pos = test_and_add (box, hash_entry, start_pos, key, occurrence_difference);
+        }
+        while (start_pos != -1);
+    }
+    g_free (hash_entry);
+}
+
+static void
+select_first_entry_in_list (PopBox* box)
+{
+    GtkTreeModel *model = gtk_tree_view_get_model (box->item_list->tree_view);
+    GtkTreeIter iter;
+    gchar* string;
+
+    if (!gtk_tree_model_get_iter_first (model, &iter))
+        return;
+
+    if (!gtk_tree_model_iter_next (model, &iter))
+        return;
+
+    gtk_tree_model_get (model, &iter, TEXT_COL, &string, -1);
+
+    gnc_item_list_select (box->item_list, string);
+
+    GtkTreePath* path = gtk_tree_path_new_first ();
+    gtk_tree_view_scroll_to_cell (box->item_list->tree_view,
+                                  path, NULL, TRUE, 0.5, 0.0);
+    gtk_tree_path_free (path);
+    g_free (string);
+}
+
+static void
+populate_list_store (CompletionCell* cell, const gchar* str)
+{
+    PopBox* box = cell->cell.gui_private;
+
+    box->in_list_select = FALSE;
+
+    if (box->stop_searching)
+        return;
+
+    if (str && *str)
+        box->newval = normalize_and_fold ((gchar*)str);
+    else
+        return;
+
+    // disconnect list store from tree view
+    box->item_store = gnc_item_list_disconnect_store (box->item_list);
+
+    block_list_signals (cell);
+
+    if (box->sort_enabled) // if sorting, disable it
+        set_sort_column_enabled (box, FALSE);
+
+    gtk_list_store_clear (box->item_store);
+
+    // add to the list store
+    g_hash_table_foreach (box->item_hash, add_item, box);
+
+    if (box->sort_enabled) // if sorting, enable it
+        set_sort_column_enabled (box, TRUE);
+
+    unblock_list_signals (cell);
+
+    // reconnect list store to tree view
+    gnc_item_list_connect_store (box->item_list, box->item_store);
+
+    // if no entries, do not show popup
+    if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL(box->item_store), NULL) == 1)
+    {
+        box->stop_searching = TRUE;
+        hide_popup (box);
+    }
+    else
+        gnc_item_edit_show_popup (box->item_edit);
+
+    block_list_signals (cell); // Prevent recursion, select first entry
+    select_first_entry_in_list (box);
+    unblock_list_signals (cell);
+
+    g_free (box->newval);
+}
+
+static void
+gnc_completion_cell_modify_verify (BasicCell* bcell,
+                                   const char* change,
+                                   int change_len,
+                                   const char* newval,
+                                   int newval_len,
+                                   int* cursor_position,
+                                   int* start_selection,
+                                   int* end_selection)
+{
+    CompletionCell* cell = (CompletionCell*) bcell;
+    PopBox* box = cell->cell.gui_private;
+    glong newval_chars = g_utf8_strlen (newval, newval_len);
+
+    if (box->in_list_select)
+    {
+        gnc_basic_cell_set_value_internal (bcell, newval);
+        *cursor_position = -1;
+        *start_selection = 0;
+        *end_selection = 0;
+        return;
+    }
+
+    // check to enable searching
+    if (((*cursor_position < newval_chars) &&
+         (g_utf8_strlen (bcell->value, -1) < newval_chars)) ||
+         (g_utf8_strlen (bcell->value, -1) > newval_chars))
+    {
+         box->stop_searching = FALSE;
+    }
+
+    populate_list_store (cell, newval);
+
+    if (g_strcmp0 (newval, "") == 0)
+    {
+        block_list_signals (cell); // Prevent recursion, unselect all
+        gnc_item_list_select (box->item_list, NULL);
+        unblock_list_signals (cell);
+        hide_popup (box);
+    }
+    gnc_basic_cell_set_value_internal (bcell, newval);
+}
+
+static gboolean
+gnc_completion_cell_direct_update (BasicCell* bcell,
+                                   int* cursor_position,
+                                   int* start_selection,
+                                   int* end_selection,
+                                   void* gui_data)
+{
+    CompletionCell* cell = (CompletionCell*) bcell;
+    PopBox* box = cell->cell.gui_private;
+    GdkEventKey* event = gui_data;
+
+    if (event->type != GDK_KEY_PRESS)
+        return FALSE;
+
+    switch (event->keyval)
+    {
+    case GDK_KEY_Tab:
+    case GDK_KEY_ISO_Left_Tab:
+        {
+            char* string = gnc_item_list_get_selection (box->item_list);
+
+            if (!string)
+                break;
+
+            g_signal_emit_by_name (G_OBJECT(box->item_list), "change_item",
+                                   string, (gpointer)bcell);
+
+            g_free (string);
+            break;
+        }
+    }
+
+    if (box->strict)
+        box->in_list_select = gnc_item_in_list (box->item_list, bcell->value);
+
+    if (!bcell->value)
+        item_store_clear (cell);
+
+    return FALSE;
+}
+
+void
+gnc_completion_cell_reverse_sort (CompletionCell* cell, gboolean is_reversed)
+{
+    if (!cell)
+        return;
+
+    PopBox* box = cell->cell.gui_private;
+
+    if (is_reversed != box->register_is_reversed)
+    {
+        gnc_completion_cell_clear_menu (cell);
+        box->register_is_reversed = is_reversed;
+        box->occurrence = 0;
+    }
+}
+
+static void
+gnc_completion_cell_gui_realize (BasicCell* bcell, gpointer data)
+{
+    GnucashSheet* sheet = data;
+    GncItemEdit* item_edit = gnucash_sheet_get_item_edit (sheet);
+    CompletionCell* cell = (CompletionCell*) bcell;
+    PopBox* box = cell->cell.gui_private;
+
+    /* initialize gui-specific, private data */
+    box->sheet = sheet;
+    box->item_edit = item_edit;
+    box->item_list = GNC_ITEM_LIST(gnc_item_list_new (box->item_store));
+
+    block_list_signals (cell);
+    set_sort_column_enabled (box, FALSE);
+    unblock_list_signals (cell);
+
+    gtk_widget_show_all (GTK_WIDGET(box->item_list));
+    g_object_ref_sink (box->item_list);
+
+    /* to mark cell as realized, remove the realize method */
+    cell->cell.gui_realize = NULL;
+    cell->cell.gui_move = gnc_completion_cell_gui_move;
+    cell->cell.enter_cell = gnc_completion_cell_enter;
+    cell->cell.leave_cell = gnc_completion_cell_leave;
+    cell->cell.gui_destroy = gnc_completion_cell_gui_destroy;
+    cell->cell.modify_verify = gnc_completion_cell_modify_verify;
+    cell->cell.direct_update = gnc_completion_cell_direct_update;
+}
+
+static void
+reset_item_list_to_default_setup (BasicCell* bcell)
+{
+    PopBox* box = bcell->gui_private;
+    PopupToggle popup_toggle;
+
+    item_store_clear ((CompletionCell*) bcell);
+
+    popup_toggle = box->item_edit->popup_toggle;
+    gtk_widget_set_sensitive (GTK_WIDGET(popup_toggle.tbutton), TRUE);
+    gtk_widget_set_visible (GTK_WIDGET(popup_toggle.tbutton), TRUE);
+
+    GtkTreeViewColumn *column = gtk_tree_view_get_column (
+                                    GTK_TREE_VIEW(box->item_list->tree_view), TEXT_COL);
+    gtk_tree_view_column_clear_attributes (column,box->item_list->renderer);
+    gtk_tree_view_column_add_attribute (column, box->item_list->renderer,
+                                        "text", TEXT_COL);
+    box->list_popped = FALSE;
+}
+
+static void
+gnc_completion_cell_gui_move (BasicCell* bcell)
+{
+    PopBox* box = bcell->gui_private;
+
+    completion_disconnect_signals ((CompletionCell*) bcell);
+
+    gnc_item_edit_set_popup (box->item_edit, NULL, NULL,
+                             NULL, NULL, NULL, NULL, NULL);
+
+    reset_item_list_to_default_setup (bcell);
+}
+
+static int
+popup_get_height (G_GNUC_UNUSED GtkWidget* widget,
+                  int space_available,
+                  int row_height,
+                  gpointer user_data)
+{
+    PopBox* box = user_data;
+    GtkScrolledWindow* scrollwin = GNC_ITEM_LIST(widget)->scrollwin;
+    GtkWidget *hsbar = gtk_scrolled_window_get_hscrollbar (scrollwin);
+    GtkStyleContext *context = gtk_widget_get_style_context (hsbar);
+    /* Note: gtk_scrolled_window_get_overlay_scrolling (scrollwin) always returns
+       TRUE so look for style class "overlay-indicator" on the scrollbar. */
+    gboolean overlay = gtk_style_context_has_class (context, "overlay-indicator");
+    int count = gnc_item_list_num_entries (box->item_list);
+    int height = count * (gnc_item_list_get_cell_height (box->item_list) + 2);
+
+    if (!overlay)
+    {
+        gint minh, nath;
+        gtk_widget_get_preferred_height (hsbar, &minh, &nath);
+        height = height + minh;
+    }
+
+    if (height < space_available)
+    {
+        // if the list is empty height would be 0 so return 1 instead to
+        // satisfy the check_popup_height_is_true function
+        gint ret_height = height ? height : 1;
+
+        gtk_widget_set_size_request (GTK_WIDGET(scrollwin), -1, ret_height);
+        gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrollwin),
+                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER);
+        return ret_height;
+    }
+    else
+        gtk_widget_set_size_request (GTK_WIDGET(scrollwin), -1, -1);
+
+    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrollwin),
+                                    GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+    return space_available;
+}
+
+static int
+popup_autosize (GtkWidget* widget,
+                int max_width,
+                gpointer user_data)
+{
+    PopBox* box = user_data;
+
+    if (!box || !box->autosize)
+        return max_width;
+
+    return gnc_item_list_autosize (GNC_ITEM_LIST(widget)) + 20;
+}
+
+static void
+popup_set_focus (GtkWidget* widget,
+                 G_GNUC_UNUSED gpointer user_data)
+{
+    /* An empty GtkTreeView grabbing focus causes the key_press events to be
+     * lost because there's no entry cell to handle them.
+     */
+    if (gnc_item_list_num_entries (GNC_ITEM_LIST(widget)))
+        gtk_widget_grab_focus (GTK_WIDGET (GNC_ITEM_LIST(widget)->tree_view));
+}
+
+static void
+popup_post_show (GtkWidget* widget,
+                 G_GNUC_UNUSED gpointer user_data)
+{
+    gnc_item_list_autosize (GNC_ITEM_LIST(widget));
+    gnc_item_list_show_selected (GNC_ITEM_LIST(widget));
+}
+
+static int
+popup_get_width (GtkWidget* widget,
+                 G_GNUC_UNUSED gpointer user_data)
+{
+    GtkAllocation alloc;
+    gtk_widget_get_allocation (GTK_WIDGET (GNC_ITEM_LIST(widget)->tree_view),
+                               &alloc);
+    return alloc.width;
+}
+
+static gboolean
+gnc_completion_cell_enter (BasicCell* bcell,
+                           int* cursor_position,
+                           int* start_selection,
+                           int* end_selection)
+{
+    CompletionCell* cell = (CompletionCell*) bcell;
+    PopBox* box = bcell->gui_private;
+    PopupToggle popup_toggle;
+
+    gnc_item_edit_set_popup (box->item_edit,
+                             GTK_WIDGET(box->item_list),
+                             popup_get_height, popup_autosize,
+                             popup_set_focus, popup_post_show,
+                             popup_get_width, box);
+
+    popup_toggle = box->item_edit->popup_toggle;
+    gtk_widget_set_sensitive (GTK_WIDGET(popup_toggle.tbutton), FALSE);
+    gtk_widget_set_visible (GTK_WIDGET(popup_toggle.tbutton), FALSE);
+
+    GtkTreeViewColumn *column = gtk_tree_view_get_column (
+                                    GTK_TREE_VIEW(box->item_list->tree_view), TEXT_COL);
+    gtk_tree_view_column_clear_attributes (column, box->item_list->renderer);
+    gtk_tree_view_column_add_attribute (column, box->item_list->renderer,
+                                        "markup", TEXT_MARKUP_COL);
+
+    completion_connect_signals (cell);
+
+    *cursor_position = -1;
+    *start_selection = 0;
+    *end_selection = -1;
+
+    return TRUE;
+}
+
+static void
+gnc_completion_cell_leave (BasicCell* bcell)
+{
+    PopBox* box = bcell->gui_private;
+
+    completion_disconnect_signals ((CompletionCell*) bcell);
+
+    gnc_item_edit_set_popup (box->item_edit, NULL, NULL,
+                             NULL, NULL, NULL, NULL, NULL);
+
+    reset_item_list_to_default_setup (bcell);
+
+    if (box->strict && !box->in_list_select)
+        gnc_basic_cell_set_value_internal (bcell, "");
+}
+
+void
+gnc_completion_cell_set_strict (CompletionCell* cell, gboolean strict)
+{
+    if (!cell)
+        return;
+
+    PopBox* box = cell->cell.gui_private;
+    if (!box)
+        return;
+
+    box->strict = strict;
+}
+
+void
+gnc_completion_cell_set_autosize (CompletionCell* cell, gboolean autosize)
+{
+    if (!cell)
+        return;
+
+    PopBox* box = cell->cell.gui_private;
+    if (!box)
+        return;
+
+    box->autosize = autosize;
+}



Summary of changes:
 gnucash/gnome/assistant-stock-transaction.cpp      | 2465 +++++++++++++-------
 gnucash/gnome/test/CMakeLists.txt                  |   37 +
 .../test/gtest-assistant-stock-transaction.cpp     |  274 +++
 .../org.gnucash.GnuCash.dialogs.gschema.xml.in     |   11 +
 .../gtkbuilder/assistant-stock-transaction.glade   |    6 +-
 .../register/ledger-core/split-register-layout.c   |    2 +-
 gnucash/register/ledger-core/split-register-load.c |   44 +-
 gnucash/register/ledger-core/split-register.c      |   17 +-
 gnucash/register/register-core/CMakeLists.txt      |    1 +
 gnucash/register/register-core/combocell.h         |    8 -
 gnucash/register/register-core/completioncell.h    |   79 +
 gnucash/register/register-core/register-common.c   |    1 +
 gnucash/register/register-core/register-common.h   |    1 +
 gnucash/register/register-gnome/CMakeLists.txt     |    1 +
 gnucash/register/register-gnome/combocell-gnome.c  |   92 +-
 .../register/register-gnome/completioncell-gnome.c |  960 ++++++++
 .../register/register-gnome/gnucash-item-list.c    |   48 +-
 .../register/register-gnome/gnucash-item-list.h    |    6 +-
 gnucash/register/register-gnome/gnucash-register.c |    2 +
 19 files changed, 3007 insertions(+), 1048 deletions(-)
 create mode 100644 gnucash/gnome/test/gtest-assistant-stock-transaction.cpp
 create mode 100644 gnucash/register/register-core/completioncell.h
 create mode 100644 gnucash/register/register-gnome/completioncell-gnome.c



More information about the gnucash-changes mailing list