gnucash stable: Multiple changes pushed

Robert Fewell bobit at code.gnucash.org
Mon Mar 16 05:33:06 EDT 2026


Updated	 via  https://github.com/Gnucash/gnucash/commit/be94e9ce (commit)
	 via  https://github.com/Gnucash/gnucash/commit/15f7ca44 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/93fed182 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/950f118e (commit)
	 via  https://github.com/Gnucash/gnucash/commit/09f33eb4 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/5df0a083 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/b037d7dd (commit)
	from  https://github.com/Gnucash/gnucash/commit/5f1c6232 (commit)



commit be94e9ce41a394dc892bb48cc626789c1e59657c
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue Feb 3 16:03:32 2026 +0000

    Control the 'Transaction/Split Paste' menu item
    
    Only enable the 'Transaction/Split Paste' menu item when there is
    something to paste.

diff --git a/gnucash/gnome/gnc-plugin-page-register.cpp b/gnucash/gnome/gnc-plugin-page-register.cpp
index 415378329d..58660e6b0c 100644
--- a/gnucash/gnome/gnc-plugin-page-register.cpp
+++ b/gnucash/gnome/gnc-plugin-page-register.cpp
@@ -1028,6 +1028,12 @@ gnc_plugin_page_register_ui_update (gpointer various,
 
     gnc_plugin_business_split_reg_ui_update (GNC_PLUGIN_PAGE(page));
 
+    // Transaction/Split paste action
+    action = gnc_plugin_page_get_action (GNC_PLUGIN_PAGE(page),
+                                         "PasteTransactionAction");
+    g_simple_action_set_enabled (G_SIMPLE_ACTION(action),
+                                 gnc_split_register_has_copied_item());
+
     /* If we are read only, make any modifying action inactive */
     if (read_only_reg)
     {
@@ -3804,6 +3810,12 @@ gnc_plugin_page_register_cmd_cut_transaction (GSimpleAction *simple,
 
     priv = GNC_PLUGIN_PAGE_REGISTER_GET_PRIVATE (page);
     gsr_default_cut_txn_handler (priv->gsr, NULL);
+
+    // Transaction/Split paste action
+    GAction *action = gnc_plugin_page_get_action (GNC_PLUGIN_PAGE(page),
+                                                  "PasteTransactionAction");
+    g_simple_action_set_enabled (G_SIMPLE_ACTION(action),
+                                 gnc_split_register_has_copied_item());
     LEAVE (" ");
 }
 
@@ -3823,6 +3835,12 @@ gnc_plugin_page_register_cmd_copy_transaction (GSimpleAction *simple,
     priv = GNC_PLUGIN_PAGE_REGISTER_GET_PRIVATE (page);
     reg = gnc_ledger_display_get_split_register (priv->ledger);
     gnc_split_register_copy_current (reg);
+
+    // Transaction/Split paste action
+    GAction *action = gnc_plugin_page_get_action (GNC_PLUGIN_PAGE(page),
+                                                  "PasteTransactionAction");
+    g_simple_action_set_enabled (G_SIMPLE_ACTION(action),
+                                 gnc_split_register_has_copied_item());
     LEAVE (" ");
 }
 
diff --git a/gnucash/register/ledger-core/split-register.c b/gnucash/register/ledger-core/split-register.c
index c518563710..3afd39dcf8 100644
--- a/gnucash/register/ledger-core/split-register.c
+++ b/gnucash/register/ledger-core/split-register.c
@@ -105,6 +105,12 @@ clear_copied_item()
     copied_item.anchor_split_index = 0;
 }
 
+gboolean
+gnc_split_register_has_copied_item (void)
+{
+    return copied_item.ft || copied_item.fs;
+}
+
 static void
 gnc_copy_split_onto_split (Split* from, Split* to,
                            Account *template_account,
diff --git a/gnucash/register/ledger-core/split-register.h b/gnucash/register/ledger-core/split-register.h
index 36d2bf6491..ed628c2bcb 100644
--- a/gnucash/register/ledger-core/split-register.h
+++ b/gnucash/register/ledger-core/split-register.h
@@ -470,6 +470,10 @@ gnc_split_register_get_split_amount_virt_loc (SplitRegister* reg, Split* split,
  *    created, or NULL if nothing happened. */
 Split* gnc_split_register_duplicate_current (SplitRegister* reg);
 
+/** Return TRUE if copied_item holds a transaction or split.
+ */
+gboolean gnc_split_register_has_copied_item (void);
+
 /** Makes a copy of the current entity, either a split or a
  *    transaction, so that it can be pasted later. */
 void gnc_split_register_copy_current (SplitRegister* reg);

commit 15f7ca44864481947dbcafefb51e68d266958540
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue Feb 3 16:06:57 2026 +0000

    Do not display the date for template duplication

diff --git a/gnucash/register/ledger-core/split-register.c b/gnucash/register/ledger-core/split-register.c
index 2d034e67d1..c518563710 100644
--- a/gnucash/register/ledger-core/split-register.c
+++ b/gnucash/register/ledger-core/split-register.c
@@ -640,7 +640,8 @@ gnc_split_register_duplicate_current (SplitRegister* reg)
                    : gnc_get_num_action (trans, NULL));
 
         if (!gnc_dup_trans_dialog (gnc_split_register_get_parent (reg), NULL,
-                                   TRUE, &date, in_num, &out_num, in_tnum, &out_tnum,
+                                   !reg->is_template, &date,
+                                   in_num, &out_num, in_tnum, &out_tnum,
                                    xaccTransGetDocLink (trans), &out_tdoclink))
         {
             gnc_resume_gui_refresh ();

commit 93fed1822f914c1ff8bfd25d257beee30bbe97c3
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue Feb 3 16:00:58 2026 +0000

    Add tests for copying template transactions.
    
    Test template transaction to template transaction.
    Test normal transaction to template transaction.
    Test template transaction to normal transaction.

diff --git a/gnucash/register/ledger-core/test/utest-split-register-copy-ops.c b/gnucash/register/ledger-core/test/utest-split-register-copy-ops.c
index 09c9474514..2c4782d44e 100644
--- a/gnucash/register/ledger-core/test/utest-split-register-copy-ops.c
+++ b/gnucash/register/ledger-core/test/utest-split-register-copy-ops.c
@@ -1,6 +1,6 @@
 /********************************************************************
- * utest-split-register-copy-ops.c: GLib g_test test suite for split-register-copy-ops.c.		    *
- * Copyright 2019 Geert Janssens <geert at kobaltwit.be>		    *
+ * utest-split-register-copy-ops.c:                                 *
+ * Copyright 2019 Geert Janssens <geert at kobaltwit.be>               *
  *                                                                  *
  * This program is free software; you can redistribute it and/or    *
  * modify it under the terms of the GNU General Public License as   *
@@ -14,7 +14,7 @@
  *                                                                  *
  * 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            *
+ * https://www.gnu.org/licenses/old-licenses/gpl-2.0.html           *
  * or contact:                                                      *
  *                                                                  *
  * Free Software Foundation           Voice:  +1-617-542-5942       *
@@ -28,6 +28,7 @@
 /* Add specific headers for this class */
 #include "split-register-copy-ops.h"
 #include <stdint.h>
+#include "SX-book-p.h"
 
 static const gchar *suitename = "/register/ledger-core/split-register-copy-ops";
 void test_suite_split_register_copy_ops ( void );
@@ -55,10 +56,30 @@ typedef struct
     Account *acc2;
     gnc_commodity *curr;
 
+    Account *template_acct;
+    Account *acc3;
+    Account *acc4;
+
     FloatingTxn ft;
     FloatingSplit fs1, fs2;
 } FlFixture;
 
+typedef struct
+{
+    QofBook *book;
+    Account *acc1;
+    Account *acc2;
+    gnc_commodity *curr;
+
+    Account *template_acct;
+    Account *acc3;
+    Account *acc4;
+
+    Transaction *txn;
+    Transaction *template_txn;
+
+} TemplateFixture;
+
 static void
 setup( Fixture *fixture, gconstpointer pData )
 {
@@ -108,6 +129,130 @@ setup( Fixture *fixture, gconstpointer pData )
     xaccAccountRecomputeBalance(fixture->acc2);
 }
 
+static void
+setup_template( TemplateFixture *fixture, gconstpointer pData )
+{
+    fixture->book = qof_book_new();
+
+    Account *templateRoot = xaccMallocAccount(fixture->book);
+    gnc_book_set_template_root (fixture->book, templateRoot);
+    gnc_commodity *template_commodity = gnc_commodity_new(fixture->book, "template",
+                                                          GNC_COMMODITY_NS_TEMPLATE,
+                                                          "template", "template", 1);
+    xaccAccountSetCommodity(templateRoot, template_commodity);
+
+    fixture->template_acct = xaccMallocAccount(fixture->book);
+    gnc_account_append_child (templateRoot, fixture->template_acct);
+
+    // Normal Transaction
+    time64 entered = gnc_dmy2time64 (20, 4, 2012);
+    time64 posted = gnc_dmy2time64 (21, 4, 2012);
+
+    Split *split1 = xaccMallocSplit(fixture->book);
+    Split *split2 = xaccMallocSplit(fixture->book);
+
+    fixture->acc1 = xaccMallocAccount(fixture->book);
+    fixture->acc2 = xaccMallocAccount(fixture->book);
+
+    fixture->curr = gnc_commodity_new(fixture->book, "Gnu Rand", "CURRENCY", "GNR", "", 100);
+    xaccAccountSetCommodity(fixture->acc1, fixture->curr);
+    xaccAccountSetCommodity(fixture->acc2, fixture->curr);
+
+    fixture->txn = xaccMallocTransaction (fixture->book);
+
+    xaccSplitSetMemo (split1, CACHE_INSERT ("foo"));
+    xaccSplitSetAction (split1, CACHE_INSERT ("bar"));
+    xaccSplitSetAmount (split1, gnc_numeric_create (3200, 100));
+    xaccSplitSetValue (split1, gnc_numeric_create (3200, 100));
+    xaccSplitSetAccount (split1, fixture->acc1);
+
+    xaccSplitSetAmount (split2, gnc_numeric_create (-3200, 100));
+    xaccSplitSetValue (split2, gnc_numeric_create (-3200, 100));
+    xaccSplitSetAccount (split2, fixture->acc2);
+
+    xaccTransBeginEdit (fixture->txn);
+    {
+        xaccTransSetNum (fixture->txn, CACHE_INSERT ("123"));
+        xaccTransSetDescription (fixture->txn, CACHE_INSERT ("Waldo Pepper"));
+        xaccTransSetDatePostedSecs (fixture->txn, posted);
+        xaccTransSetDateEnteredSecs (fixture->txn, entered);
+        xaccTransSetCurrency (fixture->txn, fixture->curr);
+        xaccSplitSetParent (split1, fixture->txn);
+        xaccSplitSetParent (split2, fixture->txn);
+        xaccTransSetNotes (fixture->txn, "Salt pork sausage");
+    }
+    xaccTransCommitEdit (fixture->txn);
+    xaccAccountSortSplits(fixture->acc1, FALSE);
+    xaccAccountSortSplits(fixture->acc2, FALSE);
+    xaccAccountRecomputeBalance(fixture->acc1);
+    xaccAccountRecomputeBalance(fixture->acc2);
+
+
+    // Template Transaction
+    Split *split3 = xaccMallocSplit (fixture->book);
+    Split *split4 = xaccMallocSplit (fixture->book);
+
+    fixture->acc3 = xaccMallocAccount(fixture->book);
+    fixture->acc4 = xaccMallocAccount(fixture->book);
+
+    const GncGUID* guid_acc3 = xaccAccountGetGUID(fixture->acc3);
+    const GncGUID* guid_acc4 = xaccAccountGetGUID(fixture->acc4);
+
+    xaccAccountSetCommodity(fixture->template_acct, template_commodity);
+    xaccAccountSetType(fixture->template_acct, ACCT_TYPE_BANK);
+
+    xaccAccountSetCommodity(fixture->acc3, fixture->curr);
+    xaccAccountSetCommodity(fixture->acc4, fixture->curr);
+
+    fixture->template_txn = xaccMallocTransaction (fixture->book);
+
+    xaccSplitSetMemo (split3, CACHE_INSERT ("foo"));
+    xaccSplitSetAction (split3, CACHE_INSERT ("bar"));
+
+    gnc_numeric numeric_zero = gnc_numeric_zero ();
+    gnc_numeric numeric = gnc_numeric_create (41,2);
+
+    qof_begin_edit (QOF_INSTANCE (split3));
+    qof_instance_set (QOF_INSTANCE (split3),
+              "sx-debit-formula", "",
+              "sx-debit-numeric", &numeric_zero,
+              "sx-credit-formula", "20.5",
+              "sx-credit-numeric", &numeric,
+              "sx-account", guid_acc3,
+              "sx-shares", "10",
+              NULL);
+
+    xaccSplitSetMemo (split4, CACHE_INSERT ("zoo"));
+    xaccSplitSetAction (split4, CACHE_INSERT ("tar"));
+
+    qof_begin_edit (QOF_INSTANCE (split4));
+    qof_instance_set (QOF_INSTANCE (split4),
+              "sx-debit-formula", "20.5",
+              "sx-debit-numeric", &numeric,
+              "sx-credit-formula", "",
+              "sx-credit-numeric", &numeric_zero,
+              "sx-account", guid_acc4,
+              "sx-shares", "10",
+              NULL);
+
+    xaccSplitSetAccount (split3, fixture->template_acct);
+    xaccSplitSetAccount (split4, fixture->template_acct);
+
+    xaccTransBeginEdit (fixture->template_txn);
+    {
+        xaccTransSetNum (fixture->template_txn, CACHE_INSERT ("123"));
+        xaccTransSetDescription (fixture->template_txn, CACHE_INSERT ("Waldo Pepper"));
+        xaccTransSetCurrency (fixture->template_txn, fixture->curr);
+
+        xaccSplitSetParent (split3, fixture->template_txn);
+        xaccSplitSetParent (split4, fixture->template_txn);
+        xaccTransSetNotes (fixture->template_txn, "Salt pork sausage");
+    }
+    xaccTransCommitEdit (fixture->template_txn);
+    xaccAccountSortSplits(fixture->acc3, FALSE);
+    xaccAccountSortSplits(fixture->acc4, FALSE);
+
+}
 static void
 teardown( Fixture *fixture, gconstpointer pData )
 {
@@ -120,6 +265,29 @@ teardown( Fixture *fixture, gconstpointer pData )
     qof_book_destroy( fixture->book );
 };
 
+static void
+teardown_template( TemplateFixture *fixture, gconstpointer pData )
+{
+    xaccTransDestroy (fixture->txn);
+    xaccTransDestroy (fixture->template_txn);
+
+    xaccAccountBeginEdit(fixture->acc1);
+    xaccAccountDestroy(fixture->acc1);
+    xaccAccountBeginEdit(fixture->acc2);
+    xaccAccountDestroy(fixture->acc2);
+
+    xaccAccountBeginEdit(fixture->acc3);
+    xaccAccountDestroy(fixture->acc3);
+    xaccAccountBeginEdit(fixture->acc4);
+    xaccAccountDestroy(fixture->acc4);
+
+    xaccAccountBeginEdit(fixture->template_acct);
+    xaccAccountDestroy(fixture->template_acct);
+
+    gnc_commodity_destroy(fixture->curr);
+    qof_book_destroy( fixture->book );
+};
+
 static void
 flsetup( FlFixture *fixture, gconstpointer pData )
 {
@@ -180,6 +348,8 @@ flteardown( FlFixture *fixture, gconstpointer pData )
     qof_book_destroy( fixture->book );
 };
 
+
+
 // Not Used
 /* gnc_float_split_get_reconcile_state - trivial getter, skipping
 char gnc_float_split_get_reconcile_state (const FloatingSplit *fs)// Local: 0:0:0
@@ -267,7 +437,7 @@ test_gnc_split_to_float_split (Fixture *fixture, gconstpointer pData)
 
     g_assert_nonnull (s);
 
-    fs = gnc_split_to_float_split (s);
+    fs = gnc_split_to_float_split (s, FALSE);
     g_assert_true (fs->m_split == s);
     g_assert_true (fs->m_account == xaccSplitGetAccount (s));
     g_assert_true (fs->m_transaction == xaccSplitGetParent (s));
@@ -289,7 +459,7 @@ test_gnc_float_split_to_split (Fixture *fixture, gconstpointer pData)
     Split *s = xaccMallocSplit(fixture->book);
     Transaction *txn = xaccMallocTransaction (fixture->book);
 
-    gnc_float_split_to_split (&fs, s);
+    gnc_float_split_to_split (&fs, s, NULL);
     g_assert_true (fixture->acc1 == xaccSplitGetAccount (s));
     g_assert_cmpstr ("Memo1", ==, xaccSplitGetMemo (s));
     g_assert_cmpstr ("Action1", ==, xaccSplitGetAction (s));
@@ -366,7 +536,7 @@ test_gnc_txn_to_float_txn (Fixture *fixture, gconstpointer pData)
     FloatingSplit *fs;
     Split *s;
 
-    ft = gnc_txn_to_float_txn (fixture->txn, FALSE);
+    ft = gnc_txn_to_float_txn (fixture->txn, FALSE, FALSE);
 
     /* Check transaction fields */
     g_assert_true (ft->m_txn == fixture->txn);
@@ -423,7 +593,7 @@ test_gnc_txn_to_float_txn_cut_semantics (Fixture *fixture, gconstpointer pData)
     FloatingSplit *fs;
     Split *s;
 
-    ft = gnc_txn_to_float_txn (fixture->txn, TRUE);
+    ft = gnc_txn_to_float_txn (fixture->txn, TRUE, FALSE);
 
     /* Check transaction fields */
     g_assert_true (ft->m_txn == fixture->txn);
@@ -618,6 +788,257 @@ test_gnc_float_txn_to_txn_swap_accounts_swap_nocommit (FlFixture *fixture, gcons
     impl_test_gnc_float_txn_to_txn_swap_accounts(fixture, &prefs);
 }
 
+static void
+test_gnc_template_to_template (TemplateFixture *fixture, gconstpointer pData)
+{
+    FloatingTxn *ft = NULL;
+
+    ft = gnc_txn_to_float_txn (fixture->template_txn, FALSE, TRUE);
+    g_assert_true (ft->m_txn == fixture->template_txn);
+
+    Transaction *new_txn = xaccMallocTransaction (fixture->book);
+    gnc_float_txn_to_template_txn (ft, new_txn, fixture->template_acct, FALSE);
+
+    /* Check transaction fields */
+    g_assert_cmpstr (xaccTransGetDescription (fixture->template_txn), ==, xaccTransGetDescription (new_txn));
+    g_assert_cmpstr (xaccTransGetNotes (fixture->template_txn), ==, xaccTransGetNotes (new_txn));
+
+    SplitList *sl_template_txn = xaccTransGetSplitList (fixture->template_txn), *siter_template_txn;
+    SplitList *sl_new_txn = xaccTransGetSplitList (new_txn), *siter_new_txn;
+
+    /* Check split fields of first split */
+    Split *s_template, *s_new_txn;
+
+    siter_template_txn = sl_template_txn;
+    siter_new_txn = sl_new_txn;
+
+    s_template = siter_template_txn->data;
+    s_new_txn = siter_new_txn->data;
+
+    // First split
+    g_assert_cmpstr (xaccSplitGetMemo (s_template), ==, xaccSplitGetMemo (s_new_txn));
+    g_assert_cmpstr (xaccSplitGetAction (s_template), ==, xaccSplitGetAction (s_new_txn));
+
+    GncGUID* guid = NULL;
+    const gchar *cf, *df, *shares;
+    gnc_numeric *cn, *dn;
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-account", &guid, NULL);
+    const GncGUID* guid_acc3 = xaccAccountGetGUID (fixture->acc3);
+    g_assert_true (guid_equal (guid_acc3, guid));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-formula", &cf, NULL);
+    g_assert_cmpstr ("20.5", ==, cf);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-numeric", &cn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_create (41,2), *cn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-formula", &df, NULL);
+    g_assert_cmpstr ("", ==, df);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-numeric", &dn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_zero(), *dn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-shares", &shares, NULL);
+    g_assert_cmpstr ("10", ==, shares);
+
+    // Second split
+    siter_new_txn = siter_new_txn->next;
+    siter_template_txn = siter_template_txn->next;
+
+    s_template = siter_template_txn->data;
+    s_new_txn = siter_new_txn->data;
+
+    g_assert_cmpstr (xaccSplitGetMemo (s_template), ==, xaccSplitGetMemo (s_new_txn));
+    g_assert_cmpstr (xaccSplitGetAction (s_template), ==, xaccSplitGetAction (s_new_txn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-account", &guid, NULL);
+    const GncGUID* guid_acc4 = xaccAccountGetGUID (fixture->acc4);
+    g_assert_true (guid_equal (guid_acc4, guid));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-formula", &cf, NULL);
+    g_assert_cmpstr ("", ==, cf);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-numeric", &cn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_zero(), *cn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-formula", &df, NULL);
+    g_assert_cmpstr ("20.5", ==, df);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-numeric", &dn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_create (41,2), *dn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-shares", &shares, NULL);
+    g_assert_cmpstr ("10", ==, shares);
+
+    g_assert_null (siter_template_txn->next);
+    g_assert_null (siter_new_txn->next);
+
+    guid_free (guid);
+    g_free (cn);
+    g_free (dn);
+
+    gnc_float_txn_free (ft);
+}
+
+static void
+test_gnc_normal_to_template (TemplateFixture *fixture, gconstpointer pData)
+{
+    FloatingTxn *ft = NULL;
+
+    ft = gnc_txn_to_float_txn (fixture->txn, FALSE, FALSE);
+    g_assert_true (ft->m_txn == fixture->txn);
+
+    Transaction *new_txn = xaccMallocTransaction (fixture->book);
+    gnc_float_txn_to_template_txn (ft, new_txn, fixture->template_acct, FALSE);
+
+    /* Check transaction fields */
+    g_assert_cmpstr (xaccTransGetDescription (fixture->txn), ==, xaccTransGetDescription (new_txn));
+    g_assert_cmpstr (xaccTransGetNotes (fixture->txn), ==, xaccTransGetNotes (new_txn));
+
+    SplitList *sl_txn = xaccTransGetSplitList (fixture->txn), *siter_txn;
+    SplitList *sl_new_txn = xaccTransGetSplitList (new_txn), *siter_new_txn;
+
+    /* Check split fields of first split */
+    Split *s_txn, *s_new_txn;
+
+    siter_txn = sl_txn;
+    siter_new_txn = sl_new_txn;
+
+    s_txn = siter_txn->data;
+    s_new_txn = siter_new_txn->data;
+
+    // First split
+    g_assert_cmpstr (xaccSplitGetMemo (s_new_txn), ==, xaccSplitGetMemo (s_txn));
+    g_assert_cmpstr (xaccSplitGetAction (s_new_txn), ==, xaccSplitGetAction (s_txn));
+
+    GncGUID* guid = NULL;
+    const gchar *cf, *df;
+    gnc_numeric *cn, *dn;
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-account", &guid, NULL);
+    const GncGUID* guid_acc1 = xaccAccountGetGUID (fixture->acc1);
+    g_assert_true (guid_equal (guid_acc1, guid));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-formula", &cf, NULL);
+    g_assert_cmpstr ("", ==, cf);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-numeric", &cn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_zero(), *cn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-formula", &df, NULL);
+    g_assert_cmpstr ("32.", ==, df);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-numeric", &dn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_abs(xaccSplitGetAmount (s_txn)), *dn));
+
+    // Second split
+    siter_new_txn = siter_new_txn->next;
+    siter_txn = siter_txn->next;
+
+    s_txn = siter_txn->data;
+    s_new_txn = siter_new_txn->data;
+
+    g_assert_cmpstr (xaccSplitGetMemo (s_new_txn), ==, xaccSplitGetMemo (s_txn));
+    g_assert_cmpstr (xaccSplitGetAction (s_new_txn), ==, xaccSplitGetAction (s_txn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-account", &guid, NULL);
+    const GncGUID* guid_acc2 = xaccAccountGetGUID (fixture->acc2);
+    g_assert_true (guid_equal (guid_acc2, guid));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-formula", &cf, NULL);
+    g_assert_cmpstr ("32.", ==, cf);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-credit-numeric", &cn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_abs(xaccSplitGetAmount (s_txn)), *cn));
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-formula", &df, NULL);
+    g_assert_cmpstr ("", ==, df);
+
+    qof_instance_get (QOF_INSTANCE (s_new_txn), "sx-debit-numeric", &dn, NULL);
+    g_assert_true (gnc_numeric_equal(gnc_numeric_zero(), *dn));
+
+    g_assert_null (siter_txn->next);
+    g_assert_null (siter_new_txn->next);
+
+    guid_free (guid);
+    g_free (cn);
+    g_free (dn);
+
+    gnc_float_txn_free (ft);
+}
+
+static void
+test_gnc_template_to_normal (TemplateFixture *fixture, gconstpointer pData)
+{
+    FloatingTxn *ft = NULL;
+
+    ft = gnc_txn_to_float_txn (fixture->template_txn, FALSE, TRUE);
+    g_assert_true (ft->m_txn == fixture->template_txn);
+
+    Transaction *new_txn = xaccMallocTransaction (fixture->book);
+    gnc_float_txn_to_txn (ft, new_txn, FALSE);
+
+    /* Check transaction fields */
+    g_assert_cmpstr (xaccTransGetDescription (fixture->template_txn), ==, xaccTransGetDescription (new_txn));
+    g_assert_cmpstr (xaccTransGetNotes (fixture->template_txn), ==, xaccTransGetNotes (new_txn));
+
+    SplitList *sl_template_txn = xaccTransGetSplitList (fixture->template_txn), *siter_template_txn;
+    SplitList *sl_new_txn = xaccTransGetSplitList (new_txn), *siter_new_txn;
+
+    /* Check split fields of first split */
+    Split *s_template_txn, *s_new_txn;
+
+    siter_template_txn = sl_template_txn;
+    siter_new_txn = sl_new_txn;
+
+    s_template_txn = siter_template_txn->data;
+    s_new_txn = siter_new_txn->data;
+
+    // First split
+    g_assert_cmpstr (xaccSplitGetMemo (s_template_txn), ==, xaccSplitGetMemo (s_new_txn));
+    g_assert_cmpstr (xaccSplitGetAction (s_template_txn), ==, xaccSplitGetAction (s_new_txn));
+
+    GncGUID* guid = NULL;
+    Account *template_split_account;
+    gnc_numeric *cn, *dn;
+
+    qof_instance_get (QOF_INSTANCE (s_template_txn), "sx-account", &guid, NULL);
+    template_split_account = xaccAccountLookup (guid, fixture->book);
+    g_assert_true (xaccSplitGetAccount (s_new_txn) == template_split_account);
+
+    qof_instance_get (QOF_INSTANCE (s_template_txn), "sx-credit-numeric", &cn, NULL);
+    g_assert_true (gnc_numeric_equal(xaccSplitGetAmount (s_new_txn), gnc_numeric_neg(*cn)));
+    g_assert_true (gnc_numeric_equal(xaccSplitGetValue  (s_new_txn), gnc_numeric_neg(*cn)));
+
+    // Second split
+    siter_new_txn = siter_new_txn->next;
+    siter_template_txn = siter_template_txn->next;
+
+    s_template_txn = siter_template_txn->data;
+    s_new_txn = siter_new_txn->data;
+
+    g_assert_cmpstr (xaccSplitGetMemo (s_template_txn), ==, xaccSplitGetMemo (s_new_txn));
+    g_assert_cmpstr (xaccSplitGetAction (s_template_txn), ==, xaccSplitGetAction (s_new_txn));
+
+    qof_instance_get (QOF_INSTANCE (s_template_txn), "sx-account", &guid, NULL);
+    template_split_account = xaccAccountLookup (guid, fixture->book);
+    g_assert_true (xaccSplitGetAccount (s_new_txn) == template_split_account);
+
+    qof_instance_get (QOF_INSTANCE (s_template_txn), "sx-debit-numeric", &dn, NULL);
+    g_assert_true (gnc_numeric_equal(xaccSplitGetAmount (s_new_txn), *dn));
+    g_assert_true (gnc_numeric_equal(xaccSplitGetValue  (s_new_txn), *dn));
+
+    g_assert_null (siter_template_txn->next);
+    g_assert_null (siter_new_txn->next);
+
+    guid_free (guid);
+    g_free (cn);
+    g_free (dn);
+
+    gnc_float_txn_free (ft);
+}
+
 void
 test_suite_split_register_copy_ops (void)
 {
@@ -633,4 +1054,7 @@ test_suite_split_register_copy_ops (void)
     GNC_TEST_ADD (suitename, "gnc float txn to txn swap commit", FlFixture, NULL, flsetup, test_gnc_float_txn_to_txn_swap_accounts_swap_commit, flteardown);
     GNC_TEST_ADD (suitename, "gnc float txn to txn swap nocommit", FlFixture, NULL, flsetup, test_gnc_float_txn_to_txn_swap_accounts_swap_nocommit, flteardown);
 
+    GNC_TEST_ADD (suitename, "gnc template transaction to template transaction", TemplateFixture, NULL, setup_template, test_gnc_template_to_template, teardown_template);
+    GNC_TEST_ADD (suitename, "gnc normal transaction to template transaction", TemplateFixture, NULL, setup_template, test_gnc_normal_to_template, teardown_template);
+    GNC_TEST_ADD (suitename, "gnc template transaction to normal transaction", TemplateFixture, NULL, setup_template, test_gnc_template_to_normal, teardown_template);
 }

commit 950f118ec95ecaef04c27c2cbd652d0761e47a36
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue Feb 3 15:58:56 2026 +0000

    Fix copying template transactions.
    
    Add functions to allow for copying template transactions.

diff --git a/gnucash/register/ledger-core/split-register-control.cpp b/gnucash/register/ledger-core/split-register-control.cpp
index d1e098e100..dfa0571b75 100644
--- a/gnucash/register/ledger-core/split-register-control.cpp
+++ b/gnucash/register/ledger-core/split-register-control.cpp
@@ -936,7 +936,10 @@ gnc_split_register_auto_completion (SplitRegister *reg,
                                          gnc_get_current_book ());
         g_assert (pending_trans == trans);
 
-        gnc_copy_trans_onto_trans (auto_trans, trans, FALSE, FALSE);
+        Account *template_account = xaccAccountLookup (&info->template_account,
+                                                       gnc_get_current_book ());
+
+        gnc_copy_trans_onto_trans (auto_trans, trans, FALSE, template_account, FALSE);
         /* if there is a doclink, let's clear it */
         if (xaccTransGetDocLink (auto_trans) != NULL)
             xaccTransSetDocLink (trans, "");
diff --git a/gnucash/register/ledger-core/split-register-copy-ops.c b/gnucash/register/ledger-core/split-register-copy-ops.c
index 11e783e530..7c88c84c2a 100644
--- a/gnucash/register/ledger-core/split-register-copy-ops.c
+++ b/gnucash/register/ledger-core/split-register-copy-ops.c
@@ -24,6 +24,8 @@
 
 #include "config.h"
 #include "split-register-copy-ops.h"
+#include "gnc-ui-util.h"
+#include "SX-book.h"
 
 /* accessors */
 Split *gnc_float_split_get_split (const FloatingSplit *fs)
@@ -138,11 +140,98 @@ void gnc_float_split_set_value (FloatingSplit *fs, const gnc_numeric value)
     fs->m_value = value;
 }
 
+static void
+gnc_float_template_sx_data_free (FloatingTemplateSxData *ftsd)
+{
+    g_return_if_fail (ftsd);
+
+    guid_free (ftsd->m_account_guid);
+    g_free (ftsd->m_credit_numeric);
+    g_free (ftsd->m_debit_numeric);
+
+    g_free (ftsd);
+}
+
+static FloatingTemplateSxData*
+gnc_split_to_float_template_sx_data (Split *split)
+{
+    FloatingTemplateSxData *ftsd;
+    GncGUID* guid = NULL;
+    const gchar *cf, *df, *shares;
+    gnc_numeric *cn, *dn;
+
+    g_return_val_if_fail (split, NULL);
+
+    ftsd = g_new0 (FloatingTemplateSxData, 1);
+
+    qof_instance_get (QOF_INSTANCE (split), "sx-account", &guid, NULL);
+    ftsd->m_account_guid = guid;
+
+    qof_instance_get (QOF_INSTANCE (split), "sx-credit-formula", &cf, NULL);
+    ftsd->m_credit_formula = cf;
+
+    qof_instance_get (QOF_INSTANCE (split), "sx-credit-numeric", &cn, NULL);
+    ftsd->m_credit_numeric = cn;
+
+    qof_instance_get (QOF_INSTANCE (split), "sx-debit-formula", &df, NULL);
+    ftsd->m_debit_formula = df;
+
+    qof_instance_get (QOF_INSTANCE (split), "sx-debit-numeric", &dn, NULL);
+    ftsd->m_debit_numeric = dn;
+
+    qof_instance_get (QOF_INSTANCE (split), "sx-shares", &shares, NULL);
+    ftsd->m_shares = shares;
+
+    return ftsd;
+}
+
+static void
+gnc_split_set_sx_settings (Split *split, const char *cf, gnc_numeric cn,
+                           const char *df, gnc_numeric dn)
+{
+    qof_instance_set (QOF_INSTANCE (split), "sx-credit-formula", cf, NULL);
+    qof_instance_set (QOF_INSTANCE (split), "sx-debit-formula", df, NULL);
+    qof_instance_set (QOF_INSTANCE (split), "sx-credit-numeric", &cn, NULL);
+    qof_instance_set (QOF_INSTANCE (split), "sx-debit-numeric", &dn, NULL);
+}
+
+static void
+gnc_float_template_sx_data_to_split (const FloatingSplit *fs, Split *split)
+{
+    g_return_if_fail (fs);
+    g_return_if_fail (split);
+
+    FloatingTemplateSxData *ftsd = fs->m_template_sx_data;
+
+    const gchar *cf = "", *df = "";
+    gnc_numeric cn = gnc_numeric_zero(), dn = gnc_numeric_zero();
+
+    if (ftsd->m_account_guid)
+        qof_instance_set (QOF_INSTANCE (split), "sx-account", ftsd->m_account_guid, NULL);
+
+    if (ftsd->m_credit_formula)
+        cf = ftsd->m_credit_formula;
+
+    if (ftsd->m_credit_numeric && !gnc_numeric_zero_p (*ftsd->m_credit_numeric))
+        cn = *ftsd->m_credit_numeric;
+
+    if (ftsd->m_debit_formula)
+        df = ftsd->m_debit_formula;
+
+    if (ftsd->m_debit_numeric && !gnc_numeric_zero_p (*ftsd->m_debit_numeric))
+        dn = *ftsd->m_debit_numeric;
+
+    gnc_split_set_sx_settings (split, cf, cn, df, dn);
+
+    if (ftsd->m_shares)
+        qof_instance_set (QOF_INSTANCE (split), "sx-shares", ftsd->m_shares, NULL);
+}
+
 /* This function takes a split and returns a representation
    of it as a floating_split structure. Assumes the transaction is open
    for editing.
 */
-FloatingSplit *gnc_split_to_float_split (Split *split)
+FloatingSplit *gnc_split_to_float_split (Split *split, gboolean is_template)
 {
     FloatingSplit *fs;
 
@@ -159,30 +248,101 @@ FloatingSplit *gnc_split_to_float_split (Split *split)
     fs->m_amount = xaccSplitGetAmount (split);
     fs->m_value = xaccSplitGetValue (split);
 
+    if (is_template)
+        fs->m_template_sx_data = gnc_split_to_float_template_sx_data (split);
+    else
+        fs->m_template_sx_data = NULL;
+
     return fs;
 }
 
+static void
+register_fs_to_template_split (const FloatingSplit *fs, Split *split)
+{
+    const GncGUID *guid = qof_instance_get_guid (QOF_INSTANCE (fs->m_account));
+    qof_instance_set (QOF_INSTANCE(split), "sx-account", guid, NULL);
+
+    char string[32];
+    gnc_commodity *acount_commodity =  xaccAccountGetCommodity (fs->m_account);
+    GNCPrintAmountInfo print_info = gnc_commodity_print_info (acount_commodity, FALSE);
+    gnc_numeric zero = gnc_numeric_zero ();
+    gnc_numeric abs_diff = gnc_numeric_abs (fs->m_amount);
+    xaccSPrintAmount (string, abs_diff, print_info);
+
+    if (gnc_numeric_negative_p (fs->m_amount))
+        gnc_split_set_sx_settings (split, string, abs_diff, "", zero);
+
+    if (!gnc_numeric_negative_p (fs->m_amount))
+        gnc_split_set_sx_settings (split, "", zero, string, abs_diff);
+}
+
+static Account*
+template_fs_to_register_split (const FloatingSplit *fs, Split *split, Account *split_account)
+{
+    FloatingTemplateSxData *ftsd = fs->m_template_sx_data;
+
+    if (ftsd->m_credit_numeric && !gnc_numeric_zero_p (*ftsd->m_credit_numeric))
+    {
+        xaccSplitSetAmount (split, gnc_numeric_neg(*ftsd->m_credit_numeric));
+        xaccSplitSetValue (split, gnc_numeric_neg(*ftsd->m_credit_numeric));
+    }
+
+    if (ftsd->m_debit_numeric && !gnc_numeric_zero_p (*ftsd->m_debit_numeric))
+    {
+        xaccSplitSetAmount (split, *ftsd->m_debit_numeric);
+        xaccSplitSetValue (split, *ftsd->m_debit_numeric);
+    }
+
+    if (ftsd->m_account_guid)
+        split_account = xaccAccountLookup (ftsd->m_account_guid,
+                                           qof_instance_get_book (QOF_INSTANCE(split)));
+
+    return split_account;
+}
+
 /* Copy a temporary split representation onto a real split.
    If possible, insert the split into the account of the
    split representation. Not all values are copied. The reconcile
    status and date are not copied. The split's guid is,
    of course, unchanged.
 */
-void gnc_float_split_to_split (const FloatingSplit *fs, Split *split)
+void
+gnc_float_split_to_split (const FloatingSplit *fs, Split *split, Account *template_account)
 {
-    g_return_if_fail(split);
+    g_return_if_fail (split);
+
+    Account *split_account = fs->m_account;
 
     if (fs->m_memo)
         xaccSplitSetMemo (split, fs->m_memo);
     if (fs->m_action)
         xaccSplitSetAction (split, fs->m_action);
-    xaccSplitSetAmount (split, fs->m_amount);
-    xaccSplitSetValue (split, fs->m_value);
-    if (fs->m_account)
+
+    if (template_account && fs->m_template_sx_data) // From Sched to Sched
     {
-        xaccAccountBeginEdit (fs->m_account);
-        xaccSplitSetAccount (split, fs->m_account);
-        xaccAccountCommitEdit (fs->m_account);
+        split_account = template_account;
+        gnc_float_template_sx_data_to_split (fs, split);
+    }
+    else if (template_account && !fs->m_template_sx_data) // From Reg to Sched
+    {
+        split_account = template_account;
+        register_fs_to_template_split (fs, split);
+    }
+    else if (!template_account && fs->m_template_sx_data) // From Sched to Reg
+    {
+        split_account = template_fs_to_register_split (fs, split, split_account);
+    }
+    else // From Reg to Reg
+    {
+        xaccSplitSetAmount (split, fs->m_amount);
+        xaccSplitSetValue (split, fs->m_value);
+    }
+
+    if (split_account)
+    {
+        xaccAccountBeginEdit (split_account);
+        xaccSplitSetAccount (split, split_account);
+        xaccAccountCommitEdit (split_account);
     }
 }
 
@@ -192,6 +352,10 @@ void gnc_float_split_free (FloatingSplit *fs)
 
     CACHE_REMOVE (fs->m_memo);
     CACHE_REMOVE (fs->m_action);
+
+    if (fs->m_template_sx_data)
+        gnc_float_template_sx_data_free (fs->m_template_sx_data);
+
     g_free (fs);
 }
 
@@ -336,7 +500,7 @@ void gnc_float_txn_append_float_split (FloatingTxn *ft, FloatingSplit *fs)
 
 /* This function takes a C transaction and returns
    a representation of it as a floating_txn. */
-FloatingTxn *gnc_txn_to_float_txn (Transaction *txn, gboolean use_cut_semantics)
+FloatingTxn *gnc_txn_to_float_txn (Transaction *txn, gboolean use_cut_semantics, gboolean is_template)
 {
     GList *iter;
 
@@ -359,7 +523,7 @@ FloatingTxn *gnc_txn_to_float_txn (Transaction *txn, gboolean use_cut_semantics)
         Split *split = iter->data;
         if (split && xaccTransStillHasSplit (txn, split))
         {
-            FloatingSplit *fs = gnc_split_to_float_split (split);
+            FloatingSplit *fs = gnc_split_to_float_split (split, is_template);
             ft->m_splits = g_list_prepend (ft->m_splits, fs);
         }
     }
@@ -368,6 +532,73 @@ FloatingTxn *gnc_txn_to_float_txn (Transaction *txn, gboolean use_cut_semantics)
     return ft;
 }
 
+gboolean
+gnc_float_txn_has_template (const FloatingTxn *ft)
+{
+    GList *iter;
+    gboolean ftsd_exists = FALSE;
+
+    g_return_val_if_fail (ft, FALSE);
+
+    for (iter = ft->m_splits; iter; iter = iter->next)
+    {
+        FloatingSplit *fs = iter->data;
+        if (!fs)
+            continue;
+        if (fs->m_template_sx_data)
+            ftsd_exists = TRUE;
+    }
+    return ftsd_exists;
+}
+
+void
+gnc_float_txn_to_template_txn (const FloatingTxn *ft, Transaction *txn, Account *template_account, gboolean do_commit)
+{
+    GList *iter;
+
+    g_return_if_fail (ft);
+    g_return_if_fail (txn);
+
+    if (!xaccTransIsOpen (txn))
+        xaccTransBeginEdit (txn);
+
+    if (ft->m_currency)
+        xaccTransSetCurrency (txn, ft->m_currency);
+    if (ft->m_description)
+        xaccTransSetDescription (txn, ft->m_description);
+    if (ft->m_num)
+        xaccTransSetNum (txn, ft->m_num);
+    if (ft->m_notes)
+        xaccTransSetNotes (txn, ft->m_notes);
+    if (ft->m_doclink)
+        xaccTransSetDocLink (txn, ft->m_doclink);
+    if (ft->m_date_posted)
+        xaccTransSetDatePostedSecs (txn, ft->m_date_posted);
+
+    /* strip off the old splits */
+    xaccTransClearSplits(txn);
+
+    /* and put on the new ones! Please note they go in the *same*
+       order as in the original transaction. This is important. */
+    for (iter = ft->m_splits; iter; iter = iter->next)
+    {
+        FloatingSplit *fs = iter->data;
+        if (!fs)
+            continue;
+
+        Split *split = xaccMallocSplit (xaccTransGetBook (txn));
+
+        gnc_float_split_to_split (fs, split, template_account);
+
+        xaccSplitSetParent (split, txn);
+    }
+
+    /* close the transaction */
+    if (do_commit)
+        xaccTransCommitEdit (txn);
+}
+
+
 void gnc_float_txn_to_txn (const FloatingTxn *ft, Transaction *txn, gboolean do_commit)
 {
     gnc_float_txn_to_txn_swap_accounts (ft, txn, NULL, NULL, do_commit);
@@ -376,7 +607,9 @@ void gnc_float_txn_to_txn (const FloatingTxn *ft, Transaction *txn, gboolean do_
 /* Copy a temporary representation of a transaction onto a real transaction.
  I f they exist the two account*s (acct1 and acct2) are used to swap accounts
  when when creating splits. */
-void gnc_float_txn_to_txn_swap_accounts (const FloatingTxn *ft, Transaction *txn, Account *acct1, Account *acct2, gboolean do_commit)
+void gnc_float_txn_to_txn_swap_accounts (const FloatingTxn *ft, Transaction *txn,
+                                         Account *acct1, Account *acct2,
+                                         gboolean do_commit)
 {
     GList *iter;
 
@@ -401,7 +634,7 @@ void gnc_float_txn_to_txn_swap_accounts (const FloatingTxn *ft, Transaction *txn
 
     /* strip off the old splits */
     xaccTransClearSplits(txn);
-    
+
     /* and put on the new ones! Please note they go in the *same*
        order as in the original transaction. This is important. */
     for (iter = ft->m_splits; iter; iter = iter->next)
@@ -423,7 +656,8 @@ void gnc_float_txn_to_txn_swap_accounts (const FloatingTxn *ft, Transaction *txn
             new_acc = fs->m_account;
 
         fs->m_account = new_acc;
-        gnc_float_split_to_split (fs, split);
+
+        gnc_float_split_to_split (fs, split, NULL);
         fs->m_account = old_acc;
         xaccSplitSetParent (split, txn);
     }
diff --git a/gnucash/register/ledger-core/split-register-copy-ops.h b/gnucash/register/ledger-core/split-register-copy-ops.h
index 707353136a..a3cae8ca79 100644
--- a/gnucash/register/ledger-core/split-register-copy-ops.h
+++ b/gnucash/register/ledger-core/split-register-copy-ops.h
@@ -33,6 +33,17 @@
 #include "Account.h"
 #include "Transaction.h"
 
+typedef struct
+{
+    GncGUID      *m_account_guid;
+    const char   *m_credit_formula;
+    const char   *m_debit_formula;
+    gnc_numeric  *m_credit_numeric;
+    gnc_numeric  *m_debit_numeric;
+    const char   *m_shares;
+
+} FloatingTemplateSxData;
+
 typedef struct
 {
     Split       *m_split;
@@ -44,6 +55,9 @@ typedef struct
     char         m_reconcile_state;
     gnc_numeric  m_value;
     gnc_numeric  m_amount;
+
+    FloatingTemplateSxData *m_template_sx_data;
+
 } FloatingSplit;
 
 typedef struct
@@ -81,8 +95,8 @@ void gnc_float_split_set_reconcile_date (FloatingSplit *fs, time64 reconcile_dat
 void gnc_float_split_set_amount (FloatingSplit *fs, gnc_numeric amount);
 void gnc_float_split_set_value (FloatingSplit *fs, gnc_numeric value);
 
-FloatingSplit *gnc_split_to_float_split (Split *split);
-void gnc_float_split_to_split (const FloatingSplit *fs, Split *split);
+FloatingSplit *gnc_split_to_float_split (Split *split, gboolean is_template);
+void gnc_float_split_to_split (const FloatingSplit *fs, Split *split, Account *template_account);
 
 void gnc_float_split_free (FloatingSplit *fs);
 
@@ -113,10 +127,17 @@ void gnc_float_txn_set_splits (FloatingTxn *ft, SplitList *splits);
 
 void gnc_float_txn_append_float_split (FloatingTxn *ft, FloatingSplit *fs);
 
-FloatingTxn *gnc_txn_to_float_txn (Transaction *txn, gboolean use_cut_semantics);
+gboolean gnc_float_txn_has_template (const FloatingTxn *ft);
+
+FloatingTxn *gnc_txn_to_float_txn (Transaction *txn, gboolean use_cut_semantics, gboolean is_template);
+
+void gnc_float_txn_to_template_txn (const FloatingTxn *ft, Transaction *txn,
+                                    Account *template_account, gboolean do_commit);
 
 void gnc_float_txn_to_txn (const FloatingTxn *ft, Transaction *txn, gboolean do_commit);
-void gnc_float_txn_to_txn_swap_accounts (const FloatingTxn *ft, Transaction *txn, Account *acct1, Account *acct2, gboolean do_commit);
+void gnc_float_txn_to_txn_swap_accounts (const FloatingTxn *ft, Transaction *txn,
+                                         Account *acct1, Account *acct2,
+                                         gboolean do_commit);
 
 void gnc_float_txn_free (FloatingTxn *ft);
 
diff --git a/gnucash/register/ledger-core/split-register.c b/gnucash/register/ledger-core/split-register.c
index d5c9959647..2d034e67d1 100644
--- a/gnucash/register/ledger-core/split-register.c
+++ b/gnucash/register/ledger-core/split-register.c
@@ -106,36 +106,51 @@ clear_copied_item()
 }
 
 static void
-gnc_copy_split_onto_split (Split* from, Split* to, gboolean use_cut_semantics)
+gnc_copy_split_onto_split (Split* from, Split* to,
+                           Account *template_account,
+                           gboolean use_cut_semantics)
 {
     FloatingSplit *fs;
+    gboolean is_template = FALSE;
 
     if ((from == NULL) || (to == NULL))
         return;
 
-    fs = gnc_split_to_float_split (from);
+    if (template_account)
+        is_template = TRUE;
+
+    fs = gnc_split_to_float_split (from, is_template);
     if (!fs)
         return;
 
-    gnc_float_split_to_split (fs, to);
+    gnc_float_split_to_split (fs, to, template_account);
     gnc_float_split_free (fs);
 }
 
 void
 gnc_copy_trans_onto_trans (Transaction* from, Transaction* to,
                            gboolean use_cut_semantics,
+                           Account *template_account,
                            gboolean do_commit)
 {
     FloatingTxn *ft;
+    gboolean is_template = FALSE;
 
     if ((from == NULL) || (to == NULL))
         return;
 
-    ft = gnc_txn_to_float_txn (from, use_cut_semantics);
+    if (template_account)
+        is_template = TRUE;
+
+    ft = gnc_txn_to_float_txn (from, use_cut_semantics, is_template);
     if (!ft)
         return;
 
-    gnc_float_txn_to_txn (ft, to, do_commit);
+    if (is_template)
+        gnc_float_txn_to_template_txn (ft, to, template_account, do_commit);
+    else
+        gnc_float_txn_to_txn (ft, to, do_commit);
+
     gnc_float_txn_free (ft);
 }
 
@@ -555,7 +570,11 @@ gnc_split_register_duplicate_current (SplitRegister* reg)
 
         xaccTransBeginEdit (trans);
         xaccSplitSetParent (new_split, trans);
-        gnc_copy_split_onto_split (split, new_split, FALSE);
+
+        Account *template_account = xaccAccountLookup (&info->template_account,
+                                                       gnc_get_current_book ());
+
+        gnc_copy_split_onto_split (split, new_split, template_account, FALSE);
         if (new_act_num) /* if new number supplied by user dialog */
             gnc_set_num_action (NULL, new_split, out_num, NULL);
 
@@ -668,7 +687,11 @@ gnc_split_register_duplicate_current (SplitRegister* reg)
         new_trans = xaccMallocTransaction (gnc_get_current_book ());
 
         xaccTransBeginEdit (new_trans);
-        gnc_copy_trans_onto_trans (trans, new_trans, FALSE, FALSE);
+
+        Account *template_account = xaccAccountLookup (&info->template_account,
+                                                       gnc_get_current_book ());
+
+        gnc_copy_trans_onto_trans (trans, new_trans, FALSE, template_account, FALSE);
         xaccTransSetDatePostedSecsNormalized (new_trans, date);
         /* We also must set a new DateEntered on the new entry
          * because otherwise the ordering is not deterministic */
@@ -796,7 +819,7 @@ gnc_split_register_copy_current_internal (SplitRegister* reg,
     if (cursor_class == CURSOR_CLASS_SPLIT)
     {
         /* We are on a split in an expanded transaction. Just copy the split. */
-        new_fs = gnc_split_to_float_split (split);
+        new_fs = gnc_split_to_float_split (split, reg->is_template);
 
         if (new_fs)
         {
@@ -810,7 +833,7 @@ gnc_split_register_copy_current_internal (SplitRegister* reg,
     else
     {
         /* We are on a transaction row. Copy the whole transaction. */
-        new_ft = gnc_txn_to_float_txn (trans, use_cut_semantics);
+        new_ft = gnc_txn_to_float_txn (trans, use_cut_semantics, reg->is_template);
 
         if (new_ft)
         {
@@ -917,6 +940,7 @@ gnc_split_register_paste_current (SplitRegister* reg)
     Split* blank_split;
     Split* trans_split;
     Split* split;
+    Account *template_account = NULL;
 
     ENTER ("reg=%p", reg);
 
@@ -934,6 +958,9 @@ gnc_split_register_paste_current (SplitRegister* reg)
 
     trans_split = gnc_split_register_get_current_trans_split (reg, NULL);
 
+    template_account = xaccAccountLookup (&info->template_account,
+                                          gnc_get_current_book ());
+
     /* This shouldn't happen, but be paranoid. */
     if (trans == NULL)
     {
@@ -1015,8 +1042,7 @@ gnc_split_register_paste_current (SplitRegister* reg)
             LEAVE ("copy buffer doesn't represent a split");
             return;
         }
-
-        gnc_float_split_to_split (copied_item.fs, split);
+        gnc_float_split_to_split (copied_item.fs, split, template_account);
     }
     else
     {
@@ -1035,13 +1061,25 @@ gnc_split_register_paste_current (SplitRegister* reg)
             return;
         }
 
-
         if (copied_item.ftype != GNC_TYPE_TRANSACTION)
         {
             LEAVE ("copy buffer doesn't represent a transaction");
             return;
         }
 
+        if ((reg->type != SEARCH_LEDGER) && (reg->type != GENERAL_JOURNAL))
+        {
+            if (gnc_float_txn_has_template (copied_item.ft))
+            {
+                const gchar *msg_text = _("Scheduled transactions can only be pasted to the General Journal");
+
+                gnc_warning_dialog (GTK_WINDOW (gnc_split_register_get_parent (reg)), "%s", msg_text);
+
+                LEAVE ("Paste only allowed to General Journal from scheduled transactions");
+                return;
+            }
+        }
+
         /* Ask before overwriting an existing transaction. */
         if (split != blank_split &&
             !gnc_verify_dialog (GTK_WINDOW (gnc_split_register_get_parent (reg)),
@@ -1069,15 +1107,24 @@ gnc_split_register_paste_current (SplitRegister* reg)
         copied_leader = xaccAccountLookup (&copied_item.leader_guid,
                                            gnc_get_current_book ());
         default_account = gnc_split_register_get_default_account (reg);
+
         if (copied_leader && default_account)
         {
             gnc_float_txn_to_txn_swap_accounts (copied_item.ft, trans,
                                                 copied_leader,
-                                                default_account, FALSE);
+                                                default_account,
+                                                FALSE);
         }
         else
-            gnc_float_txn_to_txn (copied_item.ft, trans, FALSE);
-
+        {
+            if (reg->is_template)
+            {
+                gnc_float_txn_to_template_txn (copied_item.ft, trans,
+                                               template_account, FALSE);
+            }
+            else
+                gnc_float_txn_to_txn (copied_item.ft, trans, FALSE);
+        }
         num_splits = xaccTransCountSplits (trans);
         if (split_index >= num_splits)
             split_index = 0;
@@ -1619,7 +1666,7 @@ gnc_split_register_save_to_copy_buffer (SplitRegister *reg,
                 Split* temp_split;
 
                 temp_split = xaccMallocSplit (gnc_get_current_book ());
-                other_fs = gnc_split_to_float_split (temp_split);
+                other_fs = gnc_split_to_float_split (temp_split, reg->is_template);
                 xaccSplitDestroy (temp_split);
 
                 gnc_float_txn_append_float_split (ft, other_fs);
diff --git a/gnucash/register/ledger-core/split-register.h b/gnucash/register/ledger-core/split-register.h
index 99b2a79cd8..36d2bf6491 100644
--- a/gnucash/register/ledger-core/split-register.h
+++ b/gnucash/register/ledger-core/split-register.h
@@ -607,6 +607,7 @@ gboolean gnc_split_register_full_refresh_ok (SplitRegister* reg);
 /** Private function -- outsiders must not use this */
 void gnc_copy_trans_onto_trans (Transaction* from, Transaction* to,
                                 gboolean use_cut_semantics,
+                                Account *template_account,
                                 gboolean do_commit);
 
 #endif

commit 09f33eb4fdf9bf3e14dc681aac2af12dfc5294ea
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue Feb 3 14:36:54 2026 +0000

    Fix only checking first scheduled transaction
    
    Fixes only testing the first transaction by changing the return value
    of the 'TransactionCallback' function for xaccAccountForEachTransaction
    to FALSE.

diff --git a/gnucash/gnome/dialog-sx-editor.c b/gnucash/gnome/dialog-sx-editor.c
index 39fddeea83..7e2bc00e1e 100644
--- a/gnucash/gnome/dialog-sx-editor.c
+++ b/gnucash/gnome/dialog-sx-editor.c
@@ -792,7 +792,7 @@ check_transaction_splits (Transaction *txn, gpointer data)
             return FALSE;
         }
     }
-    return TRUE;
+    return FALSE; // return FALSE to continue to next transaction
 }
 
 /*******************************************************************************

commit 5df0a083a583867f6b7c78c7df1b1430f53afae8
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue Feb 3 14:33:55 2026 +0000

    Bug 798122 - Message about unable to balance transaction
    
    It was suggested that when the scheduled transactions are tested for
    being in balance and they are not the discrepancy could be displayed to
    aid resolution.

diff --git a/gnucash/gnome/dialog-sx-editor.c b/gnucash/gnome/dialog-sx-editor.c
index 4857929463..39fddeea83 100644
--- a/gnucash/gnome/dialog-sx-editor.c
+++ b/gnucash/gnome/dialog-sx-editor.c
@@ -437,6 +437,9 @@ typedef struct _txnCreditDebitSums
 {
     gnc_numeric    creditSum;
     gnc_numeric    debitSum;
+    gnc_commodity *base_cmdty;
+    GtkWindow     *window;
+    gboolean       multi_commodity;
 } txnCreditDebitSums;
 
 static txnCreditDebitSums *
@@ -444,6 +447,9 @@ tcds_new (void)
 {
     txnCreditDebitSums *tcds = g_new0 (txnCreditDebitSums, 1);
     tcds->creditSum = tcds->debitSum = gnc_numeric_zero ();
+    tcds->base_cmdty = NULL;
+    tcds->window = NULL;
+    tcds->multi_commodity = FALSE;
     return tcds;
 }
 
@@ -455,6 +461,8 @@ set_sums_to_zero (gpointer key,
     txnCreditDebitSums *tcds = (txnCreditDebitSums*)val;
     tcds->creditSum = gnc_numeric_zero ();
     tcds->debitSum  = gnc_numeric_zero ();
+    tcds->base_cmdty = NULL;
+    tcds->multi_commodity = FALSE;
 }
 
 inline static gnc_numeric
@@ -467,6 +475,7 @@ static void
 check_credit_debit_balance (gpointer key, gpointer val, gpointer ud)
 {
     txnCreditDebitSums *tcds = (txnCreditDebitSums*)val;
+    Transaction *txn = GNC_TRANSACTION(key);
     gboolean *unbalanced = (gboolean*)ud;
     gnc_numeric diff = tcds_difference (tcds);
     const char *result = gnc_numeric_zero_p (diff) ? "true" : "false";
@@ -476,6 +485,21 @@ check_credit_debit_balance (gpointer key, gpointer val, gpointer ud)
            gnc_num_dbg_to_string (tcds->debitSum),
            gnc_num_dbg_to_string (tcds->creditSum),
            gnc_num_dbg_to_string (diff));
+
+    if (!gnc_numeric_zero_p (diff) && !tcds->multi_commodity)
+    {
+        char string[32];
+        gchar *msg_text;
+        const gchar *desc = xaccTransGetDescription (txn);
+        GNCPrintAmountInfo print_info = gnc_commodity_print_info (tcds->base_cmdty, TRUE);
+        gnc_numeric abs_diff = gnc_numeric_abs (diff);
+        xaccSPrintAmount (string, abs_diff, print_info);
+        msg_text = g_strdup_printf (_("Transaction with description '%s' can not be balanced.\n"
+                                      "The difference is %s"), desc, string);
+
+        gnc_warning_dialog (tcds->window, "%s", msg_text);
+        g_free (msg_text);
+    }
 }
 
 static gboolean
@@ -622,29 +646,23 @@ gnc_sxed_check_autocreate (GncSxEditorDialog *sxed, int ttVarCount,
 }
 
 static gboolean
-gnc_sxed_split_check_account (GncSxEditorDialog *sxed, Split *s,
-                              gnc_commodity *base_cmdty, gboolean *multi_cmdty)
+gnc_sxed_split_check_account (GncSxEditorDialog *sxed, Split *s, txnCreditDebitSums *tcds)
 {
-    gnc_commodity *split_cmdty = NULL;
-    gnc_numeric split_amount;
-    Account *acct = NULL;
     GncGUID *acct_guid = NULL;
-    qof_instance_get (QOF_INSTANCE (s),
-                      "sx-account", &acct_guid,
-                      NULL);
-    acct = xaccAccountLookup (acct_guid, gnc_get_current_book ());
+    qof_instance_get (QOF_INSTANCE (s), "sx-account", &acct_guid, NULL);
+    Account *acct = xaccAccountLookup (acct_guid, gnc_get_current_book ());
     guid_free (acct_guid);
     // If the split is being destroyed always return TRUE.
     if (acct == NULL && !qof_instance_get_destroying (s))
         return FALSE;
-    split_cmdty = xaccAccountGetCommodity (acct);
-    split_amount = xaccSplitGetAmount (s);
-    if (!gnc_numeric_zero_p (split_amount) && base_cmdty == NULL)
-    {
-        base_cmdty = split_cmdty;
-    }
-    *multi_cmdty |= (!gnc_numeric_zero_p (split_amount) &&
-                    !gnc_commodity_equal (split_cmdty, base_cmdty));
+
+    gnc_commodity *split_cmdty = xaccAccountGetCommodity (acct);
+
+    if (!tcds->base_cmdty)
+        tcds->base_cmdty = split_cmdty;
+
+    tcds->multi_commodity |= !gnc_commodity_equal (split_cmdty, tcds->base_cmdty);
+
     return TRUE;
 }
 
@@ -719,17 +737,16 @@ check_transaction_splits (Transaction *txn, gpointer data)
 
     for (; splitList; splitList = splitList->next)
     {
-        gnc_commodity *base_cmdty = NULL;
         Split *s = (Split*)splitList->data;
 
-        if (sd->tcds == NULL)
+        if (g_hash_table_lookup (sd->txns, (gpointer)txn) == NULL)
         {
             sd->tcds = tcds_new ();
+            sd->tcds->window = GTK_WINDOW(sd->sxed->dialog);
             g_hash_table_insert (sd->txns, (gpointer)txn, (gpointer)(sd->tcds));
         }
 
-        if (!gnc_sxed_split_check_account (sd->sxed, s, base_cmdty,
-                                           &sd->multi_commodity))
+        if (!gnc_sxed_split_check_account (sd->sxed, s, sd->tcds))
         {
             gchar *message = g_strdup_printf
                 (_("Split with memo %s has an invalid account."),
@@ -742,6 +759,8 @@ check_transaction_splits (Transaction *txn, gpointer data)
             return FALSE;
         }
 
+        sd->multi_commodity |= sd->tcds->multi_commodity;
+
         if (!gnc_sxed_split_calculate_formula (sd->sxed, s, sd->vars,
                                                "sx-credit-formula",
                                                sd->tcds))
@@ -870,8 +889,9 @@ gnc_sxed_check_consistent (GncSxEditorDialog *sxed)
     if (unbalanceable)
     {
         const char *msg =
-            _("The Scheduled Transaction Editor cannot automatically "
-              "balance this transaction. Should it still be entered?");
+            _("The Scheduled Transaction Editor cannot automatically balance "
+              "all of the transactions in this this Scheduled Transaction.\n"
+              "Should it still be entered?");
         if (!gnc_verify_dialog (GTK_WINDOW (sxed->dialog), FALSE, "%s", msg))
             return FALSE;
     }

commit b037d7dd7defbf2f2277ff6cbc09dd3dceb7ed6a
Author: Robert Fewell <14uBobIT at gmail.com>
Date:   Tue Feb 3 14:18:46 2026 +0000

    Realign a couple of structures in dialog-sx-editor.c

diff --git a/gnucash/gnome/dialog-sx-editor.c b/gnucash/gnome/dialog-sx-editor.c
index 7d7e809862..4857929463 100644
--- a/gnucash/gnome/dialog-sx-editor.c
+++ b/gnucash/gnome/dialog-sx-editor.c
@@ -101,9 +101,9 @@ typedef enum _EndTypeEnum
 
 struct _GncSxEditorDialog
 {
-    GtkWidget *dialog;
-    GtkBuilder *builder;
-    GtkNotebook *notebook;
+    GtkWidget    *dialog;
+    GtkBuilder   *builder;
+    GtkNotebook  *notebook;
     SchedXaction *sx;
     /* If this is a new scheduled transaction or not. */
     int newsxP;
@@ -111,34 +111,33 @@ struct _GncSxEditorDialog
     /* The various widgets in the dialog */
     GNCLedgerDisplay *ledger;
 
-    GncFrequency *gncfreq;
+    GncFrequency     *gncfreq;
     GncDenseCalStore *dense_cal_model;
-    GncDenseCal *example_cal;
-
-    GtkEntry *nameEntry;
+    GncDenseCal      *example_cal;
 
-    GtkLabel *lastOccurLabel;
+    GtkEntry        *nameEntry;
+    GtkLabel        *lastOccurLabel;
 
     GtkToggleButton *enabledOpt;
     GtkToggleButton *autocreateOpt;
     GtkToggleButton *notifyOpt;
     GtkToggleButton *advanceOpt;
-    GtkSpinButton *advanceSpin;
+    GtkSpinButton   *advanceSpin;
     GtkToggleButton *remindOpt;
-    GtkSpinButton *remindSpin;
+    GtkSpinButton   *remindSpin;
 
     GtkToggleButton *optEndDate;
     GtkToggleButton *optEndNone;
     GtkToggleButton *optEndCount;
-    EndType end_type;
-    GtkEntry *endCountSpin;
-    GtkEntry *endRemainSpin;
-    GNCDateEdit *endDateEntry;
+    EndType          end_type;
+    GtkEntry        *endCountSpin;
+    GtkEntry        *endRemainSpin;
+    GNCDateEdit     *endDateEntry;
 
-    char *sxGUIDstr;
+    char            *sxGUIDstr;
 
     GncEmbeddedWindow *embed_window;
-    GncPluginPage *plugin_page;
+    GncPluginPage     *plugin_page;
 };
 
 /** Prototypes **********************************************************/
@@ -436,8 +435,8 @@ gnc_sxed_check_changed (GncSxEditorDialog *sxed)
  ****************************************************************************/
 typedef struct _txnCreditDebitSums
 {
-    gnc_numeric creditSum;
-    gnc_numeric debitSum;
+    gnc_numeric    creditSum;
+    gnc_numeric    debitSum;
 } txnCreditDebitSums;
 
 static txnCreditDebitSums *



Summary of changes:
 gnucash/gnome/dialog-sx-editor.c                   | 103 +++--
 gnucash/gnome/gnc-plugin-page-register.cpp         |  18 +
 .../ledger-core/split-register-control.cpp         |   5 +-
 .../register/ledger-core/split-register-copy-ops.c | 262 +++++++++++-
 .../register/ledger-core/split-register-copy-ops.h |  29 +-
 gnucash/register/ledger-core/split-register.c      |  88 ++++-
 gnucash/register/ledger-core/split-register.h      |   5 +
 .../test/utest-split-register-copy-ops.c           | 438 ++++++++++++++++++++-
 8 files changed, 863 insertions(+), 85 deletions(-)



More information about the gnucash-changes mailing list