gnucash stable: Multiple changes pushed

John Ralls jralls at code.gnucash.org
Tue Jun 23 18:45:27 EDT 2026


Updated	 via  https://github.com/Gnucash/gnucash/commit/7f4e8c29 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/3e98c5d8 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/222714d7 (commit)
	from  https://github.com/Gnucash/gnucash/commit/32a6868f (commit)



commit 7f4e8c29e06d21c33a511e2202dcbb3504ad16fa
Merge: 32a6868f52 3e98c5d8c8
Author: John Ralls <jralls at ceridwen.us>
Date:   Tue Jun 23 14:55:41 2026 -0700

    Merge Noah Noerr's 'online-id-engine-accessors' into stable.


commit 3e98c5d8c8aa9a9556ba595c8d6c80970e145d50
Author: Noah R <Noerr at users.noreply.github.com>
Date:   Sun Jun 21 16:39:24 2026 -0700

    [engine] Add first-class online_id accessors for Split and Account
    
    Promote the OFX/HBCI online_id to named engine accessors:
      xaccSplitGetOnlineID / SetOnlineID / HasOnlineID
      xaccAccountGetOnlineID / SetOnlineID
    The getters return an instance-owned const char* (mirroring
    xaccTransGetDocLink / xaccTransGetNotes), so the existing
    add_methods_with_prefix auto-wrapper exposes them in the Python bindings
    with no .i changes and no %newobject.  They write to the same engine KVP
    slot ("online_id") the desktop importer uses, so there is no data or
    behavior change.
    
    With the accessors in place the gnc_import_*_online_id wrappers in
    import-utilities are redundant, so replace every call site (OFX,
    AqBanking, and the generic matcher/backend) with them, delete
    import-utilities.cpp, and drop the online_id declarations from
    import-utilities.h (its importer preference-key macros are retained).
    
    - The engine getters return an instance-owned const char* instead of a
      g_strdup'd copy, so callers no longer free the result; the affected
      locals are retyped const and their g_free()s dropped.
      hash_account_online_ids() g_strdups before inserting, since its hash
      table owns its keys (g_free key-destructor).
    - xaccAccountSetOnlineID(acc, "") clears the slot, matching the OFX
      "delete the online_id" intent (the old wrapper stored an empty string).
    - Drop the two unused wrappers: gnc_import_set_trans_online_id (marked
      "Not actually used") and gnc_import_trans_has_online_id (no callers).
    - Add xaccSplitGet/SetOnlineID to the Split gmock so test-import-backend
      links without import-utilities.

diff --git a/bindings/python/tests/test_account.py b/bindings/python/tests/test_account.py
index 7c1ab96668..008916d5c8 100644
--- a/bindings/python/tests/test_account.py
+++ b/bindings/python/tests/test_account.py
@@ -17,6 +17,15 @@ class TestAccount(AccountSession):
         self.account.SetName(NAME)
         self.assertEqual( NAME, self.account.GetName() )
 
+    def test_online_id(self):
+        ONLINE_ID = "061000104:0123456789:CHECKING"
+        self.assertEqual( None, self.account.GetOnlineID() )
+        self.account.SetOnlineID(ONLINE_ID)
+        self.assertEqual( ONLINE_ID, self.account.GetOnlineID() )
+        # Passing None (or "") clears it.
+        self.account.SetOnlineID(None)
+        self.assertEqual( None, self.account.GetOnlineID() )
+
     def test_split(self):
         SPLIT = Split(self.book)
         self.assertTrue(self.account.insert_split(SPLIT))
diff --git a/bindings/python/tests/test_split.py b/bindings/python/tests/test_split.py
index 7358d37761..de8d1b2ca9 100644
--- a/bindings/python/tests/test_split.py
+++ b/bindings/python/tests/test_split.py
@@ -21,6 +21,19 @@ class TestSplit(SplitSession):
         self.split.SetMemo(MEMO)
         self.assertEqual( MEMO, self.split.GetMemo() )
 
+    def test_online_id(self):
+        FITID = "20240131-0001-1234567890"
+        # Unset online_id reads back as None (no KVP slot present).
+        self.assertEqual( None, self.split.GetOnlineID() )
+        self.assertFalse( self.split.HasOnlineID() )
+        self.split.SetOnlineID(FITID)
+        self.assertEqual( FITID, self.split.GetOnlineID() )
+        self.assertTrue( self.split.HasOnlineID() )
+        # Passing "" (or None) clears it.
+        self.split.SetOnlineID("")
+        self.assertEqual( None, self.split.GetOnlineID() )
+        self.assertFalse( self.split.HasOnlineID() )
+
     def test_account(self):
         ACCT = Account(self.book)
         ACCT.SetCommodity(self.currency)
diff --git a/gnucash/import-export/CMakeLists.txt b/gnucash/import-export/CMakeLists.txt
index 3765468acd..b7b7cf0ea7 100644
--- a/gnucash/import-export/CMakeLists.txt
+++ b/gnucash/import-export/CMakeLists.txt
@@ -20,7 +20,6 @@ set (generic_import_SOURCES
   import-format-dialog.cpp
   import-match-picker.cpp
   import-parse.cpp
-  import-utilities.cpp
   import-settings.cpp
   import-main-matcher.cpp
   import-pending-matches.cpp
diff --git a/gnucash/import-export/aqb/assistant-ab-initial.c b/gnucash/import-export/aqb/assistant-ab-initial.c
index c65acb956b..5a2dba7f30 100644
--- a/gnucash/import-export/aqb/assistant-ab-initial.c
+++ b/gnucash/import-export/aqb/assistant-ab-initial.c
@@ -63,7 +63,6 @@
 #include "gnc-ui-util.h"
 #include "gnc-session.h"
 #include "import-account-matcher.h"
-#include "import-utilities.h"
 /* This static indicates the debugging module that this .o belongs to.  */
 static QofLogModule log_module = GNC_MOD_ASSISTANT;
 
@@ -684,7 +683,7 @@ save_kvp_acc_cb(gpointer key, gpointer value, gpointer user_data)
     const gchar *ab_accountid, *gnc_accountid;
     const gchar *ab_bankcode, *gnc_bankcode;
     gchar *ab_online_id;
-    gchar *gnc_online_id;
+    const gchar *gnc_online_id;
 
     g_return_if_fail(ab_acc && gnc_acc);
 
@@ -707,11 +706,10 @@ save_kvp_acc_cb(gpointer key, gpointer value, gpointer user_data)
         gnc_ab_set_account_bankcode(gnc_acc, ab_bankcode);
 
     ab_online_id = gnc_ab_create_online_id(ab_bankcode, ab_accountid);
-    gnc_online_id = gnc_import_get_acc_online_id(gnc_acc);
+    gnc_online_id = xaccAccountGetOnlineID(gnc_acc);
     if (ab_online_id && (!gnc_online_id || (strcmp(ab_online_id, gnc_online_id) != 0)))
-        gnc_import_set_acc_online_id(gnc_acc, ab_online_id);
+        xaccAccountSetOnlineID(gnc_acc, ab_online_id);
     g_free(ab_online_id);
-    g_free (gnc_online_id);
 }
 
 static void
diff --git a/gnucash/import-export/aqb/gnc-ab-utils.c b/gnucash/import-export/aqb/gnc-ab-utils.c
index 935468304d..2c4defcc92 100644
--- a/gnucash/import-export/aqb/gnc-ab-utils.c
+++ b/gnucash/import-export/aqb/gnc-ab-utils.c
@@ -49,7 +49,6 @@
 #include "gnc-ui.h"
 #include "import-account-matcher.h"
 #include "import-main-matcher.h"
-#include "import-utilities.h"
 #include "qof.h"
 #include "engine-helpers.h"
 #include <aqbanking/gui/abgui.h>
@@ -569,7 +568,7 @@ gnc_ab_trans_to_gnc (const AB_TRANSACTION *ab_trans, Account *gnc_acc)
 
     /* Set OFX unique transaction ID */
     if (fitid && *fitid)
-        gnc_import_set_split_online_id (split, fitid);
+        xaccSplitSetOnlineID (split, fitid);
 
     /* FIXME: Extract function */
     {
diff --git a/gnucash/import-export/import-account-matcher.cpp b/gnucash/import-export/import-account-matcher.cpp
index 4179e27d18..c82589ab8a 100644
--- a/gnucash/import-export/import-account-matcher.cpp
+++ b/gnucash/import-export/import-account-matcher.cpp
@@ -34,7 +34,6 @@
 #include <glib/gi18n.h>
 
 #include "import-account-matcher.h"
-#include "import-utilities.h"
 #include "dialog-account.h"
 #include "dialog-utils.h"
 
@@ -70,15 +69,11 @@ typedef struct
 static gpointer test_acct_online_id_match(Account *acct, gpointer data)
 {
     AccountOnlineMatch *match = (AccountOnlineMatch*)data;
-    char *acct_online_id = gnc_import_get_acc_online_id(acct);
+    const char *acct_online_id = xaccAccountGetOnlineID(acct);
     int acct_len, match_len;
 
     if (acct_online_id == NULL || match->online_id == NULL)
-    {
-        if (acct_online_id)
-            g_free (acct_online_id);
         return NULL;
-    }
 
     acct_len = strlen(acct_online_id);
     match_len = strlen(match->online_id);
@@ -91,10 +86,7 @@ static gpointer test_acct_online_id_match(Account *acct, gpointer data)
     if (strncmp (acct_online_id, match->online_id, acct_len) == 0)
     {
         if (strncmp(acct_online_id, match->online_id, match_len) == 0)
-        {
-            g_free (acct_online_id);
             return (gpointer *) acct;
-        }
         if (match->partial_match == NULL)
         {
             match->partial_match = acct;
@@ -102,8 +94,8 @@ static gpointer test_acct_online_id_match(Account *acct, gpointer data)
         }
         else
         {
-            char *partial_online_id =
-                gnc_import_get_acc_online_id(match->partial_match);
+            const char *partial_online_id =
+                xaccAccountGetOnlineID(match->partial_match);
             int partial_len = strlen(partial_online_id);
             if (partial_online_id[partial_len - 1] == ' ')
                 --partial_len;
@@ -133,11 +125,9 @@ static gpointer test_acct_online_id_match(Account *acct, gpointer data)
                 g_free (name1);
                 g_free (name2);
             }
-            g_free (partial_online_id);
         }
     }
 
-    g_free (acct_online_id);
     return NULL;
 }
 
@@ -469,7 +459,7 @@ Account * gnc_import_select_account(GtkWidget *parent,
 
                 if (account_online_id_value)
                 {
-                    gnc_import_set_acc_online_id(retval, account_online_id_value);
+                    xaccAccountSetOnlineID(retval, account_online_id_value);
                 }
                 ok_pressed_retval = TRUE;
                 break;
diff --git a/gnucash/import-export/import-backend.cpp b/gnucash/import-export/import-backend.cpp
index 2028645597..4cef7ecbc0 100644
--- a/gnucash/import-export/import-backend.cpp
+++ b/gnucash/import-export/import-backend.cpp
@@ -861,11 +861,9 @@ process_reconcile(Account *base_acc,
 
     /* Copy the online id to the reconciled transaction, so
      *      the match will be remembered */
-    auto online_id = gnc_import_get_split_online_id(trans_info->first_split);
+    auto online_id = xaccSplitGetOnlineID(trans_info->first_split);
     if (online_id && *online_id)
-        gnc_import_set_split_online_id(selected_match->split, online_id);
-
-    g_free (online_id);
+        xaccSplitSetOnlineID(selected_match->split, online_id);
 
     /* Done editing. */
     /*DEBUG("CommitEdit selected_match")*/
@@ -1040,9 +1038,9 @@ hash_account_online_ids (Account *account)
           (g_str_hash, g_str_equal, g_free, nullptr);
      for (auto split : xaccAccountGetSplits (account))
      {
-        auto id = gnc_import_get_split_online_id (split);
+        auto id = xaccSplitGetOnlineID (split);
         if (id && *id)
-            g_hash_table_insert (acct_hash, (void*) id, GINT_TO_POINTER (1));
+            g_hash_table_insert (acct_hash, (void*) g_strdup (id), GINT_TO_POINTER (1));
      }
      return acct_hash;
 }
@@ -1056,7 +1054,7 @@ gboolean gnc_import_exists_online_id (Transaction *trans, GHashTable* acct_id_ha
     auto source_split = xaccTransGetSplit(trans, 0);
     g_assert(source_split);
 
-    auto source_online_id = gnc_import_get_split_online_id (source_split);
+    auto source_online_id = xaccSplitGetOnlineID (source_split);
 
     // No online id, no point in continuing. We'd crash if we tried.
     if (!source_online_id)
@@ -1081,7 +1079,6 @@ gboolean gnc_import_exists_online_id (Transaction *trans, GHashTable* acct_id_ha
         DEBUG("Transaction with online ID %s already exists, date: %s", source_online_id, date_str);
         g_free (date_str);
     }
-    g_free (source_online_id);
     return online_id_exists;
 }
 
diff --git a/gnucash/import-export/import-main-matcher.cpp b/gnucash/import-export/import-main-matcher.cpp
index f8fb60ea05..3a5e7c2bcd 100644
--- a/gnucash/import-export/import-main-matcher.cpp
+++ b/gnucash/import-export/import-main-matcher.cpp
@@ -2360,7 +2360,7 @@ create_hash_of_potential_matches (GList *candidate_splits,
          candidate = g_list_next (candidate))
     {
         auto split = static_cast<Split*>(candidate->data);
-        if (gnc_import_split_has_online_id (split))
+        if (xaccSplitHasOnlineID (split))
             continue;
         /* In this context an open transaction represents a freshly
          * downloaded one. That can't possibly be a match yet */
diff --git a/gnucash/import-export/import-utilities.cpp b/gnucash/import-export/import-utilities.cpp
deleted file mode 100644
index a0480221c9..0000000000
--- a/gnucash/import-export/import-utilities.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-/********************************************************************\
- * 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 Import_Export
-    @{ */
-/** @internal
-    @file import-utilities.c
-    @brief Utility functions for writing import modules.
-    @author Copyright (C) 2002 Benoit Grégoire <bock at step.polymtl.ca>
-*/
-#include <config.h>
-
-
-#include <glib.h>
-
-#include <stdlib.h>
-#include "import-utilities.h"
-#include "qof.h"
-#include "Account.h"
-#include "Transaction.h"
-
-
-/********************************************************************\
- * Setter and getter functions for the online_id kvp frame in
- * Account, Transaction and Split
-\********************************************************************/
-
-gchar *
-gnc_import_get_acc_online_id (Account * account)
-{
-    gchar *id = NULL;
-    qof_instance_get (QOF_INSTANCE (account), "online-id", &id, NULL);
-    return id;
-}
-
-/* Used in the midst of editing a transaction; make it save the
- * account data. */
-void
-gnc_import_set_acc_online_id (Account *account, const gchar *id)
-{
-    g_return_if_fail (account != NULL);
-    xaccAccountBeginEdit (account);
-    qof_instance_set (QOF_INSTANCE (account), "online-id", id, NULL);
-    xaccAccountCommitEdit (account);
-}
-
-gchar *
-gnc_import_get_trans_online_id (Transaction * transaction)
-{
-    gchar *id = NULL;
-    qof_instance_get (QOF_INSTANCE (transaction), "online-id", &id, NULL);
-    return id;
-}
-
-/* Not actually used */
-void
-gnc_import_set_trans_online_id (Transaction *transaction, const gchar *id)
-{
-    g_return_if_fail (transaction != NULL);
-    xaccTransBeginEdit (transaction);
-    qof_instance_set (QOF_INSTANCE (transaction), "online-id", id, NULL);
-    xaccTransCommitEdit (transaction);
-}
-
-gboolean
-gnc_import_trans_has_online_id (Transaction * transaction)
-{
-    gchar *online_id = gnc_import_get_trans_online_id(transaction);
-    gboolean retval = (online_id && *online_id);
-    g_free (online_id);
-    return retval;
-}
-
-gchar *
-gnc_import_get_split_online_id (Split * split)
-{
-    gchar *id = NULL;
-    qof_instance_get (QOF_INSTANCE (split), "online-id", &id, NULL);
-    return id;
-}
-
-/* Used several places in a transaction edit where many other
- * parameters are also being set, so individual commits wouldn't be
- * appropriate. Besides, there isn't a function for one.*/
-void
-gnc_import_set_split_online_id (Split *split, const gchar *id)
-{
-    g_return_if_fail (split != NULL);
-    qof_instance_set (QOF_INSTANCE (split), "online-id", id, NULL);
-}
-
-gboolean
-gnc_import_split_has_online_id (Split * split)
-{
-    gchar *online_id = gnc_import_get_split_online_id(split);
-    gboolean retval = (online_id && *online_id);
-    g_free (online_id);
-    return retval;
-}
-
-/* @} */
diff --git a/gnucash/import-export/import-utilities.h b/gnucash/import-export/import-utilities.h
index 3f20886a6d..0dbb7f9bba 100644
--- a/gnucash/import-export/import-utilities.h
+++ b/gnucash/import-export/import-utilities.h
@@ -19,7 +19,7 @@
 /** @addtogroup Import_Export
     @{ */
 /** @file import-utilities.h
-    @brief Utility functions for writing import modules.
+    @brief Preference keys for the generic importer.
     @author Copyright (C) 2002 Benoit Grégoire <bock at step.polymtl.ca>
 */
 #ifndef IMPORT_UTILITIES_H
@@ -40,50 +40,5 @@
 
 #include "Account.h"
 
-#ifdef __cplusplus
-extern "C"
-{
 #endif
-
-/** @name Setter-getters
-    Setter and getter functions for the online_id field for
-    Accounts.
-	@{
-*/
-gchar * gnc_import_get_acc_online_id(Account * account);
-void gnc_import_set_acc_online_id(Account * account,
-                                  const gchar * string_value);
 /** @} */
-/** @name Setter-getters
-    Setter and getter functions for the online_id field for
-    Transactions.
-	@{
-*/
-gchar * gnc_import_get_trans_online_id(Transaction * transaction);
-void gnc_import_set_trans_online_id(Transaction * transaction,
-                                    const gchar * string_value);
-/** @} */
-
-gboolean gnc_import_trans_has_online_id(Transaction * transaction);
-
-/** @name Setter-getters
-    Setter and getter functions for the online_id field for
-    Splits.
-	@{
-*/
-gchar * gnc_import_get_split_online_id(Split * split);
-void gnc_import_set_split_online_id(Split * split,
-                                    const gchar * string_value);
-/** @} */
-
-gboolean gnc_import_split_has_online_id(Split * split);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
-/** @} */
-
-
-
diff --git a/gnucash/import-export/ofx/gnc-ofx-import.cpp b/gnucash/import-export/ofx/gnc-ofx-import.cpp
index 312d1e9240..8e4d218917 100644
--- a/gnucash/import-export/ofx/gnc-ofx-import.cpp
+++ b/gnucash/import-export/ofx/gnc-ofx-import.cpp
@@ -592,8 +592,7 @@ process_bank_transaction(Transaction *transaction, Account *import_account,
     }
     if (data->fi_id_valid)
     {
-        gnc_import_set_split_online_id(split,
-                                       sanitize_string (data->fi_id));
+        xaccSplitSetOnlineID(split, sanitize_string (data->fi_id));
     }
 }
 
@@ -618,7 +617,7 @@ create_investment_subaccount(GtkWindow *parent, Account* parent_acct,
                             ACCT_TYPE_STOCK);
     if (investment_account)
     {
-        gnc_import_set_acc_online_id(investment_account, inv_data->online_id);
+        xaccAccountSetOnlineID(investment_account, inv_data->online_id);
         inv_data->choosing = FALSE;
         ofx_parent_account = parent_acct;
     }
@@ -644,7 +643,7 @@ continue_account_selection(GtkWidget* parent, Account* account,
             gnc_commodity_get_fullname(commodity),
             gnc_commodity_get_fullname(xaccAccountGetCommodity(account)));
     // We must also delete the online_id that was set in gnc_import_select_account()
-    gnc_import_set_acc_online_id(account, "");
+    xaccAccountSetOnlineID(account, "");
     return keep_going;
 }
 
@@ -815,8 +814,7 @@ add_investment_split(Transaction* transaction, Account* account,
         xaccAccountTypesCompatible(xaccAccountGetType(account),
                                    ACCT_TYPE_ASSET))
     {
-        gnc_import_set_split_online_id(split,
-                                       sanitize_string (data->fi_id));
+        xaccSplitSetOnlineID(split, sanitize_string (data->fi_id));
     }
 }
 
@@ -837,7 +835,7 @@ add_currency_split(Transaction *transaction, Account* account,
     // Set split memo from ofx transaction name or memo
     gnc_ofx_set_split_memo(data, split);
     if (data->fi_id_valid)
-        gnc_import_set_split_online_id (split, sanitize_string (data->fi_id));
+        xaccSplitSetOnlineID (split, sanitize_string (data->fi_id));
 }
 
 /* ******** Process an investment transaction **********/
diff --git a/gnucash/import-export/test/CMakeLists.txt b/gnucash/import-export/test/CMakeLists.txt
index 0c1859462f..af2b1c0506 100644
--- a/gnucash/import-export/test/CMakeLists.txt
+++ b/gnucash/import-export/test/CMakeLists.txt
@@ -59,7 +59,6 @@ set(gtest_import_backend_SOURCES
   gtest-import-backend.cpp
   ${CMAKE_SOURCE_DIR}/gnucash/import-export/import-backend.cpp
   ${CMAKE_SOURCE_DIR}/gnucash/import-export/import-settings.cpp
-  ${CMAKE_SOURCE_DIR}/gnucash/import-export/import-utilities.cpp
   ${CMAKE_SOURCE_DIR}/libgnucash/engine/mocks/gmock-qofinstance.cpp
   ${CMAKE_SOURCE_DIR}/libgnucash/app-utils/mocks/gmock-gnc-prefs.cpp
   ${CMAKE_SOURCE_DIR}/libgnucash/engine/mocks/gmock-qofbook.cpp
diff --git a/libgnucash/engine/Account.cpp b/libgnucash/engine/Account.cpp
index 019c613a53..852d5ac674 100644
--- a/libgnucash/engine/Account.cpp
+++ b/libgnucash/engine/Account.cpp
@@ -2638,6 +2638,13 @@ xaccAccountSetNotes (Account *acc, const char *str)
     set_kvp_string_path (acc, {"notes"}, str);
 }
 
+void
+xaccAccountSetOnlineID (Account *acc, const char *id)
+{
+    g_return_if_fail (GNC_IS_ACCOUNT(acc));
+    set_kvp_string_path (acc, {KEY_ONLINE_ID}, id);
+}
+
 
 void
 xaccAccountSetAssociatedAccount (Account *acc, const char *tag, const Account* assoc_acct)
@@ -3350,6 +3357,13 @@ xaccAccountGetNotes (const Account *acc)
     return get_kvp_string_path (acc, {"notes"});
 }
 
+const char *
+xaccAccountGetOnlineID (const Account *acc)
+{
+    g_return_val_if_fail (GNC_IS_ACCOUNT(acc), nullptr);
+    return get_kvp_string_path (acc, {KEY_ONLINE_ID});
+}
+
 Account*
 xaccAccountGetAssociatedAccount (const Account *acc, const char *tag)
 {
diff --git a/libgnucash/engine/Account.h b/libgnucash/engine/Account.h
index 0043d6a410..d8e7dd406b 100644
--- a/libgnucash/engine/Account.h
+++ b/libgnucash/engine/Account.h
@@ -317,6 +317,14 @@ typedef enum
     /** Set the account's notes */
     void xaccAccountSetNotes (Account *account, const char *notes);
 
+    /** Set the account's online_id, the identifier (e.g. an OFX/HBCI
+     *  BANKID+ACCTID composite) of the online account this GnuCash account
+     *  is mapped to for bank imports.  This is the same value (engine KVP
+     *  slot "online_id") that the desktop OFX/HBCI importer uses to match a
+     *  downloaded statement to its GnuCash account.  Passing NULL or ""
+     *  clears it.  Wraps its own begin/commit edit. */
+    void xaccAccountSetOnlineID (Account *account, const char *id);
+
     /** Set the account's associated account e.g. stock account -> dividend account */
     void xaccAccountSetAssociatedAccount (Account *acc, const char *tag,
                                           const Account *assoc_acct);
@@ -426,6 +434,11 @@ typedef enum
     /** Get the account's notes */
     const char * xaccAccountGetNotes (const Account *account);
 
+    /** Get the account's online_id (see xaccAccountSetOnlineID).  The
+     *  returned string is owned by the account and must NOT be freed;
+     *  returns NULL if no online_id is set. */
+    const char * xaccAccountGetOnlineID (const Account *account);
+
     /** Get the account's associated account e.g. stock account -> dividend account */
     Account* xaccAccountGetAssociatedAccount (const Account *acc, const char *tag);
     /** Get the last num field of an Account */
diff --git a/libgnucash/engine/Split.cpp b/libgnucash/engine/Split.cpp
index ad257808b4..1050e28b8d 100644
--- a/libgnucash/engine/Split.cpp
+++ b/libgnucash/engine/Split.cpp
@@ -101,6 +101,7 @@ enum
 
 static const char * split_type_normal = "normal";
 static const char * split_type_stock_split = "stock-split";
+static const char * split_online_id = "online_id";
 
 /* GObject Initialization */
 G_DEFINE_TYPE(Split, gnc_split, QOF_TYPE_INSTANCE)
@@ -1901,6 +1902,35 @@ xaccSplitGetMemo (const Split *split)
     return split ? split->memo : nullptr;
 }
 
+void
+xaccSplitSetOnlineID (Split *split, const char *id)
+{
+    if (!split) return;
+    /* No xaccTransBeginEdit/CommitEdit here: the online_id is normally set
+     * while many other parameters of a transaction are being changed, so
+     * the caller wraps this in the parent transaction's edit. */
+    std::optional<const char*> val;
+    if (id && *id)
+        val = g_strdup (id);
+    qof_instance_set_path_kvp<const char*> (QOF_INSTANCE(split), val, {split_online_id});
+    qof_instance_set_dirty (QOF_INSTANCE(split));
+}
+
+const char *
+xaccSplitGetOnlineID (const Split *split)
+{
+    if (!split) return nullptr;
+    auto rv{qof_instance_get_path_kvp<const char*> (QOF_INSTANCE(split), {split_online_id})};
+    return rv ? *rv : nullptr;
+}
+
+gboolean
+xaccSplitHasOnlineID (const Split *split)
+{
+    auto id = xaccSplitGetOnlineID (split);
+    return (id && *id);
+}
+
 const char *
 xaccSplitGetAction (const Split *split)
 {
diff --git a/libgnucash/engine/Split.h b/libgnucash/engine/Split.h
index 14f1457c51..26fd6b01c3 100644
--- a/libgnucash/engine/Split.h
+++ b/libgnucash/engine/Split.h
@@ -160,6 +160,25 @@ void          xaccSplitSetMemo (Split *split, const char *memo);
 /** Returns the memo string. */
 const char *  xaccSplitGetMemo (const Split *split);
 
+/** The online_id is the OFX/HBCI "FITID" recorded on a split when it is
+ * imported.  It is used to recognise the split on re-import so that
+ * overlapping or re-downloaded statements don't create duplicates.  This
+ * is the same value (engine KVP slot "online_id") that the desktop
+ * OFX/HBCI importer reads and writes.
+ *
+ * The setter only changes engine data: call it inside the parent
+ * transaction's edit (xaccTransBeginEdit()/xaccTransCommitEdit()), and a
+ * session save persists it.  Passing NULL or "" clears the online_id. */
+void          xaccSplitSetOnlineID (Split *split, const char *id);
+
+/** Returns the split's online_id.  The returned string is owned by the
+ * split and must NOT be freed; it is valid until the online_id is changed
+ * or the split is destroyed.  Returns NULL if no online_id is set. */
+const char *  xaccSplitGetOnlineID (const Split *split);
+
+/** Returns TRUE if the split has a non-empty online_id. */
+gboolean      xaccSplitHasOnlineID (const Split *split);
+
 /** The Action is an arbitrary user-assigned string.
  * The action field is an arbitrary user-assigned value.
  * It is meant to be a very short (one to ten character) string that
diff --git a/libgnucash/engine/mocks/gmock-Split.cpp b/libgnucash/engine/mocks/gmock-Split.cpp
index 523d99a6d9..f80d929f5b 100644
--- a/libgnucash/engine/mocks/gmock-Split.cpp
+++ b/libgnucash/engine/mocks/gmock-Split.cpp
@@ -110,6 +110,21 @@ xaccSplitSetMemo (Split *split, const char *memo)
     gnc_mocksplit(split)->set_memo(memo);
 }
 
+const char *
+xaccSplitGetOnlineID (const Split *split)
+{
+    SCOPED_TRACE("");
+    auto mocksplit = gnc_mocksplit(split);
+    return mocksplit ? mocksplit->get_online_id() : nullptr;
+}
+
+void
+xaccSplitSetOnlineID (Split *split, const char *id)
+{
+    ASSERT_TRUE(GNC_IS_MOCKSPLIT(split));
+    gnc_mocksplit(split)->set_online_id(id);
+}
+
 char
 xaccSplitGetReconcile (const Split *split)
 {
diff --git a/libgnucash/engine/mocks/gmock-Split.hpp b/libgnucash/engine/mocks/gmock-Split.hpp
index b2667d3381..a156ff9c8e 100644
--- a/libgnucash/engine/mocks/gmock-Split.hpp
+++ b/libgnucash/engine/mocks/gmock-Split.hpp
@@ -73,6 +73,8 @@ public:
     MOCK_METHOD1(set_value, void(gnc_numeric));
     MOCK_CONST_METHOD0(get_memo, const char *());
     MOCK_METHOD1(set_memo, void(const char *));
+    MOCK_CONST_METHOD0(get_online_id, const char *());
+    MOCK_METHOD1(set_online_id, void(const char *));
     MOCK_CONST_METHOD0(get_reconcile, char());
     MOCK_METHOD1(set_reconcile, void(char));
     MOCK_METHOD1(set_date_reconciled_secs, void(time64));
diff --git a/libgnucash/engine/test/test-engine-kvp-properties.c b/libgnucash/engine/test/test-engine-kvp-properties.c
index f9a6577eea..2db9e95299 100644
--- a/libgnucash/engine/test/test-engine-kvp-properties.c
+++ b/libgnucash/engine/test/test-engine-kvp-properties.c
@@ -442,11 +442,67 @@ test_vendor_kvp_properties (Fixture *fixture, gconstpointer pData)
 
 }
 
+static void
+test_split_online_id_accessors (Fixture *fixture, gconstpointer pData)
+{
+    Split *split = fixture->split;
+    const char *fitid = "20240131-0001-1234567890";
+    gchar *raw = NULL;
+
+    /* Initially unset. */
+    g_assert_true (xaccSplitGetOnlineID (split) == NULL);
+    g_assert_true (!xaccSplitHasOnlineID (split));
+
+    /* Round-trip via the new accessors. */
+    xaccSplitSetOnlineID (split, fitid);
+    g_assert_cmpstr (xaccSplitGetOnlineID (split), ==, fitid);
+    g_assert_true (xaccSplitHasOnlineID (split));
+
+    /* GUI parity: the value is stored in the engine KVP slot "online_id",
+     * readable via the "online-id" GObject property used by the importer. */
+    qof_instance_get (QOF_INSTANCE (split), "online-id", &raw, NULL);
+    g_assert_cmpstr (raw, ==, fitid);
+    g_free (raw);
+
+    /* "" clears it. */
+    xaccSplitSetOnlineID (split, "");
+    g_assert_true (xaccSplitGetOnlineID (split) == NULL);
+    g_assert_true (!xaccSplitHasOnlineID (split));
+
+    /* NULL clears it too. */
+    xaccSplitSetOnlineID (split, fitid);
+    xaccSplitSetOnlineID (split, NULL);
+    g_assert_true (xaccSplitGetOnlineID (split) == NULL);
+}
+
+static void
+test_account_online_id_accessors (Fixture *fixture, gconstpointer pData)
+{
+    Account *acct = fixture->acct;
+    const char *acct_online_id = "061000104:0123456789:CHECKING";
+    gchar *raw = NULL;
+
+    g_assert_true (xaccAccountGetOnlineID (acct) == NULL);
+
+    xaccAccountSetOnlineID (acct, acct_online_id);
+    g_assert_cmpstr (xaccAccountGetOnlineID (acct), ==, acct_online_id);
+
+    /* GUI parity: stored in the "online_id" KVP slot. */
+    qof_instance_get (QOF_INSTANCE (acct), "online-id", &raw, NULL);
+    g_assert_cmpstr (raw, ==, acct_online_id);
+    g_free (raw);
+
+    xaccAccountSetOnlineID (acct, NULL);
+    g_assert_true (xaccAccountGetOnlineID (acct) == NULL);
+}
+
 void test_suite_engine_kvp_properties (void)
 {
     GNC_TEST_ADD (suitename, "Account", Fixture, NULL, setup_account, test_account_kvp_properties, teardown);
     GNC_TEST_ADD (suitename, "Transaction", Fixture, NULL, setup_trans, test_trans_kvp_properties, teardown);
     GNC_TEST_ADD (suitename, "Split", Fixture, NULL, setup_split, test_split_kvp_properties, teardown);
+    GNC_TEST_ADD (suitename, "Split online_id accessors", Fixture, NULL, setup_split, test_split_online_id_accessors, teardown);
+    GNC_TEST_ADD (suitename, "Account online_id accessors", Fixture, NULL, setup_account, test_account_online_id_accessors, teardown);
     GNC_TEST_ADD (suitename, "Lot", Fixture, NULL, setup_lot, test_lot_kvp_properties, teardown);
     GNC_TEST_ADD (suitename, "Customer", Fixture, NULL, setup_customer, test_customer_kvp_properties, teardown);
     GNC_TEST_ADD (suitename, "Employee", Fixture, NULL, setup_employee, test_employee_kvp_properties, teardown);

commit 222714d764e36ea030db0a8791af608f0e0ef0bd
Author: Noah R <Noerr at users.noreply.github.com>
Date:   Sun Jun 21 16:35:56 2026 -0700

    [engine] Remove the unused Transaction online_id property
    
    The "online-id" GObject property on Transaction (KVP slot "online_id")
    was vestigial: GnuCash records the OFX/HBCI import identifier on the
    bank/asset Split, not on the Transaction.  Drop the property, its
    get/set_property cases, the xaccTransClone special-case that cleared it,
    and the corresponding test_trans_kvp_properties coverage.

diff --git a/libgnucash/engine/Transaction.cpp b/libgnucash/engine/Transaction.cpp
index c169700b55..67c1bbcf89 100644
--- a/libgnucash/engine/Transaction.cpp
+++ b/libgnucash/engine/Transaction.cpp
@@ -200,7 +200,6 @@ enum
     PROP_DESCRIPTION,	/* Table */
     PROP_INVOICE,	/* KVP */
     PROP_SX_TXN,	/* KVP */
-    PROP_ONLINE_ACCOUNT,/* KVP */
 };
 
 void
@@ -331,9 +330,6 @@ gnc_transaction_get_property(GObject* object,
     case PROP_SX_TXN:
         qof_instance_get_kvp (QOF_INSTANCE (tx), value, 1, GNC_SX_FROM);
         break;
-    case PROP_ONLINE_ACCOUNT:
-        qof_instance_get_kvp (QOF_INSTANCE (tx), value, 1, "online_id");
-        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -379,9 +375,6 @@ gnc_transaction_set_property(GObject* object,
     case PROP_SX_TXN:
         qof_instance_set_kvp (QOF_INSTANCE (tx), value, 1, GNC_SX_FROM);
         break;
-    case PROP_ONLINE_ACCOUNT:
-        qof_instance_set_kvp (QOF_INSTANCE (tx), value, 1, "online_id");
-        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -470,16 +463,6 @@ gnc_transaction_class_init(TransactionClass* klass)
 			   "transactions",
 			   GNC_TYPE_GUID,
 			   G_PARAM_READWRITE));
-
-    g_object_class_install_property
-    (gobject_class,
-     PROP_ONLINE_ACCOUNT,
-     g_param_spec_string ("online-id",
-                          "Online Account ID",
-                          "The online account which corresponds to this "
-			  "account for OFX/HCBI import",
-                          nullptr,
-                          G_PARAM_READWRITE));
 }
 
 /********************************************************************\
@@ -678,9 +661,6 @@ xaccTransClone (const Transaction *from)
     xaccTransBeginEdit (to);
     qof_instance_copy_kvp (QOF_INSTANCE (to), QOF_INSTANCE (from));
 
-    /* But not the online-id! */
-    qof_instance_set (QOF_INSTANCE (to), "online-id", nullptr, nullptr);
-
     for (GList* lfrom = from->splits, *lto = to->splits; lfrom && lto;
          lfrom = g_list_next (lfrom), lto = g_list_next (lto))
         xaccSplitCopyKvp (GNC_SPLIT(lfrom->data), GNC_SPLIT(lto->data));
diff --git a/libgnucash/engine/Transaction.h b/libgnucash/engine/Transaction.h
index 3f6f1fdef2..937dadc629 100644
--- a/libgnucash/engine/Transaction.h
+++ b/libgnucash/engine/Transaction.h
@@ -352,7 +352,6 @@ const char *  xaccTransGetDocLink(const Transaction *trans);
  The Notes field is only visible in the register in double-line mode */
 const char *  xaccTransGetNotes (const Transaction *trans);
 
-
 /** Sets whether or not this transaction is a "closing transaction" */
 void          xaccTransSetIsClosingTxn (Transaction *trans, gboolean is_closing);
 
diff --git a/libgnucash/engine/test/test-engine-kvp-properties.c b/libgnucash/engine/test/test-engine-kvp-properties.c
index 1a9f2fb107..f9a6577eea 100644
--- a/libgnucash/engine/test/test-engine-kvp-properties.c
+++ b/libgnucash/engine/test/test-engine-kvp-properties.c
@@ -189,14 +189,11 @@ test_trans_kvp_properties (Fixture *fixture, gconstpointer pData)
     GncGUID *invoice = guid_new ();
     GncGUID *from_sx = guid_new ();
     GncGUID *invoice_r, *from_sx_r;
-    gchar *online_id = "my online id";
-    gchar *online_id_r;
 
     xaccTransBeginEdit (fixture->trans);
     qof_instance_set (QOF_INSTANCE (fixture->trans),
 		      "invoice", invoice,
 		      "from-sched-xaction", from_sx,
-		      "online-id", online_id,
 		      NULL);
 
     g_assert_true (qof_instance_is_dirty (QOF_INSTANCE (fixture->trans)));
@@ -205,17 +202,14 @@ test_trans_kvp_properties (Fixture *fixture, gconstpointer pData)
     qof_instance_get (QOF_INSTANCE (fixture->trans),
 		      "invoice", &invoice_r,
 		      "from-sched-xaction", &from_sx_r,
-		      "online-id", &online_id_r,
 		      NULL);
     g_assert_true (guid_equal (invoice, invoice_r));
     g_assert_true (guid_equal (from_sx, from_sx_r));
-    g_assert_cmpstr (online_id, ==, online_id_r);
     g_assert_true (!qof_instance_is_dirty (QOF_INSTANCE (fixture->trans)));
     guid_free (invoice);
     guid_free (invoice_r);
     guid_free (from_sx);
     guid_free (from_sx_r);
-    g_free (online_id_r);
 }
 
 static void



Summary of changes:
 bindings/python/tests/test_account.py              |   9 ++
 bindings/python/tests/test_split.py                |  13 +++
 gnucash/import-export/CMakeLists.txt               |   1 -
 gnucash/import-export/aqb/assistant-ab-initial.c   |   8 +-
 gnucash/import-export/aqb/gnc-ab-utils.c           |   3 +-
 gnucash/import-export/import-account-matcher.cpp   |  18 +---
 gnucash/import-export/import-backend.cpp           |  13 +--
 gnucash/import-export/import-main-matcher.cpp      |   2 +-
 gnucash/import-export/import-utilities.cpp         | 116 ---------------------
 gnucash/import-export/import-utilities.h           |  47 +--------
 gnucash/import-export/ofx/gnc-ofx-import.cpp       |  12 +--
 gnucash/import-export/test/CMakeLists.txt          |   1 -
 libgnucash/engine/Account.cpp                      |  14 +++
 libgnucash/engine/Account.h                        |  13 +++
 libgnucash/engine/Split.cpp                        |  30 ++++++
 libgnucash/engine/Split.h                          |  19 ++++
 libgnucash/engine/Transaction.cpp                  |  20 ----
 libgnucash/engine/Transaction.h                    |   1 -
 libgnucash/engine/mocks/gmock-Split.cpp            |  15 +++
 libgnucash/engine/mocks/gmock-Split.hpp            |   2 +
 .../engine/test/test-engine-kvp-properties.c       |  62 +++++++++--
 21 files changed, 191 insertions(+), 228 deletions(-)
 delete mode 100644 gnucash/import-export/import-utilities.cpp



More information about the gnucash-changes mailing list