gnucash master: Multiple changes pushed

John Ralls jralls at code.gnucash.org
Fri Oct 14 14:31:22 EDT 2022


Updated	 via  https://github.com/Gnucash/gnucash/commit/939a7740 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/7f2a09a6 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/fe9b23ff (commit)
	 via  https://github.com/Gnucash/gnucash/commit/88d658fe (commit)
	 via  https://github.com/Gnucash/gnucash/commit/50c72b4f (commit)
	 via  https://github.com/Gnucash/gnucash/commit/7d93774d (commit)
	 via  https://github.com/Gnucash/gnucash/commit/81d4ea95 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/c78fe37f (commit)
	 via  https://github.com/Gnucash/gnucash/commit/99dffa71 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/90bcde2c (commit)
	 via  https://github.com/Gnucash/gnucash/commit/70c9d4c9 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/97e730b8 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/673a9255 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/e817091d (commit)
	 via  https://github.com/Gnucash/gnucash/commit/7eaa0eb2 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/d97ea777 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/6ffb0bb6 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/4c47e911 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/6db7800c (commit)
	 via  https://github.com/Gnucash/gnucash/commit/29ce9256 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/b5bc6463 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/19064093 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/734fb6ce (commit)
	 via  https://github.com/Gnucash/gnucash/commit/d3072950 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/2b870666 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/a82c72cf (commit)
	 via  https://github.com/Gnucash/gnucash/commit/37dfab7f (commit)
	 via  https://github.com/Gnucash/gnucash/commit/dd831671 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/e3ab3845 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/b8642e55 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/4dd39228 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/784aca5a (commit)
	 via  https://github.com/Gnucash/gnucash/commit/e9577b79 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/277f299a (commit)
	 via  https://github.com/Gnucash/gnucash/commit/585de5d1 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/70ab8a8a (commit)
	 via  https://github.com/Gnucash/gnucash/commit/4c286396 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/7765e137 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/e5c6f602 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/e97fc3e4 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/bf357315 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/8c08feda (commit)
	 via  https://github.com/Gnucash/gnucash/commit/8896d61c (commit)
	 via  https://github.com/Gnucash/gnucash/commit/fbf9aecd (commit)
	 via  https://github.com/Gnucash/gnucash/commit/1a0be99b (commit)
	 via  https://github.com/Gnucash/gnucash/commit/a00bce16 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/fcbe6cf1 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/5c13da0e (commit)
	 via  https://github.com/Gnucash/gnucash/commit/6ecc1ef7 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/65ae4642 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/3685e5de (commit)
	 via  https://github.com/Gnucash/gnucash/commit/6ce91d7f (commit)
	 via  https://github.com/Gnucash/gnucash/commit/616a672d (commit)
	 via  https://github.com/Gnucash/gnucash/commit/a6771754 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/f3fdc5de (commit)
	 via  https://github.com/Gnucash/gnucash/commit/1d94887a (commit)
	 via  https://github.com/Gnucash/gnucash/commit/466db526 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/8c4bd86c (commit)
	 via  https://github.com/Gnucash/gnucash/commit/9d62755b (commit)
	 via  https://github.com/Gnucash/gnucash/commit/32df095d (commit)
	 via  https://github.com/Gnucash/gnucash/commit/d79306f7 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/f658ff40 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/8b772384 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/2f7ed7f2 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/b0ae402c (commit)
	 via  https://github.com/Gnucash/gnucash/commit/3c75d212 (commit)
	from  https://github.com/Gnucash/gnucash/commit/3c306eae (commit)



commit 939a77407c137db37e95ee78917e6cdff97453c2
Merge: 3c306eae6 7f2a09a69
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Oct 14 11:25:14 2022 -0700

    Merge branch 'price-quotes-cpp'


commit 7f2a09a69fda5b54b0e349910480384271db4456
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 17:59:14 2022 -0700

    [price-quotes] Handle short error strings from finance-quote-wrapper.
    
    This keeps the translation work in GnuCash and improves the error
    signalling in gnc-quotes.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 0e522c57c..152c1a3b0 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -136,6 +136,7 @@ private:
 
 static void show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
 static void show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
+static std::string parse_quotesource_error(const std::string& line);
 
 static const std::string empty_string{};
 
@@ -293,16 +294,9 @@ GncQuotesImpl::fetch (CommVec& commodities)
     m_failures.clear();
     if (commodities.empty())
         throw (GncQuoteException(bl::translate("GncQuotes::Fetch called with no commodities.")));
-    try
-    {
-        auto quote_str{query_fq (commodities)};
-        auto ptree{parse_quotes (quote_str)};
-        create_quotes(ptree, commodities);
-    }
-    catch (const GncQuoteException& err)
-    {
-        std::cerr << _("Finance::Quote retrieval failed with error ") << err.what() << std::endl;
-    }
+    auto quote_str{query_fq (commodities)};
+    auto ptree{parse_quotes (quote_str)};
+    create_quotes(ptree, commodities);
 }
 
 void
@@ -436,7 +430,12 @@ get_quotes(const std::string& json_str, const std::unique_ptr<GncQuoteSource>& q
     {
         std::string err_str;
         for (auto line: errors)
-            err_str.append(line + "\n");
+        {
+            if (line == "invalid_json\n")
+                PERR("Finanace Quote Wrapper was unable to parse %s",
+                     json_str.c_str());
+            err_str += parse_quotesource_error(line);
+        }
         throw(GncQuoteException(err_str));
     }
 
@@ -874,6 +873,32 @@ show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbo
         std::cout << std::endl;
     }
 }
+
+static std::string
+parse_quotesource_error(const std::string& line)
+{
+    std::string err_str;
+    if (line == "invalid_json\n")
+    {
+        err_str += _("GnuCash submitted invalid json to Finance::Quote. The details were logged.");
+    }
+    else if (line.substr(0, 15) == "missing_modules")
+    {
+        PERR("Missing Finance::Quote Dependencies: %s",
+             line.substr(17).c_str());
+        err_str += _("Perl is missing the following modules. Please see https://wiki.gnucash.org/wiki/Online_Quotes#Finance::Quote for detailed corrective action. ");
+        err_str += line.substr(17);
+    }
+    else
+    {
+        PERR("Unrecognized Finance::Quote Error %s", line.c_str());
+        err_str +=_("Unrecognized Finance::Quote Error: ");
+        err_str += line;
+    }
+    err_str += "\n";
+    return err_str;
+}
+
 /********************************************************************
  * gnc_quotes_get_quotable_commodities
  * list commodities in a given namespace that get price quotes
diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index dc5194261..3f1136e80 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -53,16 +53,35 @@ public:
     GncMockQuoteSource(StrVec&& quotes, StrVec&& errors) :
         m_quotes{std::move(quotes)}, m_errors{std::move(errors)}{}
     ~GncMockQuoteSource() override = default;
-    virtual const std::string& get_version() const noexcept override { return m_version; }
-    virtual const StrVec& get_sources() const noexcept override { return m_sources; }
-    virtual QuoteResult get_quotes(const std::string&) const override;
-    virtual bool usable() const noexcept override { return true; }
+    const std::string& get_version() const noexcept override { return m_version; }
+    const StrVec& get_sources() const noexcept override { return m_sources; }
+    QuoteResult get_quotes(const std::string&) const override;
+};
+
+class GncFailedQuoteSource final : public GncQuoteSource
+{
+
+    const std::string m_version{"0"};
+    const StrVec m_sources;
+public:
+    GncFailedQuoteSource()
+        {
+            std::string err{"Failed to initialize Finance::Quote: "};
+            err += "missing_modules Mozilla::CA Try::Tiny";
+            throw GncQuoteSourceError (err);
+        }
+    ~GncFailedQuoteSource() override = default;
+    const std::string& get_version() const noexcept override { return m_version; }
+    const StrVec& get_sources() const noexcept override { return m_sources; }
+    QuoteResult get_quotes(const std::string&) const override {return {0, {}, {}}; }
 };
 
 QuoteResult
 GncMockQuoteSource::get_quotes(const std::string& json_string) const
 {
-    return {0, m_quotes, m_errors};
+    if (m_errors.empty())
+        return {0, m_quotes, m_errors};
+    return {1, m_quotes, m_errors};
 }
 
 class GncQuotesTest : public ::testing::Test
@@ -357,3 +376,36 @@ TEST_F(GncQuotesTest, test_version)
     GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
     EXPECT_STREQ("9.99", quotes.version().c_str());
 }
+
+TEST_F(GncQuotesTest, test_failure_invalid_json)
+{
+    StrVec quote_vec, err_vec{"invalid_json\n"};
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    EXPECT_THROW(quotes.fetch(m_book), GncQuoteException);
+    try
+    {
+        quotes.fetch(m_book);
+    }
+    catch (const GncQuoteException& err)
+    {
+        EXPECT_STREQ("GnuCash submitted invalid json to Finance::Quote. The details were logged.\n",
+                     err.what());
+    }
+
+}
+
+TEST_F(GncQuotesTest, test_failure_missing_modules)
+{
+    EXPECT_THROW(GncQuotesImpl quotes(m_book, std::make_unique<GncFailedQuoteSource>()),
+                 GncQuoteSourceError);
+    try
+    {
+        GncQuotesImpl quotes(m_book, std::make_unique<GncFailedQuoteSource>());
+    }
+    catch (const GncQuoteSourceError& err)
+    {
+        EXPECT_STREQ("Failed to initialize Finance::Quote: missing_modules Mozilla::CA Try::Tiny",
+                     err.what());
+    }
+
+}

commit fe9b23ff2bec973b37f5131365f4551558d12baa
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 17:57:11 2022 -0700

    [price quotes] Pass short errors to gnc-quotes.
    
    And wordier but not translated messages when STDERR is a tty.

diff --git a/libgnucash/quotes/finance-quote-wrapper.in b/libgnucash/quotes/finance-quote-wrapper.in
index 4f05516ad..cbb0e8976 100755
--- a/libgnucash/quotes/finance-quote-wrapper.in
+++ b/libgnucash/quotes/finance-quote-wrapper.in
@@ -86,18 +86,26 @@ sub check_modules {
 
   return unless @missing;
 
-  print STDERR "\n";
-  print STDERR "You need to install the following Perl modules:\n";
-  foreach my $mod (@missing) {
-    print STDERR "  ".$mod."\n";
+# Test for STDERR being a tty and output a detailed message if it is
+# and a short message if it isn't; in the latter case we're probably
+# being called from GnuCash and it will emit its own localized error.
+  if (-t STDERR)
+  {
+      print STDERR "\n";
+      print STDERR "You need to install the following Perl modules:\n";
+      foreach my $mod (@missing) {
+          print STDERR "  ".$mod."\n";
+      }
+
+      print STDERR "\n";
+      print STDERR "Please see https://wiki.gnucash.org/wiki/Online_Quotes#Finance::Quote for detailed corrective action.\n";
+
+      print "missing-lib\n";
+  }
+  else
+  {
+      print STDERR "missing_modules ", join(" ", @missing), "\n";
   }
-
-  print STDERR "\n";
-  print STDERR "Use your system's package manager to install them,\n";
-  print STDERR "or run 'gnc-fq-update' as root.\n";
-
-  print "missing-lib\n";
-
   exit 1;
 }
 
@@ -112,13 +120,17 @@ sub print_version  {
 }
 
 sub print_usage {
-    print STDERR
-"Usage:
+    if (-t STDERR)
+    {
+        my $message = << 'END';
+Usage:
   Check proper installation and version:
     finance-quote-wrapper -v
   Fetch quotes (input should be passed as JSON via stdin):
     finance-quote-wrapper -f
-";
+END
+        print STDERR $message;
+    }
 }
 
 sub sanitize_hash {
@@ -209,17 +221,28 @@ JSON::Parse->import(qw(valid_json parse_json));
 my $json_input = do { local $/; <STDIN> };
 
 if (!valid_json($json_input)) {
-    print STDERR "Could not parse input as valid JSON.\n";
-    print STDERR "Received input:\n$json_input\n";
+    if (-t STDERR)
+    {
+        print STDERR "Could not parse input as valid JSON.\n";
+        print STDERR "Received input:\n$json_input\n";
+    }
+    else
+    {
+        print STDERR "invalid_json\n";
+    }
     exit 1;
 }
 
 my $requests = parse_json ($json_input);
 
 my $defaultcurrency = $$requests{'defaultcurrency'};
+# This shouldn't be possible if we're called from GnuCash, so only warn in interactive use.
 if (!$defaultcurrency) {
     $defaultcurrency = "USD";
-    print STDERR "Warning: no default currency was specified, assuming 'USD'\n";
+    if (-t STDERR)
+    {
+        print STDERR "Warning: no default currency was specified, assuming 'USD'\n";
+    }
 }
 
 # Create a stockquote object.

commit 88d658fef01dee84802ae66ed5919e0f9b7a327c
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 14:58:27 2022 -0700

    [price-quotes] Date::Manip is no longer required.

diff --git a/libgnucash/quotes/gnc-fq-update.in b/libgnucash/quotes/gnc-fq-update.in
index dbbcf675a..422713b9f 100755
--- a/libgnucash/quotes/gnc-fq-update.in
+++ b/libgnucash/quotes/gnc-fq-update.in
@@ -37,7 +37,6 @@ if ($( != 0) {
 }
 
 CPAN::Shell->install('Test2'); #Required by an F::Q dependency but cpan doesn't notice.
-CPAN::Shell->install('Date::Manip'); #Required by gnc-fq-helper
 CPAN::Shell->install('Finance::Quote');
 
 ## Local Variables:

commit 50c72b4f88387465b346da28618bb96f7277a8e5
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 14:28:17 2022 -0700

    [price-quotes] Remove m_ready and usable() from GncQuoteSource.
    
    GncQuoteSource ctor throws if something is wrong so usable is always true.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index fae4ce88d..0e522c57c 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -78,7 +78,6 @@ public:
     virtual const StrVec& get_sources() const noexcept = 0;
     virtual const std::string & get_version() const noexcept = 0;
     virtual QuoteResult get_quotes(const std::string& json_str) const = 0;
-    virtual bool usable() const noexcept = 0;
 };
 
 
@@ -121,7 +120,6 @@ class GncFQQuoteSource final : public GncQuoteSource
 {
     const bfs::path c_cmd;
     const std::string c_fq_wrapper;
-    bool m_ready;
     std::string m_version;
     StrVec m_sources;
     std::string m_api_key;
@@ -131,7 +129,6 @@ public:
     const std::string& get_version() const noexcept override { return m_version; }
     const StrVec& get_sources() const noexcept override { return m_sources; }
     QuoteResult get_quotes(const std::string&) const override;
-    bool usable() const noexcept override { return m_ready; }
 private:
     QuoteResult run_cmd (const StrVec& args, const std::string& json_string) const;
 
@@ -145,8 +142,7 @@ static const std::string empty_string{};
 GncFQQuoteSource::GncFQQuoteSource() :
 c_cmd{bp::search_path("perl")},
 c_fq_wrapper{std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper"},
-m_ready{false}, m_version{},
-m_sources{}, m_api_key{}
+m_version{}, m_sources{}, m_api_key{}
 {
     StrVec args{"-w", c_fq_wrapper, "-v"};
     const std::string empty_string;
@@ -172,7 +168,6 @@ m_sources{}, m_api_key{}
         throw(GncQuoteSourceError(err));
     }
     m_version = std::move(version);
-    m_ready = true;
     sources.erase(sources.begin());
     m_sources = std::move(sources);
 
@@ -248,8 +243,6 @@ GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
                                  m_book{qof_session_get_book(gnc_get_current_session())},
                                  m_dflt_curr{gnc_default_currency()}
 {
-    if (!m_quotesource->usable())
-        return;
     m_sources = m_quotesource->get_sources();
 }
 
@@ -257,8 +250,6 @@ GncQuotesImpl::GncQuotesImpl(QofBook* book) : m_quotesource{new GncFQQuoteSource
 m_sources{}, m_book{book},
 m_dflt_curr{gnc_default_currency()}
 {
-    if (!m_quotesource->usable())
-        return;
     m_sources = m_quotesource->get_sources();
 }
 
@@ -266,8 +257,6 @@ GncQuotesImpl::GncQuotesImpl(QofBook* book, std::unique_ptr<GncQuoteSource> quot
 m_quotesource{std::move(quote_source)},
 m_sources{}, m_book{book}, m_dflt_curr{gnc_default_currency()}
 {
-    if (!m_quotesource->usable())
-        return;
     m_sources = m_quotesource->get_sources();
 }
 

commit 7d93774dd23a5287db5c9312d899fb0296ad4d7b
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 13:51:37 2022 -0700

    [price-quotes] Throw instead of returning if there aren't any commodities to quote.
    
    So that the user gets sees an error instead of silent failure.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 1f14320ee..fae4ce88d 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -303,7 +303,7 @@ GncQuotesImpl::fetch (CommVec& commodities)
 {
     m_failures.clear();
     if (commodities.empty())
-        return;
+        throw (GncQuoteException(bl::translate("GncQuotes::Fetch called with no commodities.")));
     try
     {
         auto quote_str{query_fq (commodities)};

commit 81d4ea9550d70a74d8a1ebb85c827185b50aa605
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 12:58:09 2022 -0700

    [price-quotes] Remove F::Q version format check.
    
    We don't care what the version string looks like as long as there is one.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 6f981e744..1f14320ee 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -165,12 +165,10 @@ m_sources{}, m_api_key{}
             err += err.empty() ? "" : err_line + "\n";
         throw(GncQuoteSourceError(err));
     }
-    static const boost::regex version_fmt{"[0-9]\\.[0-9][0-9]"};
     auto version{sources.front()};
-    if (version.empty() || !boost::regex_match(version, version_fmt))
+    if (version.empty())
     {
-        std::string err{bl::translate("Invalid Finance::Quote Version ")};
-            err +=  version.empty() ? "" : version;
+        std::string err{bl::translate("No Finance::Quote Version")};
         throw(GncQuoteSourceError(err));
     }
     m_version = std::move(version);

commit c78fe37ff7c8a19d75239922d722aeaf494c5424
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 12:52:36 2022 -0700

    [price-quotes] General typo fixes and code cleanup.

diff --git a/gnucash/gnucash.cpp b/gnucash/gnucash.cpp
index 77ba65e68..30226db71 100644
--- a/gnucash/gnucash.cpp
+++ b/gnucash/gnucash.cpp
@@ -179,7 +179,7 @@ scm_run_gnucash (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **
     {
         auto msg = bl::translate ("Checking Finance::Quote...").str(gnc_get_boost_locale());
         GncQuotes quotes;
-            msg = (bl::format (bl::translate("Found Finance::Quote version {1}.")) % quotes.version()).str(gnc_get_boost_locale());
+        msg = (bl::format (bl::translate("Found Finance::Quote version {1}.")) % quotes.version()).str(gnc_get_boost_locale());
         auto quote_sources = quotes.sources_as_glist();
         gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
         g_list_free (quote_sources);
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index cd4ffb9f4..6f981e744 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -128,10 +128,10 @@ class GncFQQuoteSource final : public GncQuoteSource
 public:
     GncFQQuoteSource();
     ~GncFQQuoteSource() = default;
-    virtual const std::string& get_version() const noexcept override { return m_version; }
-    virtual const StrVec& get_sources() const noexcept override { return m_sources; }
-    virtual QuoteResult get_quotes(const std::string&) const override;
-    virtual bool usable() const noexcept override { return m_ready; }
+    const std::string& get_version() const noexcept override { return m_version; }
+    const StrVec& get_sources() const noexcept override { return m_sources; }
+    QuoteResult get_quotes(const std::string&) const override;
+    bool usable() const noexcept override { return m_ready; }
 private:
     QuoteResult run_cmd (const StrVec& args, const std::string& json_string) const;
 
@@ -453,12 +453,6 @@ get_quotes(const std::string& json_str, const std::unique_ptr<GncQuoteSource>& q
         throw(GncQuoteException(err_str));
     }
 
-//        for (auto line : quotes)
-//            PINFO("Output line retrieved from wrapper:\n%s", line.c_str());
-//
-//     for (auto line : errors)
-//         PINFO("Error line retrieved from wrapper:\n%s",line.c_str());Ëš
-
     return answer;
 }
 
@@ -589,15 +583,11 @@ calc_price_time(const PriceParams& p)
             return static_cast<time64>(now);
         }
     }
-    else
-    {
-        auto now{GncDateTime()};
-        PINFO("Info: no date  was returned for %s:%s - will use %s",
-              p.ns, p.mnemonic, now.format("%Y-%m-%d %H:%M%S").c_str());
-        return static_cast<time64>(now);
-    }
 
-    return INT64_MAX; //Shouldn't be able to get here.
+    auto now{GncDateTime()};
+    PINFO("No date  was returned for %s:%s - will use %s",
+          p.ns, p.mnemonic, now.format("%Y-%m-%d %H:%M%S").c_str());
+    return static_cast<time64>(now);
 }
 
 static boost::optional<GncNumeric>
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 4de71476d..3e835e184 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -109,7 +109,7 @@ public:
      */
     const QuoteSources& sources() noexcept;
 
-    /** Get the available Finance::Quote sources as a GLixt
+    /** Get the available Finance::Quote sources as a GList
      *
      * @return A double-linked list containing the names of the installed quote sources.
      * @note the list and its contents are owned by the caller and should be freed with `g_list_free_full(list, g_free)`.

commit 99dffa71206b1c2e9849b8f372ad2135a4431bc3
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 12:51:48 2022 -0700

    [price-quotes] Use c++ syntax for PricesDialog decl.

diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index d9ba82673..bb877753c 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -74,7 +74,7 @@ static gboolean gnc_prices_dialog_key_press_cb (GtkWidget *widget,
 }
 
 
-typedef struct
+struct PricesDialog
 {
     GtkWidget * window;
     QofSession *session;
@@ -90,7 +90,7 @@ typedef struct
     GtkWidget *remove_dialog;
     GtkTreeView *remove_view;
     int remove_source;
-} PricesDialog;
+};
 
 
 void

commit 90bcde2cfbc8533aeb4a9d695398ff10299216ed
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 12:37:54 2022 -0700

    [price-quotes] Rename Gnucash::quotes_info to Gnucash::check_finance_quote.

diff --git a/gnucash/gnucash-cli.cpp b/gnucash/gnucash-cli.cpp
index 95a8aecf6..b4cc54370 100644
--- a/gnucash/gnucash-cli.cpp
+++ b/gnucash/gnucash-cli.cpp
@@ -137,7 +137,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
     {
         if (m_quotes_cmd.front() == "info")
         {
-            return Gnucash::quotes_info ();
+            return Gnucash::check_finance_quote ();
         }
         else if (m_quotes_cmd.front() == "get")
         {
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index a655f472b..8fc91e918 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -300,7 +300,7 @@ scm_report_list ([[maybe_unused]] void *data,
 }
 
 int
-Gnucash::quotes_info (void)
+Gnucash::check_finance_quote (void)
 {
     gnc_prefs_init ();
     try
diff --git a/gnucash/gnucash-commands.hpp b/gnucash/gnucash-commands.hpp
index 09133cfc5..5a32e9056 100644
--- a/gnucash/gnucash-commands.hpp
+++ b/gnucash/gnucash-commands.hpp
@@ -34,7 +34,7 @@ using StrVec = std::vector<std::string>;
 
 namespace Gnucash {
 
-    int quotes_info (void);
+    int check_finance_quote (void);
     int add_quotes (const bo_str& uri);
     int report_quotes (const char* source,
                        const StrVec& commodities,

commit 70c9d4c9e31b1d610ffce519026c986f0cda9882
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 12:01:36 2022 -0700

    [price-quotes] Fix version retrieval.
    
    Plus there's no need for a "not found" version string because GncQuotes
    construction will throw if Finance::Quote isn't correctly installed. No
    object, nothing to call version() on.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 8ccd3d885..cd4ffb9f4 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -81,6 +81,7 @@ public:
     virtual bool usable() const noexcept = 0;
 };
 
+
 class GncQuotesImpl
 {
 public:
@@ -94,7 +95,7 @@ public:
     void fetch (gnc_commodity *comm);
     void report (const char* source, const StrVec& commodities, bool verbose);
 
-    const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
+    const std::string& version() noexcept { return m_quotesource->get_version(); }
     const QuoteSources& sources() noexcept { return m_sources; }
     GList* sources_as_glist ();
     bool had_failures() noexcept { return !m_failures.empty(); }
@@ -110,7 +111,6 @@ private:
     GNCPrice* parse_one_quote(const bpt::ptree&, gnc_commodity*);
 
     std::unique_ptr<GncQuoteSource> m_quotesource;
-    std::string m_version;
     QuoteSources m_sources;
     QFVec m_failures;
     QofBook *m_book;
@@ -145,8 +145,8 @@ static const std::string empty_string{};
 GncFQQuoteSource::GncFQQuoteSource() :
 c_cmd{bp::search_path("perl")},
 c_fq_wrapper{std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper"},
-m_ready{false},
-m_version{}, m_sources{}, m_api_key{}
+m_ready{false}, m_version{},
+m_sources{}, m_api_key{}
 {
     StrVec args{"-w", c_fq_wrapper, "-v"};
     const std::string empty_string;
@@ -173,6 +173,7 @@ m_version{}, m_sources{}, m_api_key{}
             err +=  version.empty() ? "" : version;
         throw(GncQuoteSourceError(err));
     }
+    m_version = std::move(version);
     m_ready = true;
     sources.erase(sources.begin());
     m_sources = std::move(sources);
@@ -245,7 +246,7 @@ GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) c
 
 /* GncQuotes implementation */
 GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
-                                 m_version{}, m_sources{}, m_failures{},
+                                 m_sources{}, m_failures{},
                                  m_book{qof_session_get_book(gnc_get_current_session())},
                                  m_dflt_curr{gnc_default_currency()}
 {
@@ -255,7 +256,7 @@ GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
 }
 
 GncQuotesImpl::GncQuotesImpl(QofBook* book) : m_quotesource{new GncFQQuoteSource},
-m_version{}, m_sources{}, m_book{book},
+m_sources{}, m_book{book},
 m_dflt_curr{gnc_default_currency()}
 {
     if (!m_quotesource->usable())
@@ -265,7 +266,7 @@ m_dflt_curr{gnc_default_currency()}
 
 GncQuotesImpl::GncQuotesImpl(QofBook* book, std::unique_ptr<GncQuoteSource> quote_source) :
 m_quotesource{std::move(quote_source)},
-m_version{}, m_sources{}, m_book{book}, m_dflt_curr{gnc_default_currency()}
+m_sources{}, m_book{book}, m_dflt_curr{gnc_default_currency()}
 {
     if (!m_quotesource->usable())
         return;
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 5cfa61850..4de71476d 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -60,8 +60,6 @@ struct GncQuoteException : public std::runtime_error
     GncQuoteException(const std::string& msg) : std::runtime_error(msg) {}
 };
 
-const std::string not_found = std::string ("Not Found");
-
 class GncQuotesImpl;
 
 class GncQuotes
diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index f67b834c2..dc5194261 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -147,6 +147,11 @@ TEST_F(GncQuotesTest, online_wiggle)
 //    EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[1]));
     EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
 }
+#else
+TEST_F(GncQuotesTest, fq_failure)
+{
+    EXPECT_THROW(GncQuotes quotes;, GncQuoteException);
+}
 #endif
 
 TEST_F(GncQuotesTest, offline_wiggle)
@@ -346,3 +351,9 @@ TEST_F(GncQuotesTest, no_date)
     EXPECT_STREQ("last", gnc_price_get_typestr(price));
 }
 
+TEST_F(GncQuotesTest, test_version)
+{
+    StrVec quote_vec, err_vec;
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    EXPECT_STREQ("9.99", quotes.version().c_str());
+}

commit 97e730b8d81c75798472e0afb0fb1d37fcf2e92f
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Oct 13 11:24:49 2022 -0700

    [price-quotes] Reformat test quote response strings to one line per quote.

diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index 93dc56e83..f67b834c2 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -152,7 +152,12 @@ TEST_F(GncQuotesTest, online_wiggle)
 TEST_F(GncQuotesTest, offline_wiggle)
 {
     StrVec quote_vec{
-        "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004},\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}}"
+        "{"
+        "\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004},"
+        "\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},"
+        "\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},"
+        "\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}"
+        "}"
     };
     StrVec err_vec;
     GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
@@ -167,7 +172,11 @@ TEST_F(GncQuotesTest, offline_wiggle)
 TEST_F(GncQuotesTest, offline_report)
 {
     StrVec quote_vec{
-        "{\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}}"
+        "{"
+        "\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},"
+        "\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},"
+        "\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}"
+        "}"
     };
     StrVec commodities{"AAPL", "HPE", "FKCM"};
     StrVec err_vec;
@@ -179,7 +188,10 @@ TEST_F(GncQuotesTest, offline_report)
 TEST_F(GncQuotesTest, offline_currency_report)
 {
     StrVec quote_vec{
-        "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004}}"};
+        "{"
+        "\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004}"
+        "}"
+    };
     StrVec commodities{"USD", "EUR"};
     StrVec err_vec;
     GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
@@ -190,7 +202,10 @@ TEST_F(GncQuotesTest, offline_currency_report)
 TEST_F(GncQuotesTest, comvec_fetch)
 {
      StrVec quote_vec{
-        "{\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1}}"
+        "{"
+        "\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},"
+        "\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1}"
+        "}"
     };
     StrVec err_vec;
     auto commtable{gnc_commodity_table_get_table(m_book)};
@@ -208,7 +223,9 @@ TEST_F(GncQuotesTest, comvec_fetch)
 TEST_F(GncQuotesTest, fetch_one_commodity)
 {
      StrVec quote_vec{
-        "{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"currency\":\"USD\",\"success\":1}}"
+        "{"
+        "\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"currency\":\"USD\",\"success\":1}"
+        "}"
     };
     StrVec err_vec;
     auto commtable{gnc_commodity_table_get_table(m_book)};
@@ -234,7 +251,9 @@ TEST_F(GncQuotesTest, fetch_one_commodity)
 TEST_F(GncQuotesTest, fetch_one_currency)
 {
      StrVec quote_vec{
-         "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004}}"
+         "{"
+         "\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004}"
+         "}"
     };
     StrVec err_vec;
     auto commtable{gnc_commodity_table_get_table(m_book)};
@@ -262,7 +281,9 @@ TEST_F(GncQuotesTest, fetch_one_currency)
 TEST_F(GncQuotesTest, no_currency)
 {
      StrVec quote_vec{
-        "{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"success\":1}}"
+        "{"
+        "\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"success\":1}"
+        "}"
     };
     StrVec err_vec;
     auto commtable{gnc_commodity_table_get_table(m_book)};
@@ -280,7 +301,9 @@ TEST_F(GncQuotesTest, no_currency)
 TEST_F(GncQuotesTest, bad_currency)
 {
      StrVec quote_vec{
-        "{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"currency\":\"BTC\",\"success\":1}}"
+        "{"
+        "\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"currency\":\"BTC\",\"success\":1}"
+        "}"
      };
     StrVec err_vec;
     auto commtable{gnc_commodity_table_get_table(m_book)};
@@ -298,7 +321,9 @@ TEST_F(GncQuotesTest, bad_currency)
 TEST_F(GncQuotesTest, no_date)
 {
      StrVec quote_vec{
-        "{\"HPE\":{\"last\":13.37,\"currency\":\"USD\",\"success\":1}}"
+        "{"
+        "\"HPE\":{\"last\":13.37,\"currency\":\"USD\",\"success\":1}"
+        "}"
     };
     StrVec err_vec;
     auto commtable{gnc_commodity_table_get_table(m_book)};

commit 673a9255544c7cfa70ae8936a3465d210019e40b
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Oct 1 17:15:39 2022 -0700

    [price-quotes] Remove superseded gnc-fq-dump and Quotes-example.pl

diff --git a/libgnucash/quotes/CMakeLists.txt b/libgnucash/quotes/CMakeLists.txt
index 6f5174b14..331538efc 100644
--- a/libgnucash/quotes/CMakeLists.txt
+++ b/libgnucash/quotes/CMakeLists.txt
@@ -1,6 +1,6 @@
 
 set(_BIN_FILES "")
-foreach(file gnc-fq-update.in gnc-fq-dump.in finance-quote-wrapper.in)
+foreach(file gnc-fq-update.in finance-quote-wrapper.in)
   string(REPLACE ".in" "" _OUTPUT_FILE_NAME ${file})
   set(_ABS_OUTPUT_FILE ${BINDIR_BUILD}/${_OUTPUT_FILE_NAME})
   configure_file( ${file} ${_ABS_OUTPUT_FILE} @ONLY)
@@ -9,7 +9,7 @@ endforeach(file)
 
 
 set(_MAN_FILES "")
-foreach(file gnc-fq-dump finance-quote-wrapper)
+foreach(file finance-quote-wrapper)
   set(_POD_INPUT ${BINDIR_BUILD}/${file})
   set(_MAN_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file}.1)
   list(APPEND _MAN_FILES ${_MAN_OUTPUT})
@@ -26,4 +26,4 @@ add_custom_target(quotes-bin ALL DEPENDS ${_BIN_FILES})
 install(FILES ${_MAN_FILES} DESTINATION  ${CMAKE_INSTALL_MANDIR}/man1)
 install(PROGRAMS ${_BIN_FILES} DESTINATION ${CMAKE_INSTALL_BINDIR})
 
-set_dist_list(quotes_DIST CMakeLists.txt gnc-fq-dump.in gnc-fq-update.in finance-quote-wrapper.in Quote_example.pl README)
+set_dist_list(quotes_DIST CMakeLists.txt gnc-fq-update.in finance-quote-wrapper.in README)
diff --git a/libgnucash/quotes/Quote_example.pl b/libgnucash/quotes/Quote_example.pl
deleted file mode 100755
index 220ee27f1..000000000
--- a/libgnucash/quotes/Quote_example.pl
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/perl -w
-##@file
-# @brief
-# example script showing how to use the Quote perl module.
-# gets prices for some stocks, for some mutual funds
-#
-# Note that this example uses the meta-level "fetch" command.  We do
-# NOT used that in Gnucash because it's behavior is unpredictable If
-# the given method/exchange doesn't work, it'll fall back to other
-# methods, and I've seen no guarantee that all exchanges treat all
-# symbols the same.  So in Gnucash, we use the backend methods
-# directly, i.e. $quoter->fidelity_direct("IBM", "LNUX");, etc.  The
-# documentation page for each Finance::Quote sub-module describes how
-# to call it directly without fallbacks.
-#
-# @cond PERL
-
-use Finance::Quote;
-
-# Create a quote object.
-my $quoter = Finance::Quote->new();
-
-# -----------------------------------
-# get quotes for two stocks ...
-%quotes = $quoter->fetch("alphavantage","IBM", "SGI"); 
-
-# print some selected values 
-print "NYSE by Alphavantage: ", $quotes {"IBM", "name"},  
-       " last price: ", $quotes {"IBM", "last"},  "\n";
-print "NYSE by Alphavantage: ", $quotes {"SGI", "name"},   
-       " last price: ", $quotes {"SGI", "last"},  "\n";
-       
-# loop over and print all values.
-# Notes that values are stored ion a multi-dimensional associative array
-foreach $k (sort (keys %quotes)) {
-     ($sym, $attr) = split ($;, $k, 2);
-     $val = $quotes {$sym, $attr};
-     # $val = $quotes {$k};     # this also works, if desired ...
-     print "\t$sym $attr =\t $val\n";
-} 
-print "\n\n";
-
-# -----------------------------------
-# get quotes from Fidelity Investments
- at funds = ("FGRIX", "FNMIX", "FASGX", "FCONX");
-%quotes = $quoter->fetch("fidelity", at funds);
-
-foreach $f (@funds) {
-     $name = $quotes {$f, "name"};
-     $nav = $quotes {$f, "nav"};
-     print "Fidelity Fund $f $name \tNAV = $nav\n";
-}
-print "\n\n";
-
-# -----------------------------------
- at funds = ("FGRXX");
-%quotes = $quoter->fetch("fidelity", at funds);
-
-print "Not all funds have a NAV; some have Yields:\n";
-foreach $f (@funds) {
-     $name = $quotes {$f, "name"};
-     $yield = $quotes {$f, "yield"};
-     print "\tFidelity $f $name 30-day Yield = $yield percent\n";
-}
-print "\n\n";
-
-# -----------------------------------
-# demo T. Rowe Price -- same as above
- at funds = ("PRFDX", "PRIDX");
-%quotes = $quoter->fetch("troweprice", at funds);
-
-foreach $f (@funds) {
-     $nav = $quotes {$f, "nav"};
-     $dayte = $quotes {$f, "date"};
-     print "T. Rowe Price $f NAV = $nav as of $dayte\n";
-}
-print "\n\n";
-
-
-# -----------------------------------
-
-# demo for ASX.  Grab the price of Coles-Myer and Telstra
- at funds = ("CML","TLS");
-%quotes = $quoter->fetch("australia", at funds);
-foreach $f (@funds) {
-	print "ASX Price of $f is ".$quotes{$f,"last"}." at ".
-	      $quotes{$f,"date"}."\n";
-}
-print "\n\n";
-##@endcond Perl
diff --git a/libgnucash/quotes/README b/libgnucash/quotes/README
index 3f594fb59..e29671e04 100644
--- a/libgnucash/quotes/README
+++ b/libgnucash/quotes/README
@@ -8,12 +8,6 @@ finance-quote-wrapper.in:
   allows gnucash to communicate with Finance::Quote.
   The requests and responses are in json format.
 
-gnc-fq-dump.in:
-
-  Source file for gnc-fq-dump which is a perl script that retrieves
-  a quote from Finance::Quote and dumps the response to the terminal.
-  Its useful for determining problems with F::Q.
-
 gnc-fq-update.in:
 
   Source file for gnc-fq-update which is a perl script that updates
diff --git a/libgnucash/quotes/gnc-fq-dump.in b/libgnucash/quotes/gnc-fq-dump.in
deleted file mode 100755
index 6da7f415e..000000000
--- a/libgnucash/quotes/gnc-fq-dump.in
+++ /dev/null
@@ -1,242 +0,0 @@
-#!@PERL@ -w
-#
-#    Copyright (C) 2003, David Hampton <hampton at employees.org>
-#
-#    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, write to the Free Software
-#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-#    02110-1301, USA.
-#
-
-use strict;
-
-sub check_modules {
-  my @modules = qw(Finance::Quote);
-  my @missing;
-
-  foreach my $mod (@modules) {
-    if (eval "require $mod") {
-      $mod->import();
-    }
-    else {
-      push (@missing, $mod);
-    }
-  }
-
-  return unless @missing;
-
-  print STDERR "$0 cannot find all the Perl modules needed to run.\n";
-  print STDERR "You need to install the following Perl modules:\n";
-  foreach my $mod (@missing) {
-    print STDERR "  ".$mod."\n";
-  }
-  print STDERR "Use your system's package manager to install them,\n";
-  print STDERR "or run 'gnc-fq-update' as root.\n";
-
-  exit 1;
-}
-
-sub report {
-  my($itemname, $qh, $verbose) = @_;
-  my ($symbol, $date, $currency, $last, $nav, $price, $timezone, $keyname);
-  my($gccanuse, $gcshoulduse) = (1, 1);
-
-  # Sanity check returned results
-  if ((keys %$qh) < 1) {
-    printf("No results found for stock $itemname.\n");
-    return;
-  } else {
-    my ($stock, $attribute, %seen, $first);
-
-    foreach $keyname (sort keys %$qh) {
-      ($stock, $attribute) = split('\034', $keyname);
-      last if $stock eq $itemname;
-      $first = $stock if !defined $first;
-      $seen{$stock} = 1;
-    }
-
-    if ($stock ne $itemname) {
-      printf "\nNo results found for stock $itemname, but results were returned for\n";
-      printf "the stock(s) %s.  ", join(", ",  keys(%seen));
-      printf "Printing data for the first stock returned.\n\n";
-
-      # Print stats for the first stock returned.
-      $itemname = $first;
-    }
-  }
-
-  # Parse the quote fields and put warnings where necessary.
-  if (defined($$qh{$itemname, "symbol"})) {
-    $symbol = $$qh{$itemname, "symbol"};
-  } else {
-    $symbol = "$itemname (deduced)";
-    $gccanuse = 0;
-  }
-  if (defined($$qh{$itemname, "date"})) {
-    $date = $$qh{$itemname, "date"};
-  } else {
-    $date = "** missing **";
-    $gcshoulduse = 0;
-  }
-  if (defined($$qh{$itemname, "currency"})) {
-    $currency = $$qh{$itemname, "currency"};
-  } else {
-    $currency = "** missing **";
-    $gccanuse = 0;
-  }
-  if ((!defined($$qh{$itemname, "last"})) &&
-      (!defined($$qh{$itemname, "nav" })) &&
-      (!defined($$qh{$itemname, "price"}))) {
-    $$qh{$itemname, "last"} = "**missing**";
-    $$qh{$itemname, "nav"} = "**missing**";
-    $$qh{$itemname, "price"} = "**missing**";
-    $gccanuse = 0;
-  }
-  $last = defined($$qh{$itemname, "last"})
-    ? $$qh{$itemname, "last"} :  "";
-  $nav = defined($$qh{$itemname, "nav"})
-    ? $$qh{$itemname, "nav"} :  "";
-  $price = defined($$qh{$itemname, "price"})
-    ? $$qh{$itemname, "price"} :  "";
-  $timezone = defined($$qh{$itemname, "timezone"})
-    ? $$qh{$itemname, "timezone"} :  "";
-
-  # Dump gnucash recognized fields
-  printf "Finance::Quote fields Gnucash uses:\n";
-  printf "    symbol: %-20s <=== required\n",     $symbol;
-  printf "      date: %-20s <=== recommended\n",  $date;
-  printf "  currency: %-20s <=== required\n",     $currency;
-  printf "      last: %-20s <=\\\n",              $last;
-  printf "       nav: %-20s <=== one of these\n", $nav;
-  printf "     price: %-20s <=/\n",               $price;
-  printf "  timezone: %-20s <=== optional\n",     $timezone;
-
-  # Report failure
-  if ($gccanuse == 0) {
-    printf "\n** This stock quote cannot be used by GnuCash!\n\n";
-  } elsif ($gcshoulduse == 0) {
-      printf "\n** This quote will have today's date, which might be incorrect.\n";
-      printf "   GnuCash will use it, but you might prefer that it doesn't.\n\n";
-  }
-  # Dump all fields if requested
-  if ($verbose) {
-    printf "\nAll fields returned by Finance::Quote for stock $itemname\n\n";
-    printf "%-10s %10s  %s\n", "stock", "field", "value";
-    printf "%-10s %10s  %s\n", "-----", "-----", "-----";
-    foreach $keyname (sort keys %$qh) {
-      my ($stock, $key) = split('\034', $keyname);
-      printf "%-10s %10s: %s\n", $stock, $key, $$qh{$stock, $key};
-    }
-    print "\n";
-  }
-}
-
-sub chk_api_key {
-  my $exch = $_[0];
-  my $url = " https://wiki.gnucash.org/wiki/Online_Quotes#Source_Alphavantage.2C_US\n";
-  if (($exch eq "currency") || ($exch eq "alphavantage")
-  ||  ($exch eq "vanguard")) {
-    die "ERROR: ALPHAVANTAGE_API_KEY *must* be set for currency quotes and\n" .
-        "stock quotes with source 'alphavantage' or 'vanguard'; see\n" . $url
-      unless (defined ($ENV{'ALPHAVANTAGE_API_KEY'}));
-  }
-  if (($exch eq "canada") || ($exch eq "nasdaq")
-  ||  ($exch eq "nyse")   || ($exch eq "usa")) {
-    printf("WARNING: Multiple Source '%s' will not be able to use alphavantage " .
-        "unless ALPHAVANTAGE_API_KEY is set; see\n%s", $exch, $url)
-      unless (defined ($ENV{'ALPHAVANTAGE_API_KEY'}));
-    }
-}
-
-############## end of functions - start mainline #########################
-
-# Check for and load non-standard modules
-check_modules ();
-
-my $q = Finance::Quote->new;
-$q->timeout(60);
-
-if ($#ARGV < 1) {
-  my @sources = sort $q->sources();
-  printf "\nUsage: $0 [-v] <quote-source> <stock> [<stock> ...]\n\n";
-  printf "-v: verbose\n";
-  printf "Available sources are:\n     %s\n\n", join(' ', @sources);
-  exit 0;
-}
-
-my $verbose = 0;
-if ($ARGV[0] eq "-v") {
-  $verbose = 1;
-  shift;
-}
-
-my $exchange = shift;
-chk_api_key ($exchange);
-if ($exchange eq "currency") {
-  my $from = shift;
-  while ($#ARGV >= 0) {
-    my $to = shift;
-    my $result = $q->currency($from, $to);
-    # Sometimes quotes are available in only one direction.
-    # If we didn't get the one we wanted try the reverse quote
-    unless (defined($result)) {
-        my $inv_res = $q->currency($to, $from);
-        if (defined($inv_res)) {
-            my $tmp = $to;
-            $to = $from;
-            $from = $tmp;
-            $result = $inv_res;
-        }
-    }
-    if (defined($result)) {
-      printf "1 $from = $result $to\n";
-    } else {
-      printf "1 $from = <unknown> $to\n";
-    }
-  }
-} else {
-  while ($#ARGV >= 0) {
-    my $stock = shift;
-    my %quotes = $q->fetch($exchange, $stock);
-    report($stock, \%quotes, $verbose);
-    if ($#ARGV >= 0) {
-      printf "=====\n\n";
-    }
-  }
-}
-
-=head1 NAME
-
-gnc-fq-dump - Print out data from the F::Q module
-
-=head1 SYNOPSIS
-
-  Currency Exchange Rates
-    gnc-fq-dump currency USD AUD
-    gnc-fq-dump [-v] yahoo_json USDEUR=X
-  Stock Quotes    
-    gnc-fq-dump [-v] alphavantage CSCO JNPR
-    gnc-fq-dump [-v] alphavantage BAESY.PK
-    gnc-fq-dump [-v] yahoo_json CBA.AX
-    gnc-fq-dump [-v] europe 48406.PA 13000.PA
-    gnc-fq-dump [-v] vwd 632034
-    gnc-fq-dump [-v] ftportfolios FKYGTX
-
-=head1 DESCRIPTION
-
-This program obtains information from Finance::Quote about any
-specified stock, and then dumps it to the screen in annotated form.
-This will allow someone to see what is returned, and whether it
-provides all the information needed by Gnucash.
-
-=cut

commit e817091de13e27a8b5d7dcee8c68e4f02fc68cc0
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Oct 1 17:01:48 2022 -0700

    [price-quotes] Warn only once if the AlphaVantage Key isn't set.
    
    And check the environment if it's not in preferences.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 0f20f2a6f..8ccd3d885 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -124,6 +124,7 @@ class GncFQQuoteSource final : public GncQuoteSource
     bool m_ready;
     std::string m_version;
     StrVec m_sources;
+    std::string m_api_key;
 public:
     GncFQQuoteSource();
     ~GncFQQuoteSource() = default;
@@ -145,7 +146,7 @@ GncFQQuoteSource::GncFQQuoteSource() :
 c_cmd{bp::search_path("perl")},
 c_fq_wrapper{std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper"},
 m_ready{false},
-m_version{}, m_sources{}
+m_version{}, m_sources{}, m_api_key{}
 {
     StrVec args{"-w", c_fq_wrapper, "-v"};
     const std::string empty_string;
@@ -175,6 +176,15 @@ m_version{}, m_sources{}
     m_ready = true;
     sources.erase(sources.begin());
     m_sources = std::move(sources);
+
+    auto av_key = gnc_prefs_get_string ("general.finance-quote", "alphavantage-api-key");
+    if (!av_key)
+        av_key = getenv("ALPHAVANTAGE_API_KEY");
+
+    if (av_key)
+        m_api_key = std::string(av_key);
+    else
+        PWARN("No Alpha Vantage API key set, currency quotes and other AlphaVantage based quotes won't work.");
 }
 
 QuoteResult
@@ -190,10 +200,6 @@ GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) c
     StrVec out_vec, err_vec;
     int cmd_result;
 
-    auto av_key = gnc_prefs_get_string ("general.finance-quote", "alphavantage-api-key");
-    if (!av_key)
-        PWARN("No Alpha Vantage API key set, currency quotes and other AlphaVantage based quotes won't work.");
-
     try
     {
         std::future<std::vector<char> > out_buf, err_buf;
@@ -204,7 +210,7 @@ GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) c
                            bp::std_out > out_buf,
                            bp::std_err > err_buf,
                            bp::std_in < input_buf,
-                           bp::env["ALPHAVANTAGE_API_KEY"]= (av_key ? av_key : ""),
+                           bp::env["ALPHAVANTAGE_API_KEY"]= (m_api_key.empty() ? m_api_key : ""),
                            svc);
         svc.run();
         process.wait();

commit 7eaa0eb292f080acbc86f7b5c2a929f3c99fcfc3
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Oct 1 16:07:23 2022 -0700

    [price-quotes] Add dump command to gnucash-cli.

diff --git a/gnucash/gnucash-cli.cpp b/gnucash/gnucash-cli.cpp
index 90e6a1b8c..95a8aecf6 100644
--- a/gnucash/gnucash-cli.cpp
+++ b/gnucash/gnucash-cli.cpp
@@ -60,8 +60,9 @@ namespace Gnucash {
     private:
         void configure_program_options (void);
 
-        boost::optional <std::string> m_quotes_cmd;
+        std::vector<std::string> m_quotes_cmd;
         boost::optional <std::string> m_namespace;
+        bool m_verbose = false;
 
         boost::optional <std::string> m_report_cmd;
         boost::optional <std::string> m_report_name;
@@ -94,12 +95,17 @@ Gnucash::GnucashCli::configure_program_options (void)
 {
     bpo::options_description quotes_options(_("Price Quotes Retrieval Options"));
     quotes_options.add_options()
-    ("quotes,Q", bpo::value (&m_quotes_cmd),
+        ("quotes,Q", bpo::value<std::vector<std::string>> (&m_quotes_cmd)->multitoken(),
      _("Execute price quote related commands. The following commands are supported.\n\n"
        "  info: \tShow Finance::Quote version and exposed quote sources.\n"
-       "  get: \tFetch current quotes for all foreign currencies and stocks in the given GnuCash datafile.\n"))
+       "   get: \tFetch current quotes for all foreign currencies and stocks in the given GnuCash datafile.\n"
+       "  dump: \tFetch current quotes for specified currencies or stocks from a specified namespace and print the results to the console.\n"
+       "        \tThis must be followed with a source and one or more symbols, unless the source is \"currency\" in which case it must be followed with two or more symbols, the first of which is the currency in which exchange rates for the rest will be quoted.\n"))
     ("namespace", bpo::value (&m_namespace),
-     _("Regular expression determining which namespace commodities will be retrieved for"));
+     _("Regular expression determining which namespace commodities will be retrieved for when using the get command"))
+     ("verbose,V", bpo::bool_switch (&m_verbose),
+      _("When using the dump command list all of the parameters Finance::Quote returns for the symbol instead of the ones that Gnucash requires."));
+
     m_opt_desc_display->add (quotes_options);
     m_opt_desc_all.add (quotes_options);
 
@@ -127,13 +133,13 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
 {
     Gnucash::CoreApp::start();
 
-    if (m_quotes_cmd)
+    if (!m_quotes_cmd.empty())
     {
-        if (*m_quotes_cmd == "info")
+        if (m_quotes_cmd.front() == "info")
         {
             return Gnucash::quotes_info ();
         }
-        else if (*m_quotes_cmd == "get")
+        else if (m_quotes_cmd.front() == "get")
         {
 
             if (!m_file_to_load || m_file_to_load->empty())
@@ -145,9 +151,23 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
             else
                 return Gnucash::add_quotes (m_file_to_load);
         }
+        else if (m_quotes_cmd.front() == "dump")
+        {
+            if (m_quotes_cmd.size() < 3 ||
+                (m_quotes_cmd[1] == "currency" &&
+                 m_quotes_cmd.size() < 4))
+            {
+                std::cerr << bl::translate("Not enough information for quotes dump") << std::endl;
+                return 1;
+            }
+            auto source = m_quotes_cmd[1];
+            m_quotes_cmd.erase(m_quotes_cmd.begin(), m_quotes_cmd.begin() + 2);
+            return Gnucash::report_quotes(source.c_str(), m_quotes_cmd,
+                                          m_verbose);
+        }
         else
         {
-            std::cerr << bl::format (bl::translate("Unknown quotes command '{1}'")) % *m_quotes_cmd << "\n\n"
+            std::cerr << bl::format (bl::translate("Unknown quotes command '{1}'")) % m_quotes_cmd.front() << "\n\n"
                       << *m_opt_desc_display.get() << std::endl;
             return 1;
         }
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index ab0892ec0..a655f472b 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -362,6 +362,24 @@ Gnucash::add_quotes (const bo_str& uri)
     return 0;
 }
 
+int
+Gnucash::report_quotes (const char* source, const StrVec& commodities, bool verbose)
+{
+    try
+    {
+        GncQuotes quotes;
+        quotes.report(source, commodities, verbose);
+        if (quotes.had_failures())
+            std::cerr << quotes.report_failures() << std::endl;
+    }
+    catch (const GncQuoteException& err)
+    {
+        std::cerr << bl::translate("Price retrieval failed: ") << err.what() << std::endl;
+        return -1;
+   }
+    return 0;
+}
+
 int
 Gnucash::run_report (const bo_str& file_to_load,
                      const bo_str& run_report,
diff --git a/gnucash/gnucash-commands.hpp b/gnucash/gnucash-commands.hpp
index d5958ae5f..09133cfc5 100644
--- a/gnucash/gnucash-commands.hpp
+++ b/gnucash/gnucash-commands.hpp
@@ -26,14 +26,19 @@
 #define GNUCASH_COMMANDS_HPP
 
 #include <string>
+#include <vector>
 #include <boost/optional.hpp>
 
 using bo_str = boost::optional <std::string>;
+using StrVec = std::vector<std::string>;
 
 namespace Gnucash {
 
     int quotes_info (void);
     int add_quotes (const bo_str& uri);
+    int report_quotes (const char* source,
+                       const StrVec& commodities,
+                       bool verbose);
     int run_report (const bo_str& file_to_load,
                     const bo_str& run_report,
                     const bo_str& export_type,

commit d97ea7776290af70cbe50cf50bcaf4a91c584d51
Author: John Ralls <jralls at ceridwen.us>
Date:   Tue Sep 27 15:05:04 2022 -0700

    [price quotes] Add report member function to display quote information to stdout.
    
    Instead of creating price instances in the database.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 03bee51d4..0f20f2a6f 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -92,6 +92,7 @@ public:
     void fetch (QofBook *book);
     void fetch (CommVec& commodities);
     void fetch (gnc_commodity *comm);
+    void report (const char* source, const StrVec& commodities, bool verbose);
 
     const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
     const QuoteSources& sources() noexcept { return m_sources; }
@@ -101,8 +102,10 @@ public:
     std::string report_failures() noexcept;
 
 private:
+    std::string query_fq (const char* source, const StrVec& commoditites);
     std::string query_fq (const CommVec&);
-    void parse_quotes (const std::string& quote_str, const CommVec& commodities);
+    bpt::ptree parse_quotes (const std::string& quote_str);
+    void create_quotes(const bpt::ptree& pt, const CommVec& comm_vec);
     std::string comm_vec_to_json_string(const CommVec&) const;
     GNCPrice* parse_one_quote(const bpt::ptree&, gnc_commodity*);
 
@@ -133,6 +136,9 @@ private:
 
 };
 
+static void show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
+static void show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
+
 static const std::string empty_string{};
 
 GncFQQuoteSource::GncFQQuoteSource() :
@@ -293,9 +299,42 @@ GncQuotesImpl::fetch (CommVec& commodities)
     m_failures.clear();
     if (commodities.empty())
         return;
+    try
+    {
+        auto quote_str{query_fq (commodities)};
+        auto ptree{parse_quotes (quote_str)};
+        create_quotes(ptree, commodities);
+    }
+    catch (const GncQuoteException& err)
+    {
+        std::cerr << _("Finance::Quote retrieval failed with error ") << err.what() << std::endl;
+    }
+}
 
-    auto quote_str{query_fq (commodities)};
-    parse_quotes (quote_str, commodities);
+void
+GncQuotesImpl::report (const char* source, const StrVec& commodities,
+                       bool verbose)
+{
+    bool is_currency{source && strcmp(source, "currency") == 0};
+    m_failures.clear();
+    if (commodities.empty())
+    {
+        std::cerr << _("There were no commodities for which to retrieve quotes.") << std::endl;
+        return;
+    }
+    try
+    {
+        auto quote_str{query_fq (source, commodities)};
+        auto ptree{parse_quotes (quote_str)};
+        if (is_currency)
+            show_currency_quotes(ptree, commodities, verbose);
+        else
+            show_quotes(ptree, commodities, verbose);
+    }
+    catch (const GncQuoteException& err)
+    {
+        std::cerr << _("Finance::Quote retrieval failed with error ") << err.what() << std::endl;
+    }
 }
 
 const QFVec&
@@ -388,11 +427,10 @@ GncQuotesImpl::comm_vec_to_json_string (const CommVec& comm_vec) const
     return result.str();
 }
 
-std::string
-GncQuotesImpl::query_fq (const CommVec& comm_vec)
+static inline std::string
+get_quotes(const std::string& json_str, const std::unique_ptr<GncQuoteSource>& qs)
 {
-    auto json_str{comm_vec_to_json_string(comm_vec)};
-    auto [rv, quotes, errors] = m_quotesource->get_quotes(json_str);
+    auto [rv, quotes, errors] = qs->get_quotes(json_str);
     std::string answer;
 
     if (rv == 0)
@@ -408,13 +446,47 @@ GncQuotesImpl::query_fq (const CommVec& comm_vec)
         throw(GncQuoteException(err_str));
     }
 
-    return answer;
 //        for (auto line : quotes)
 //            PINFO("Output line retrieved from wrapper:\n%s", line.c_str());
 //
 //     for (auto line : errors)
 //         PINFO("Error line retrieved from wrapper:\n%s",line.c_str());Ëš
 
+    return answer;
+}
+
+std::string
+GncQuotesImpl::query_fq (const char* source, const StrVec& commodities)
+{
+    bpt::ptree pt;
+    auto is_currency{strcmp(source, "currency") == 0};
+
+    if (is_currency && commodities.size() < 2)
+        throw(GncQuoteException(_("Currency quotes requires at least two currencies")));
+
+    if (is_currency)
+        pt.put("defaultcurrency", commodities[0].c_str());
+    else
+        pt.put("defaultcurrency", gnc_commodity_get_mnemonic(m_dflt_curr));
+
+    std::for_each(is_currency ? ++commodities.cbegin() : commodities.cbegin(),
+                  commodities.cend(),
+                  [this, source, &pt](auto sym)
+                      {
+                          std::string key{source};
+                          key += "." + sym;
+                          pt.put(key, "");
+                      });
+    std::ostringstream result;
+    bpt::write_json(result, pt);
+    return get_quotes(result.str(), m_quotesource);
+}
+
+std::string
+GncQuotesImpl::query_fq (const CommVec& comm_vec)
+{
+    auto json_str{comm_vec_to_json_string(comm_vec)};
+    return get_quotes(json_str, m_quotesource);
 }
 
 struct PriceParams
@@ -636,8 +708,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
     return gnc_price;
 }
 
-void
-GncQuotesImpl::parse_quotes (const std::string& quote_str, const CommVec& comm_vec)
+bpt::ptree
+GncQuotesImpl::parse_quotes (const std::string& quote_str)
 {
     bpt::ptree pt;
     std::istringstream ss {quote_str};
@@ -671,7 +743,12 @@ GncQuotesImpl::parse_quotes (const std::string& quote_str, const CommVec& comm_v
         error_msg += what;
         throw(GncQuoteException(error_msg));
     }
+    return pt;
+}
 
+void
+GncQuotesImpl::create_quotes (const bpt::ptree& pt, const CommVec& comm_vec)
+{
     auto pricedb{gnc_pricedb_get_db(m_book)};
     for (auto comm : comm_vec)
     {
@@ -685,8 +762,134 @@ GncQuotesImpl::parse_quotes (const std::string& quote_str, const CommVec& comm_v
     }
 }
 
+static void
+show_verbose_quote(const bpt::ptree& comm_pt)
+{
+    std::for_each(comm_pt.begin(), comm_pt.end(),
+                  [](auto elem) {
+                      std::cout << std::setw(12) << std::right << elem.first << " => " <<
+                          std::left << elem.second.data() << "\n";
+                  });
+    std::cout << std::endl;
+}
+
+static void
+show_gnucash_quote(const bpt::ptree& comm_pt)
+{
+    constexpr const char* ptr{"<=== "};
+    constexpr const char* dptr{"<=\\ "};
+    constexpr const char* uptr{"<=/ "};
+    //Translators: Means that the preceding element is required
+    const char* reqd{C_("Finance::Quote", "required")};
+    //Translators: Means that the quote will work best if the preceding element is provided
+    const char* rec{C_("Finance::Quote", "recommended")};
+    //Translators: Means that one of the indicated elements is required
+    const char* oot{C_("Finance::Quote", "one of these")};
+    //Translators: Means that the preceding element is optional
+    const char* opt{C_("Finance::Quote", "optional")};
+    //Translators: Means that a required element wasn't reported. The *s are for emphasis.
+    const char* miss{C_("Finance::Quote", "**missing**")};
+
+    const std::string miss_str{miss};
+    auto outline{[](const char* label, std::string value, const char* pointer, const char* req) {
+                         std::cout << std::setw(12) << std::right << label  << std::setw(16) << std::left <<
+       value << pointer << req << "\n";
+                 }};
+    std::cout << _("Finance::Quote fields GnuCash uses:") << "\n";
+//Translators: The stock or Mutual Fund symbol, ISIN, CUSIP, etc.
+    outline(C_("Finance::Quote", "symbol: "),  comm_pt.get<char>("symbol", miss), ptr, reqd);
+//Translators: The date of the quote.
+    outline(C_("Finance::Quote", "date: "),  comm_pt.get<char>("date", miss), ptr, rec);
+//Translators: The quote currency
+    outline(C_("Finance::Quote", "currency: "),  comm_pt.get<char>("currency", miss), ptr, reqd);
+    auto last{comm_pt.get<char>("last", "")};
+    auto nav{comm_pt.get<char>("nav", "")};
+    auto price{comm_pt.get<char>("nav", "")};
+    auto no_price{last.empty() && nav.empty() && price.empty()};
+//Translators: The quote is for the most recent trade on the exchange
+    outline(C_("Finance::Quote", "last: "),  no_price ? miss_str : last, dptr, "");
+//Translators: The quote is for an open-ended mutual fund and represents the net asset value of one unit of the fund at the previous close of trading.
+    outline(C_("Finance::Quote", "nav: "),  no_price ? miss_str : nav, ptr, oot);
+//Translators: The quote is neither a last trade nor an NAV.
+    outline(C_("Finance::Quote", "price: "),  no_price ? miss_str : price, uptr, "");
+    std::cout << std::endl;
+}
+static const bpt::ptree empty_tree{};
+
+static inline const bpt::ptree&
+get_commodity_data(const bpt::ptree& pt, const std::string& comm)
+{
+    auto commdata{pt.find(comm)};
+    if (commdata == pt.not_found())
+    {
+        std::cout << comm << " " << _("Finance::Quote returned no data and set no error.") << std::endl;
+        return empty_tree;
+    }
+    auto& comm_pt{commdata->second};
+    auto success = comm_pt.get_optional<bool> ("success");
+    if (!(success && *success))
+    {
+        auto errormsg = comm_pt.get_optional<std::string> ("errormsg");
+        if (errormsg && !errormsg->empty())
+            std::cout << _("Finance::Quote reported a failure for symbol ") <<
+                comm << ": " << *errormsg << std::endl;
+        else
+            std::cout << _("Finance::Quote failed silently to retrieve a quote for symbol ") <<
+                comm << std::endl;
+        return empty_tree;
+    }
+    return comm_pt;
+}
+
+static void
+show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose)
+{
+    for (auto comm : commodities)
+    {
+        auto comm_pt{get_commodity_data(pt, comm)};
+
+        if (comm_pt == empty_tree)
+            continue;
+
+        if (verbose)
+        {
+            std::cout << comm << ":\n";
+            show_verbose_quote(comm_pt);
+        }
+        else
+        {
+            show_gnucash_quote(comm_pt);
+        }
+    }
+}
 
+static void
+show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose)
+{
+    auto to_cur{commodities.front()};
+    for (auto comm : commodities)
+    {
+        if (comm == to_cur)
+            continue;
+
+        auto comm_pt{get_commodity_data(pt, comm)};
 
+        if (comm_pt == empty_tree)
+            continue;
+
+        if (verbose)
+        {
+            std::cout << comm << ":\n";
+            show_verbose_quote(comm_pt);
+        }
+        else
+        {
+            std::cout << "1 " << comm << " = " <<
+                comm_pt.get<char>("last", "Not Found") << " " << to_cur  << "\n";
+        }
+        std::cout << std::endl;
+    }
+}
 /********************************************************************
  * gnc_quotes_get_quotable_commodities
  * list commodities in a given namespace that get price quotes
@@ -805,6 +1008,12 @@ void GncQuotes::fetch (gnc_commodity *comm)
     m_impl->fetch (comm);
 }
 
+void GncQuotes::report (const char* source, const StrVec& commodities,
+                        bool verbose)
+{
+    m_impl->report(source, commodities, verbose);
+}
+
 const std::string& GncQuotes::version() noexcept
 {
     return m_impl->version ();
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index a35d09705..5cfa61850 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -91,7 +91,14 @@ public:
      * @note Commodity must have a quote source set or the call will silently fail.
      */
     void fetch (gnc_commodity *comm);
-
+    /** Report quote results from Finance::Quote to std::cout.
+     *
+     * @param source A valid quote source
+     * @param commodities A std::vector of symbols to request quotes for.
+     * @note If requesting currency rates the first symbol is the to-currency and the rest are from-currencies. For example, {"USD", "EUR", "CAD"} will print the price of 1 Euro and 1 Canadian Dollar in US Dollars.
+     * @param verbose Ignored for currency queries. If false it will print the six fields GnuCash uses regardless of whether a value was returned; if true it will print all of the fields for which Finanace::Quote returned values.
+     */
+    void report (const char* source, const StrVec& commodities, bool verbose = false);
     /** Get the installed Finance::Quote version
      *
      * @return the Finance::Quote version string
diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index ebcd66d04..93dc56e83 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -142,10 +142,10 @@ TEST_F(GncQuotesTest, online_wiggle)
     quotes.fetch(m_book);
     auto pricedb{gnc_pricedb_get_db(m_book)};
     auto failures{quotes.failures()};
-    ASSERT_EQ(2u, failures.size());
+    ASSERT_EQ(1u, failures.size());
     EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[0]));
-    EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[1]));
-    EXPECT_EQ(2u, gnc_pricedb_get_num_prices(pricedb));
+//    EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[1]));
+    EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
 }
 #endif
 
@@ -164,6 +164,29 @@ TEST_F(GncQuotesTest, offline_wiggle)
     EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
 }
 
+TEST_F(GncQuotesTest, offline_report)
+{
+    StrVec quote_vec{
+        "{\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}}"
+    };
+    StrVec commodities{"AAPL", "HPE", "FKCM"};
+    StrVec err_vec;
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.report("yahoo_json", commodities, false);
+    quotes.report("yahoo_json", commodities, true);
+}
+
+TEST_F(GncQuotesTest, offline_currency_report)
+{
+    StrVec quote_vec{
+        "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004}}"};
+    StrVec commodities{"USD", "EUR"};
+    StrVec err_vec;
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.report("currency", commodities, false);
+    quotes.report("currency", commodities, true);
+}
+
 TEST_F(GncQuotesTest, comvec_fetch)
 {
      StrVec quote_vec{

commit 6ffb0bb633f21e24e710fba09b7a0963a318eb1d
Author: John Ralls <jralls at ceridwen.us>
Date:   Sun Sep 18 15:06:59 2022 -0700

    [price-quotes] Report quote fetch failures to the user.

diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index 81265dc34..d9ba82673 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -564,6 +564,9 @@ gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data)
         gnc_set_busy_cursor (NULL, TRUE);
         quotes.fetch (pdb_dialog->book);
         gnc_unset_busy_cursor (NULL);
+        if (quotes.had_failures())
+            gnc_warning_dialog(GTK_WINDOW(pdb_dialog->window), "%s",
+                               quotes.report_failures().c_str());
     }
     catch (const GncQuoteException& err)
     {
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index 6eb627517..ab0892ec0 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -346,10 +346,12 @@ Gnucash::add_quotes (const bo_str& uri)
         gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
         g_list_free_full (quote_sources, g_free);
         quotes.fetch(qof_session_get_book(session));
+        if (quotes.had_failures())
+            std::cerr << quotes.report_failures() << std::endl;
     }
     catch (const GncQuoteException& err)
     {
-        std::cerr << err.what() << std::endl;
+        std::cerr << bl::translate("Price retrieval failed: ") << err.what() << std::endl;
     }
     qof_session_save(session, NULL);
     if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 43113e26e..03bee51d4 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -96,6 +96,7 @@ public:
     const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
     const QuoteSources& sources() noexcept { return m_sources; }
     GList* sources_as_glist ();
+    bool had_failures() noexcept { return !m_failures.empty(); }
     const QFVec& failures() noexcept;
     std::string report_failures() noexcept;
 
@@ -821,6 +822,12 @@ GList* GncQuotes::sources_as_glist ()
 
 GncQuotes::~GncQuotes() = default;
 
+bool
+GncQuotes::had_failures() noexcept
+{
+    return m_impl->had_failures();
+}
+
 const QFVec&
 GncQuotes::failures() noexcept
 {
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index b3635f779..a35d09705 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -111,6 +111,12 @@ public:
      */
     GList* sources_as_glist () ;
 
+    /** Report if there were quotes requested but not retrieved.
+     *
+     * @returns True if there were quote failures.
+     */
+    bool had_failures() noexcept;
+
     /** Report the commodities for which quotes were requested but not successfully retrieved.
      *
      * This does not include requested commodities that didn't have a quote source.

commit 4c47e911808e8d4e77037b4f074473799101776e
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Sep 17 11:52:15 2022 -0700

    [price-quotes] Implement error codes for currency and quote failures.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 94dc064c8..43113e26e 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -96,6 +96,8 @@ public:
     const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
     const QuoteSources& sources() noexcept { return m_sources; }
     GList* sources_as_glist ();
+    const QFVec& failures() noexcept;
+    std::string report_failures() noexcept;
 
 private:
     std::string query_fq (const CommVec&);
@@ -106,6 +108,7 @@ private:
     std::unique_ptr<GncQuoteSource> m_quotesource;
     std::string m_version;
     QuoteSources m_sources;
+    QFVec m_failures;
     QofBook *m_book;
     gnc_commodity *m_dflt_curr;
 };
@@ -129,6 +132,8 @@ private:
 
 };
 
+static const std::string empty_string{};
+
 GncFQQuoteSource::GncFQQuoteSource() :
 c_cmd{bp::search_path("perl")},
 c_fq_wrapper{std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper"},
@@ -227,8 +232,9 @@ GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) c
 
 /* GncQuotes implementation */
 GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
-m_version{}, m_sources{}, m_book{qof_session_get_book(gnc_get_current_session())},
-m_dflt_curr{gnc_default_currency()}
+                                 m_version{}, m_sources{}, m_failures{},
+                                 m_book{qof_session_get_book(gnc_get_current_session())},
+                                 m_dflt_curr{gnc_default_currency()}
 {
     if (!m_quotesource->usable())
         return;
@@ -283,6 +289,7 @@ GncQuotesImpl::fetch (gnc_commodity *comm)
 void
 GncQuotesImpl::fetch (CommVec& commodities)
 {
+    m_failures.clear();
     if (commodities.empty())
         return;
 
@@ -290,6 +297,66 @@ GncQuotesImpl::fetch (CommVec& commodities)
     parse_quotes (quote_str, commodities);
 }
 
+const QFVec&
+GncQuotesImpl::failures() noexcept
+{
+    return m_failures;
+}
+
+static std::string
+explain(GncQuoteError err, const std::string& errmsg)
+{
+    std::string retval;
+    switch (err)
+    {
+    case GncQuoteError::NO_RESULT:
+        if (errmsg.empty())
+            retval += _("Finance::Quote returned no data and set no error.");
+        else
+            retval += _("Finance::Quote returned an error: ") + errmsg;
+        break;
+    case GncQuoteError::QUOTE_FAILED:
+        if (errmsg.empty())
+            retval += _("Finance::Quote reported failure set no error.");
+        else
+            retval += _("Finance::Quote reported failure with  error: ") + errmsg;
+        break;
+    case GncQuoteError::NO_CURRENCY:
+        retval += _("Finance::Quote returned a quote with no currency.");
+        break;
+    case GncQuoteError::UNKNOWN_CURRENCY:
+        retval += _("Finance::Quote returned a quote with a currency GnuCash doesn't know about.");
+        break;
+    case GncQuoteError::NO_PRICE:
+        retval += _("Finance::Quote returned a quote with no price element.");
+        break;
+    case GncQuoteError::PRICE_PARSE_FAILURE:
+        retval += _("Finance::Quote returned a quote with a price that GnuCash was unable to covert to a number.");
+        break;
+    case GncQuoteError::SUCCESS:
+    default:
+        retval += _("The quote has no error set.");
+        break;
+    }
+    return retval;
+}
+
+std::string
+GncQuotesImpl::report_failures() noexcept
+{
+    std::string retval{_("Quotes for the following commodities were unavailable or unusable:\n")};
+    std::for_each(m_failures.begin(), m_failures.end(),
+                  [&retval](auto failure)
+                  {
+                      auto [ns, sym, reason, err] = failure;
+                      retval += "* " + ns + ":" + sym + " " +
+                          explain(reason, err) + "\n";
+                  });
+    return retval;
+}
+
+/* **** Private function implementations ****/
+
 std::string
 GncQuotesImpl::comm_vec_to_json_string (const CommVec& comm_vec) const
 {
@@ -368,11 +435,13 @@ get_price_and_type(PriceParams& p, const bpt::ptree& comm_pt)
 {
     p.type = "last";
     p.price = comm_pt.get_optional<std::string> (p.type);
+
     if (!p.price)
     {
         p.type = "nav";
         p.price = comm_pt.get_optional<std::string> (p.type);
     }
+
     if (!p.price)
     {
         p.type = "price";
@@ -472,11 +541,13 @@ get_price(const PriceParams& p)
 }
 
 static gnc_commodity*
-get_currency(const PriceParams& p, QofBook* book)
+get_currency(const PriceParams& p, QofBook* book, QFVec& failures)
 {
     if (!p.currency)
     {
-        PWARN("Skipped %s:%s - Finance::Quote didn't return a currency",
+        failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_CURRENCY,
+                              empty_string);
+        PWARN("Skipped %s:%s - Finance::Quote returned a quote with no  currency",
               p.ns, p.mnemonic);
         return nullptr;
     }
@@ -487,6 +558,8 @@ get_currency(const PriceParams& p, QofBook* book)
 
     if (!currency)
     {
+        failures.emplace_back(p.ns, p.mnemonic,
+                              GncQuoteError::UNKNOWN_CURRENCY, empty_string);
         PWARN("Skipped %s:%s  - failed to parse returned currency '%s'",
               p.ns, p.mnemonic, p.currency->c_str());
         return nullptr;
@@ -507,6 +580,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
     auto comm_pt_ai{pt.find(p.mnemonic)};
     if (comm_pt_ai == pt.not_found())
     {
+        m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_RESULT,
+                                empty_string);
         PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
               p.ns, p.mnemonic);
         return nullptr;
@@ -517,6 +592,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
 
     if (!p.success)
     {
+        m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::QUOTE_FAILED,
+                                p.errormsg ? *p.errormsg : empty_string);
         PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
               p.ns, p.mnemonic,
               (p.errormsg ? p.errormsg->c_str() : "unknown"));
@@ -525,6 +602,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
 
     if (!p.price)
     {
+        m_failures.emplace_back(p.ns, p.mnemonic,
+                                GncQuoteError::NO_PRICE, empty_string);
         PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
               p.ns, p.mnemonic);
         return nullptr;
@@ -532,11 +611,16 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
 
     auto price{get_price(p)};
     if (!price)
+    {
+        m_failures.emplace_back(p.ns, p.mnemonic,
+                                GncQuoteError::PRICE_PARSE_FAILURE,
+                                empty_string);
         return nullptr;
+    }
 
-    auto currency{get_currency(p, m_book)};
+    auto currency{get_currency(p, m_book, m_failures)};
     if (!currency)
-        return nullptr;
+       return nullptr;
 
     auto quotedt{calc_price_time(p)};
     auto gnc_price = gnc_price_create (m_book);
@@ -737,3 +821,14 @@ GList* GncQuotes::sources_as_glist ()
 
 GncQuotes::~GncQuotes() = default;
 
+const QFVec&
+GncQuotes::failures() noexcept
+{
+    return m_impl->failures();
+}
+
+const std::string
+GncQuotes::report_failures() noexcept
+{
+    return m_impl->report_failures();
+}
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index dea38c623..b3635f779 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -33,9 +33,27 @@ extern  "C" {
 #include <qofbook.h>
 }
 
-using StrVec = std::vector  <std::string>;
+using StrVec = std::vector<std::string>;
 using QuoteSources = StrVec;
-using CmdOutput = std::pair <StrVec, StrVec>;
+
+enum class GncQuoteError
+{
+    SUCCESS,
+    NO_RESULT,
+    QUOTE_FAILED,
+    NO_CURRENCY,
+    UNKNOWN_CURRENCY,
+    NO_PRICE,
+    UNKNOWN_PRICE_TYPE,
+    PRICE_PARSE_FAILURE,
+};
+
+/** QuoteFailure elements are namespace, mnemonic, error code, and
+ * F::Q errormsg if there is one.
+ */
+using QuoteFailure = std::tuple<std::string, std::string,
+                                GncQuoteError, std::string>;
+using QFVec = std::vector<QuoteFailure>;
 
 struct GncQuoteException : public std::runtime_error
 {
@@ -93,6 +111,23 @@ public:
      */
     GList* sources_as_glist () ;
 
+    /** Report the commodities for which quotes were requested but not successfully retrieved.
+     *
+     * This does not include requested commodities that didn't have a quote source.
+     *
+     * @return a reference to a vector of QuoteFailure tuples.
+     * @note The vector and its contents belong to the GncQuotes object and will be destroyed with it.
+     */
+    const QFVec& failures() noexcept;
+
+    /* Report the commodities for which quotes were requested but not successfully retrieved.
+     *
+     * This does not include requested commodities that didn't have a quote source.
+     *
+     * @return A localized std::string with an intro and a list of the quote failures with a cause. The string is owned by the caller.
+     */
+    const std::string report_failures() noexcept;
+
 private:
     std::unique_ptr<GncQuotesImpl> m_impl;
 };
diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index a7fe4bc92..ebcd66d04 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -141,7 +141,11 @@ TEST_F(GncQuotesTest, online_wiggle)
     GncQuotes quotes;
     quotes.fetch(m_book);
     auto pricedb{gnc_pricedb_get_db(m_book)};
-    EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
+    auto failures{quotes.failures()};
+    ASSERT_EQ(2u, failures.size());
+    EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[0]));
+    EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[1]));
+    EXPECT_EQ(2u, gnc_pricedb_get_num_prices(pricedb));
 }
 #endif
 
@@ -153,6 +157,9 @@ TEST_F(GncQuotesTest, offline_wiggle)
     StrVec err_vec;
     GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
     quotes.fetch(m_book);
+    auto failures{quotes.failures()};
+    ASSERT_EQ(1u, failures.size());
+    EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[0]));
     auto pricedb{gnc_pricedb_get_db(m_book)};
     EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
 }
@@ -169,7 +176,9 @@ TEST_F(GncQuotesTest, comvec_fetch)
     CommVec comms{hpe, aapl};
     GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
     quotes.fetch(comms);
-        auto pricedb{gnc_pricedb_get_db(m_book)};
+    auto failures{quotes.failures()};
+    EXPECT_TRUE(failures.empty());
+    auto pricedb{gnc_pricedb_get_db(m_book)};
     EXPECT_EQ(2u, gnc_pricedb_get_num_prices(pricedb));
 }
 
@@ -184,6 +193,8 @@ TEST_F(GncQuotesTest, fetch_one_commodity)
     auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
     GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
     quotes.fetch(hpe);
+    auto failures{quotes.failures()};
+    EXPECT_TRUE(failures.empty());
     auto pricedb{gnc_pricedb_get_db(m_book)};
     auto price{gnc_pricedb_lookup_latest(pricedb, hpe, usd)};
     auto datetime{static_cast<time64>(GncDateTime("20220901160000"))};
@@ -208,8 +219,11 @@ TEST_F(GncQuotesTest, fetch_one_currency)
     auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
     GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
     quotes.fetch(eur);
+    auto failures{quotes.failures()};
+    EXPECT_TRUE(failures.empty());
     auto pricedb{gnc_pricedb_get_db(m_book)};
     auto price{gnc_pricedb_lookup_latest(pricedb, eur, usd)};
+    EXPECT_EQ(1u, gnc_pricedb_get_num_prices(pricedb));
     auto datetime{static_cast<time64>(GncDateTime())};
 
     EXPECT_EQ(usd, gnc_price_get_currency(price));
@@ -222,3 +236,65 @@ TEST_F(GncQuotesTest, fetch_one_currency)
     EXPECT_STREQ("last", gnc_price_get_typestr(price));
 }
 
+TEST_F(GncQuotesTest, no_currency)
+{
+     StrVec quote_vec{
+        "{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"success\":1}}"
+    };
+    StrVec err_vec;
+    auto commtable{gnc_commodity_table_get_table(m_book)};
+    auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
+    auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.fetch(hpe);
+    auto failures{quotes.failures()};
+    ASSERT_EQ(1u, failures.size());
+    EXPECT_EQ(GncQuoteError::NO_CURRENCY, std::get<2>(failures[0]));
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    EXPECT_EQ(0u, gnc_pricedb_get_num_prices(pricedb));
+}
+
+TEST_F(GncQuotesTest, bad_currency)
+{
+     StrVec quote_vec{
+        "{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"currency\":\"BTC\",\"success\":1}}"
+     };
+    StrVec err_vec;
+    auto commtable{gnc_commodity_table_get_table(m_book)};
+    auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
+    auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.fetch(hpe);
+    auto failures{quotes.failures()};
+    ASSERT_EQ(1u, failures.size());
+    EXPECT_EQ(GncQuoteError::UNKNOWN_CURRENCY, std::get<2>(failures[0]));
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    EXPECT_EQ(0u, gnc_pricedb_get_num_prices(pricedb));
+}
+
+TEST_F(GncQuotesTest, no_date)
+{
+     StrVec quote_vec{
+        "{\"HPE\":{\"last\":13.37,\"currency\":\"USD\",\"success\":1}}"
+    };
+    StrVec err_vec;
+    auto commtable{gnc_commodity_table_get_table(m_book)};
+    auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
+    auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.fetch(hpe);
+    auto failures{quotes.failures()};
+    EXPECT_TRUE(failures.empty());
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    auto price{gnc_pricedb_lookup_latest(pricedb, hpe, usd)};
+    auto datetime{static_cast<time64>(GncDateTime())};
+
+    EXPECT_EQ(usd, gnc_price_get_currency(price));
+    EXPECT_EQ(datetime, gnc_price_get_time64(price));
+    EXPECT_EQ(PRICE_SOURCE_FQ, gnc_price_get_source(price));
+    EXPECT_TRUE(gnc_numeric_equal(GncNumeric{1337, 100},
+                                  gnc_price_get_value(price)));
+    EXPECT_STREQ("Finance::Quote", gnc_price_get_source_string(price));
+    EXPECT_STREQ("last", gnc_price_get_typestr(price));
+}
+

commit 6db7800ca523c56c317d9fe572401819eb4f9511
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Sep 17 11:03:53 2022 -0700

    [price-quotes] Doxygen docs.

diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index e2968958a..dea38c623 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -49,20 +49,49 @@ class GncQuotesImpl;
 class GncQuotes
 {
 public:
-    // Constructor - checks for presence of Finance::Quote and import version and quote sources
+    /** Create a GncQuotes object.
+     *
+     * Throws a GncQuoteException if Finance::Quote is not installed or fails to initialize.
+     */
     GncQuotes ();
     ~GncQuotes ();
 
-    // Fetch quotes for all commodities in our db that have a quote source set
+    /** Fetch quotes for all commodities in our db that have a quote source set
+     *
+     * @param book The current book.
+     */
     void fetch (QofBook *book);
-    // Only fetch quotes for the commodities passed that have a quote source  set
+    /** Fetch quotes for a vector of commodities
+     *
+     * @param commodities std::vector of the gnc_commodity* to get quotes for.
+     * @note Commodities without a quote source will be silently ignored.
+     */
     void fetch (CommVec& commodities);
-    // Fetch quote for the commodity if it has a quote source  set
+    /** Fetch quote for a single commodity
+     *
+     * @param comm Commodity for which to retrieve a quote
+     * @note Commodity must have a quote source set or the call will silently fail.
+     */
     void fetch (gnc_commodity *comm);
 
+    /** Get the installed Finance::Quote version
+     *
+     * @return the Finance::Quote version string
+     */
     const std::string& version() noexcept;
+
+    /** Get the available Finance::Quote sources as a std::vector
+     *
+     * @return The quote sources configured in Finance::Quote
+     */
     const QuoteSources& sources() noexcept;
-    GList* sources_as_glist ();
+
+    /** Get the available Finance::Quote sources as a GLixt
+     *
+     * @return A double-linked list containing the names of the installed quote sources.
+     * @note the list and its contents are owned by the caller and should be freed with `g_list_free_full(list, g_free)`.
+     */
+    GList* sources_as_glist () ;
 
 private:
     std::unique_ptr<GncQuotesImpl> m_impl;

commit 29ce9256463495894ac929f0885eab811b1c4a07
Author: John Ralls <jralls at ceridwen.us>
Date:   Mon Sep 12 18:39:18 2022 -0700

    [price-quotes] Test the other fetch overloads and quote values.

diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index 666d783bb..a7fe4bc92 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -64,6 +64,7 @@ GncMockQuoteSource::get_quotes(const std::string& json_string) const
 {
     return {0, m_quotes, m_errors};
 }
+
 class GncQuotesTest : public ::testing::Test
 {
 protected:
@@ -155,3 +156,69 @@ TEST_F(GncQuotesTest, offline_wiggle)
     auto pricedb{gnc_pricedb_get_db(m_book)};
     EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
 }
+
+TEST_F(GncQuotesTest, comvec_fetch)
+{
+     StrVec quote_vec{
+        "{\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1}}"
+    };
+    StrVec err_vec;
+    auto commtable{gnc_commodity_table_get_table(m_book)};
+    auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
+    auto aapl{gnc_commodity_table_lookup(commtable, "NASDAQ", "AAPL")};
+    CommVec comms{hpe, aapl};
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.fetch(comms);
+        auto pricedb{gnc_pricedb_get_db(m_book)};
+    EXPECT_EQ(2u, gnc_pricedb_get_num_prices(pricedb));
+}
+
+TEST_F(GncQuotesTest, fetch_one_commodity)
+{
+     StrVec quote_vec{
+        "{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"currency\":\"USD\",\"success\":1}}"
+    };
+    StrVec err_vec;
+    auto commtable{gnc_commodity_table_get_table(m_book)};
+    auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
+    auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.fetch(hpe);
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    auto price{gnc_pricedb_lookup_latest(pricedb, hpe, usd)};
+    auto datetime{static_cast<time64>(GncDateTime("20220901160000"))};
+
+    EXPECT_EQ(usd, gnc_price_get_currency(price));
+    EXPECT_EQ(datetime, gnc_price_get_time64(price));
+    EXPECT_EQ(PRICE_SOURCE_FQ, gnc_price_get_source(price));
+    EXPECT_TRUE(gnc_numeric_equal(GncNumeric{1337, 100},
+                                  gnc_price_get_value(price)));
+    EXPECT_STREQ("Finance::Quote", gnc_price_get_source_string(price));
+    EXPECT_STREQ("last", gnc_price_get_typestr(price));
+}
+
+TEST_F(GncQuotesTest, fetch_one_currency)
+{
+     StrVec quote_vec{
+         "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004}}"
+    };
+    StrVec err_vec;
+    auto commtable{gnc_commodity_table_get_table(m_book)};
+    auto eur{gnc_commodity_table_lookup(commtable, "ISO4217", "EUR")};
+    auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
+    quotes.fetch(eur);
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    auto price{gnc_pricedb_lookup_latest(pricedb, eur, usd)};
+    auto datetime{static_cast<time64>(GncDateTime())};
+
+    EXPECT_EQ(usd, gnc_price_get_currency(price));
+    EXPECT_EQ(datetime, gnc_price_get_time64(price));
+    EXPECT_EQ(PRICE_SOURCE_FQ, gnc_price_get_source(price));
+    EXPECT_EQ(10004, gnc_price_get_value(price).num);
+    EXPECT_TRUE(gnc_numeric_equal(GncNumeric{10004, 10000},
+                                  gnc_price_get_value(price)));
+    EXPECT_STREQ("Finance::Quote", gnc_price_get_source_string(price));
+    EXPECT_STREQ("last", gnc_price_get_typestr(price));
+}
+

commit b5bc6463a313beeb7b58470d0ff87f0c08135bc0
Author: John Ralls <jralls at ceridwen.us>
Date:   Mon Sep 12 18:17:12 2022 -0700

    [price-quotes] Rework date-time handling.
    
    A check of the F::Q modules found that the only ones that return a quote
    time return a bogus one and do so only to mollify GnuCash.
    
    Since there's no good way to determine the TZ of the exchange originating
    the quote there's no good way to decide if the quote is current or from
    a previous market session, so we just punt and use a time of 16:00 for
    all quotes.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 7d93f20a7..94dc064c8 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -410,32 +410,45 @@ parse_quote_json(PriceParams& p, const bpt::ptree& comm_pt)
 static time64
 calc_price_time(const PriceParams& p)
 {
-    time64 quote_time;
-    std::string iso_date_str = GncDate().format ("%Y-%m-%d");
+    /* Note that as of F::Q v. 1.52 the only sources that provide
+     * quote times are ftfunds (aka ukfunds), morningstarch, and
+     * mstaruk_fund, but it's faked with a comment "Set a dummy time
+     * as gnucash insists on having a valid format". It's also wrong,
+     * as it lacks seconds. Best ignored.
+     */
     if (p.date)
     {
-        // Returned date is always in MM/DD/YYYY format according to F::Q man page, transform it to simplify conversion to GncDateTime
+        /* Returned date is always in MM/DD/YYYY format according to
+         * F::Q man page, transform it to simplify conversion to
+         * GncDateTime.
+         */
         auto date_tmp = *p.date;
-        iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) + "-" + date_tmp.substr (3, 2);
+        auto iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) +
+            "-" + date_tmp.substr (3, 2);
+        try
+        {
+            auto close_time{GncDateTime(iso_date_str + " 16:00:00")};
+            PINFO("Quote date included, using %s for %s:%s",
+                  close_time.format("%Y-%m-%d %H:%M:%S").c_str(), p.ns, p.mnemonic);
+            return static_cast<time64>(close_time);
+        }
+        catch (...)
+        {
+            auto now{GncDateTime()};
+            PWARN("Warning: failed to parse quote date '%s' for %s:%s - will use %s",
+                  iso_date_str.c_str(),  p.ns, p.mnemonic, now.format("%Y-%m-%d %H:%M%S").c_str());
+            return static_cast<time64>(now);
+        }
     }
     else
-        PINFO("Info: no date  was returned for %s:%s - will use today %s",
-              p.ns, p.mnemonic,
-              (iso_date_str += " " + (p.time ? *p.time : "12:00:00")).c_str());
-
-    auto can_convert = true;
-    try
-    {
-        quote_time = static_cast<time64>(GncDateTime{iso_date_str});
-    }
-    catch (...)
     {
-        PINFO("Warning: failed to parse quote date and time '%s' for %s:%s - will use today",
-              iso_date_str.c_str(),  p.ns, p.mnemonic);
-        quote_time = static_cast<time64>(GncDateTime());
+        auto now{GncDateTime()};
+        PINFO("Info: no date  was returned for %s:%s - will use %s",
+              p.ns, p.mnemonic, now.format("%Y-%m-%d %H:%M%S").c_str());
+        return static_cast<time64>(now);
     }
 
-    return quote_time;
+    return INT64_MAX; //Shouldn't be able to get here.
 }
 
 static boost::optional<GncNumeric>

commit 19064093d2ae0326263215dba8be98ff8bd350c9
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Sep 10 17:29:23 2022 -0700

    [price-quotes] Remove m_comm_vec and m_fq_answer.
    
    Passing the intermediate values comm_vec and quote_str on the stack instead.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index d65a89294..7d93f20a7 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -98,16 +98,14 @@ public:
     GList* sources_as_glist ();
 
 private:
-    void query_fq (void);
-    void parse_quotes (void);
-    std::string comm_vec_to_json_string(void) const;
+    std::string query_fq (const CommVec&);
+    void parse_quotes (const std::string& quote_str, const CommVec& commodities);
+    std::string comm_vec_to_json_string(const CommVec&) const;
     GNCPrice* parse_one_quote(const bpt::ptree&, gnc_commodity*);
 
     std::unique_ptr<GncQuoteSource> m_quotesource;
-    CommVec m_comm_vec;
     std::string m_version;
     QuoteSources m_sources;
-    std::string m_fq_answer;
     QofBook *m_book;
     gnc_commodity *m_dflt_curr;
 };
@@ -288,26 +286,17 @@ GncQuotesImpl::fetch (CommVec& commodities)
     if (commodities.empty())
         return;
 
-    m_comm_vec = std::move (commodities);  // Store for later use
-    m_book = qof_instance_get_book (m_comm_vec[0]);
-
-    query_fq ();
-    parse_quotes ();
-}
-
-static const std::vector <std::string>
-format_quotes (const std::vector<gnc_commodity*>)
-{
-    return std::vector <std::string>();
+    auto quote_str{query_fq (commodities)};
+    parse_quotes (quote_str, commodities);
 }
 
 std::string
-GncQuotesImpl::comm_vec_to_json_string (void) const
+GncQuotesImpl::comm_vec_to_json_string (const CommVec& comm_vec) const
 {
     bpt::ptree pt, pt_child;
     pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
 
-    std::for_each (m_comm_vec.cbegin(), m_comm_vec.cend(),
+    std::for_each (comm_vec.cbegin(), comm_vec.cend(),
                    [this, &pt] (auto comm)
                    {
                        auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
@@ -331,17 +320,17 @@ GncQuotesImpl::comm_vec_to_json_string (void) const
     return result.str();
 }
 
-void
-GncQuotesImpl::query_fq (void)
+std::string
+GncQuotesImpl::query_fq (const CommVec& comm_vec)
 {
-    auto json_str{comm_vec_to_json_string()};
+    auto json_str{comm_vec_to_json_string(comm_vec)};
     auto [rv, quotes, errors] = m_quotesource->get_quotes(json_str);
-    m_fq_answer.clear();
+    std::string answer;
 
     if (rv == 0)
     {
         for (auto line : quotes)
-            m_fq_answer.append(line + "\n");
+            answer.append(line + "\n");
     }
     else
     {
@@ -350,6 +339,8 @@ GncQuotesImpl::query_fq (void)
             err_str.append(line + "\n");
         throw(GncQuoteException(err_str));
     }
+
+    return answer;
 //        for (auto line : quotes)
 //            PINFO("Output line retrieved from wrapper:\n%s", line.c_str());
 //
@@ -548,10 +539,10 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
 }
 
 void
-GncQuotesImpl::parse_quotes (void)
+GncQuotesImpl::parse_quotes (const std::string& quote_str, const CommVec& comm_vec)
 {
     bpt::ptree pt;
-    std::istringstream ss {m_fq_answer};
+    std::istringstream ss {quote_str};
     const char* what = nullptr;
 
     try
@@ -584,7 +575,7 @@ GncQuotesImpl::parse_quotes (void)
     }
 
     auto pricedb{gnc_pricedb_get_db(m_book)};
-    for (auto comm : m_comm_vec)
+    for (auto comm : comm_vec)
     {
         auto price{parse_one_quote(pt, comm)};
         if (!price)

commit 734fb6ce2a7d875491794d1e7d0605a1ef1ed033
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Sep 10 16:18:32 2022 -0700

    [price-quotes] Switch error handling to exceptions.
    
    Allows for cleaner code with less state, less coupling of the GncQuotes
    class, and better transfer of error messages to client code.
    
    Also translates some error messages for presentation to users.

diff --git a/gnucash/gnome-utils/dialog-transfer.cpp b/gnucash/gnome-utils/dialog-transfer.cpp
index 9a818d558..803c7ae9d 100644
--- a/gnucash/gnome-utils/dialog-transfer.cpp
+++ b/gnucash/gnome-utils/dialog-transfer.cpp
@@ -1785,18 +1785,19 @@ gnc_xfer_dialog_fetch (GtkButton *button, XferDialog *xferData)
 
     ENTER(" ");
 
-    GncQuotes quotes;
-    if (quotes.cmd_result() != 0)
+    try
     {
-        if (!quotes.error_msg().empty())
-            PWARN ("%s", quotes.error_msg().c_str());
-        LEAVE("quote retrieval failed");
-        return;
+        GncQuotes quotes;
+        gnc_set_busy_cursor(nullptr, TRUE);
+        quotes.fetch(xferData->book);
+        gnc_unset_busy_cursor(nullptr);
+    }
+    catch (const GncQuoteException& err)
+    {
+        gnc_unset_busy_cursor(nullptr);
+        PERR("Price retrieval failed: %s", err.what());
+        gnc_error_dialog(GTK_WINDOW(xferData->dialog), _("Price retrieval failed: %s"), err.what());
     }
-
-    gnc_set_busy_cursor (nullptr, TRUE);
-    quotes.fetch (xferData->book);
-    gnc_unset_busy_cursor (nullptr);
 
     /*the results should be in the price db now, but don't crash if not. */
     PriceReq pr;
diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index bec4fdb31..81265dc34 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -559,19 +559,19 @@ gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data)
     auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
-    GncQuotes quotes;
-    if (quotes.cmd_result() != 0)
+    try {
+        GncQuotes quotes;
+        gnc_set_busy_cursor (NULL, TRUE);
+        quotes.fetch (pdb_dialog->book);
+        gnc_unset_busy_cursor (NULL);
+    }
+    catch (const GncQuoteException& err)
     {
-        if (!quotes.error_msg().empty())
-            PWARN ("%s", quotes.error_msg().c_str());
-        LEAVE("quote retrieval failed");
-        return;
+        gnc_unset_busy_cursor(nullptr);
+        PERR("Price retrieval failed: %s", err.what());
+        gnc_error_dialog(GTK_WINDOW(pdb_dialog), _("Price retrieval failed: %s"), err.what());
     }
 
-    gnc_set_busy_cursor (NULL, TRUE);
-    quotes.fetch (pdb_dialog->book);
-    gnc_unset_busy_cursor (NULL);
-
     /* Without this, the summary bar on the accounts tab
      * won't reflect the new prices (bug #522095). */
     gnc_gui_refresh_all ();
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index 145cd009c..6eb627517 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -303,9 +303,9 @@ int
 Gnucash::quotes_info (void)
 {
     gnc_prefs_init ();
-    GncQuotes quotes;
-    if (quotes.cmd_result() == 0)
+    try
     {
+        GncQuotes quotes;
         std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << "\n";
         std::cout << bl::translate ("Finance::Quote sources: ");
         for (auto source : quotes.sources())
@@ -313,12 +313,9 @@ Gnucash::quotes_info (void)
         std::cout << std::endl;
         return 0;
     }
-    else
+    catch (const GncQuoteException& err)
     {
-        std::cerr << bl::translate ("Finance::Quote isn't "
-                                    "installed properly.") << "\n";
-        std::cerr << bl::translate ("Error message:") << "\n";
-        std::cerr << quotes.error_msg() << std::endl;
+        std::cout << err.what() << std::endl;
         return 1;
     }
 }
@@ -341,32 +338,24 @@ Gnucash::add_quotes (const bo_str& uri)
     if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
         cleanup_and_exit_with_failure (session);
 
-    GncQuotes quotes;
-    if (quotes.cmd_result() == 0)
+    try
     {
+        GncQuotes quotes;
         std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
         auto quote_sources = quotes.sources_as_glist();
         gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
         g_list_free_full (quote_sources, g_free);
+        quotes.fetch(qof_session_get_book(session));
     }
-    else
+    catch (const GncQuoteException& err)
     {
-        std::cerr << bl::translate ("No quotes retrieved. Finance::Quote isn't "
-                                    "installed properly.") << "\n";
-        std::cerr << bl::translate ("Error message:") << std::endl;
-        std::cerr << quotes.error_msg() << std::endl;
+        std::cerr << err.what() << std::endl;
     }
-    quotes.fetch (qof_session_get_book(session));
-
     qof_session_save(session, NULL);
     if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
         cleanup_and_exit_with_failure (session);
 
     qof_session_destroy(session);
-
-    if (quotes.cmd_result() != 0)
-        std::cerr << bl::format (bl::translate ("Failed to add quotes to {1}.")) % *uri << "\n";
-
     qof_event_resume();
     return 0;
 }
diff --git a/gnucash/gnucash.cpp b/gnucash/gnucash.cpp
index fe7513f1a..77ba65e68 100644
--- a/gnucash/gnucash.cpp
+++ b/gnucash/gnucash.cpp
@@ -174,25 +174,25 @@ scm_run_gnucash (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **
     gnc_hook_add_dangler(HOOK_UI_SHUTDOWN, (GFunc)gnc_file_quit, NULL, NULL);
 
     /* Install Price Quote Sources */
-    auto msg = bl::translate ("Checking Finance::Quote...").str(gnc_get_boost_locale());
 
-    GncQuotes quotes;
-    if (quotes.cmd_result() == 0)
+    try
     {
-        msg = (bl::format (bl::translate("Found Finance::Quote version {1}.")) % quotes.version()).str(gnc_get_boost_locale());
+        auto msg = bl::translate ("Checking Finance::Quote...").str(gnc_get_boost_locale());
+        GncQuotes quotes;
+            msg = (bl::format (bl::translate("Found Finance::Quote version {1}.")) % quotes.version()).str(gnc_get_boost_locale());
         auto quote_sources = quotes.sources_as_glist();
         gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
         g_list_free (quote_sources);
+        gnc_update_splash_screen (msg.c_str(), GNC_SPLASH_PERCENTAGE_UNKNOWN);
     }
-    else
+    catch (const GncQuoteException& err)
     {
-        msg = bl::translate("Unable to load Finance::Quote.").str(gnc_get_boost_locale());
+        auto msg = bl::translate("Unable to load Finance::Quote.").str(gnc_get_boost_locale());
         PINFO ("Attempt to load Finance::Quote returned this error message:\n");
-        PINFO ("%s", quotes.error_msg().c_str());
+        PINFO ("%s", err.what());
+        gnc_update_splash_screen (msg.c_str(), GNC_SPLASH_PERCENTAGE_UNKNOWN);
     }
 
-    gnc_update_splash_screen (msg.c_str(), GNC_SPLASH_PERCENTAGE_UNKNOWN);
-
     gnc_hook_run(HOOK_STARTUP, NULL);
 
     if (!user_file_spec->nofile && (fn = get_file_to_load (user_file_spec->file_to_load)) && *fn )
diff --git a/libgnucash/app-utils/CMakeLists.txt b/libgnucash/app-utils/CMakeLists.txt
index 3192c5d58..ac9179994 100644
--- a/libgnucash/app-utils/CMakeLists.txt
+++ b/libgnucash/app-utils/CMakeLists.txt
@@ -50,6 +50,7 @@ set(app_utils_ALL_LIBRARIES
     gnc-engine
     ${Boost_FILESYSTEM_LIBRARY}
     ${Boost_PROPERTY_TREE_LIBRARY}
+    ${Boost_LOCALE_LIBRARY}
     ${GIO_LDFLAGS}
     ${LIBXML2_LDFLAGS}
     ${LIBXSLT_LDFLAGS}
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 6d595edf1..d65a89294 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include <algorithm>
+#include <stdexcept>
 #include <vector>
 #include <string>
 #include <iostream>
@@ -34,6 +35,7 @@
 #include <boost/property_tree/json_parser.hpp>
 #include <boost/iostreams/device/array.hpp>
 #include <boost/iostreams/stream_buffer.hpp>
+#include <boost/locale.hpp>
 #include <boost/asio.hpp>
 #include <glib.h>
 #include "gnc-commodity.hpp"
@@ -53,6 +55,7 @@ extern "C" {
 
 static const QofLogModule log_module = "gnc.price-quotes";
 
+namespace bl = boost::locale;
 namespace bp = boost::process;
 namespace bfs = boost::filesystem;
 namespace bpt = boost::property_tree;
@@ -60,6 +63,11 @@ namespace bio = boost::iostreams;
 
 using QuoteResult = std::tuple<int, StrVec, StrVec>;
 
+struct GncQuoteSourceError : public std::runtime_error
+{
+    GncQuoteSourceError(const std::string& err) : std::runtime_error(err) {}
+};
+
 CommVec
 gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
 
@@ -85,8 +93,6 @@ public:
     void fetch (CommVec& commodities);
     void fetch (gnc_commodity *comm);
 
-    int cmd_result() const noexcept { return m_cmd_result; }
-    const std::string& error_msg() noexcept { return m_error_msg; }
     const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
     const QuoteSources& sources() noexcept { return m_sources; }
     GList* sources_as_glist ();
@@ -101,8 +107,6 @@ private:
     CommVec m_comm_vec;
     std::string m_version;
     QuoteSources m_sources;
-    int m_cmd_result;
-    std::string m_error_msg;
     std::string m_fq_answer;
     QofBook *m_book;
     gnc_commodity *m_dflt_curr;
@@ -138,21 +142,25 @@ m_version{}, m_sources{}
     auto [rv, sources, errors] = run_cmd(args, empty_string);
     if (rv)
     {
-        PERR("Failed to initialize Finance::Quote %s", errors.front().c_str());
-        return;
+        std::string err{bl::translate("Failed to initialize Finance::Quote: ")};
+        for (auto err_line : errors)
+            err += err_line.empty() ? "" : err_line + "\n";
+        throw(GncQuoteSourceError(err));
     }
     if (!errors.empty())
     {
-        for(const auto& err : errors)
-            PERR("Finance::Quote check returned error %s", err.empty() ? "" : err.c_str());
-        return;
+        std::string err{bl::translate("Finance::Quote check returned error ")};
+        for(const auto& err_line : errors)
+            err += err.empty() ? "" : err_line + "\n";
+        throw(GncQuoteSourceError(err));
     }
     static const boost::regex version_fmt{"[0-9]\\.[0-9][0-9]"};
     auto version{sources.front()};
     if (version.empty() || !boost::regex_match(version, version_fmt))
     {
-        PERR("Invalid Finance::Quote Version %s", version.empty() ? "" : version.c_str());
-        return;
+        std::string err{bl::translate("Invalid Finance::Quote Version ")};
+            err +=  version.empty() ? "" : version;
+        throw(GncQuoteSourceError(err));
     }
     m_ready = true;
     sources.erase(sources.begin());
@@ -221,7 +229,7 @@ GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) c
 
 /* GncQuotes implementation */
 GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
-m_version{}, m_sources{}, m_cmd_result{}, m_error_msg{}, m_book{qof_session_get_book(gnc_get_current_session())},
+m_version{}, m_sources{}, m_book{qof_session_get_book(gnc_get_current_session())},
 m_dflt_curr{gnc_default_currency()}
 {
     if (!m_quotesource->usable())
@@ -230,7 +238,7 @@ m_dflt_curr{gnc_default_currency()}
 }
 
 GncQuotesImpl::GncQuotesImpl(QofBook* book) : m_quotesource{new GncFQQuoteSource},
-m_version{}, m_sources{}, m_cmd_result{}, m_error_msg{}, m_book{book},
+m_version{}, m_sources{}, m_book{book},
 m_dflt_curr{gnc_default_currency()}
 {
     if (!m_quotesource->usable())
@@ -240,8 +248,7 @@ m_dflt_curr{gnc_default_currency()}
 
 GncQuotesImpl::GncQuotesImpl(QofBook* book, std::unique_ptr<GncQuoteSource> quote_source) :
 m_quotesource{std::move(quote_source)},
-m_version{}, m_sources{}, m_cmd_result{}, m_error_msg{}, m_book{book},
-m_dflt_curr{gnc_default_currency()}
+m_version{}, m_sources{}, m_book{book}, m_dflt_curr{gnc_default_currency()}
 {
     if (!m_quotesource->usable())
         return;
@@ -262,12 +269,7 @@ void
 GncQuotesImpl::fetch (QofBook *book)
 {
     if (!book)
-    {
-        m_cmd_result = 1;
-        m_error_msg = _("No book set");
-        m_error_msg += "\n";
-        return;
-    }
+        throw (GncQuoteException(bl::translate("GncQuotes::Fetch called with no book.")));
     auto commodities = gnc_quotes_get_quotable_commodities (
         gnc_commodity_table_get_table (book));
     fetch (commodities);
@@ -290,8 +292,7 @@ GncQuotesImpl::fetch (CommVec& commodities)
     m_book = qof_instance_get_book (m_comm_vec[0]);
 
     query_fq ();
-    if (m_cmd_result == 0)
-        parse_quotes ();
+    parse_quotes ();
 }
 
 static const std::vector <std::string>
@@ -336,14 +337,19 @@ GncQuotesImpl::query_fq (void)
     auto json_str{comm_vec_to_json_string()};
     auto [rv, quotes, errors] = m_quotesource->get_quotes(json_str);
     m_fq_answer.clear();
-    m_cmd_result = rv;
+
     if (rv == 0)
+    {
         for (auto line : quotes)
             m_fq_answer.append(line + "\n");
+    }
     else
-        for (auto line : errors)
-            m_error_msg.append(line + "\n");
-
+    {
+        std::string err_str;
+        for (auto line: errors)
+            err_str.append(line + "\n");
+        throw(GncQuoteException(err_str));
+    }
 //        for (auto line : quotes)
 //            PINFO("Output line retrieved from wrapper:\n%s", line.c_str());
 //
@@ -546,24 +552,35 @@ GncQuotesImpl::parse_quotes (void)
 {
     bpt::ptree pt;
     std::istringstream ss {m_fq_answer};
+    const char* what = nullptr;
 
     try
     {
         bpt::read_json (ss, pt);
     }
     catch (bpt::json_parser_error &e) {
-        m_cmd_result = -1;
-        m_error_msg = m_error_msg +
-                      _("Failed to parse result returned by Finance::Quote.") + "\n" +
-                      _("Error message:") + "\n" +
-                       e.what() + "\n";
-        return;
+        what = e.what();
+    }
+    catch (const std::runtime_error& e)
+    {
+        what = e.what();
+    }
+    catch (const std::logic_error& e)
+    {
+        what = e.what();
     }
     catch (...) {
-        m_cmd_result = -1;
-        m_error_msg = m_error_msg +
-                      _("Failed to parse result returned by Finance::Quote.") + "\n";
-        return;
+        std::string error_msg{_("Failed to parse result returned by Finance::Quote.")};
+        throw(GncQuoteException(error_msg));
+    }
+    if (what)
+    {
+        std::string error_msg{_("Failed to parse result returned by Finance::Quote.")};
+        error_msg += "\n";
+        error_msg += _("Error message:");
+        error_msg += "\n";
+        error_msg += what;
+        throw(GncQuoteException(error_msg));
     }
 
     auto pricedb{gnc_pricedb_get_db(m_book)};
@@ -672,9 +689,17 @@ gnc_quotes_get_quotable_commodities (const gnc_commodity_table * table)
 // Constructor - checks for presence of Finance::Quote and import version and quote sources
 GncQuotes::GncQuotes ()
 {
-    m_impl = std::make_unique<GncQuotesImpl> ();
+    try
+    {
+        m_impl = std::make_unique<GncQuotesImpl>();
+    }
+    catch (const GncQuoteSourceError& err)
+    {
+        throw(GncQuoteException(err.what()));
+    }
 }
 
+
 void
 GncQuotes::fetch (QofBook *book)
 {
@@ -691,16 +716,6 @@ void GncQuotes::fetch (gnc_commodity *comm)
     m_impl->fetch (comm);
 }
 
-const int GncQuotes::cmd_result() noexcept
-{
-    return m_impl->cmd_result ();
-}
-
-const std::string& GncQuotes::error_msg() noexcept
-{
-    return m_impl->error_msg ();
-}
-
 const std::string& GncQuotes::version() noexcept
 {
     return m_impl->version ();
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 3c2ec0f5c..e2968958a 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -27,6 +27,7 @@
 #include <vector>
 #include <gnc-commodity.hpp>  // For CommVec alias
 #include <glib.h>
+#include <stdexcept>
 
 extern  "C" {
 #include <qofbook.h>
@@ -36,6 +37,11 @@ using StrVec = std::vector  <std::string>;
 using QuoteSources = StrVec;
 using CmdOutput = std::pair <StrVec, StrVec>;
 
+struct GncQuoteException : public std::runtime_error
+{
+    GncQuoteException(const std::string& msg) : std::runtime_error(msg) {}
+};
+
 const std::string not_found = std::string ("Not Found");
 
 class GncQuotesImpl;
@@ -54,8 +60,6 @@ public:
     // Fetch quote for the commodity if it has a quote source  set
     void fetch (gnc_commodity *comm);
 
-    const int cmd_result() noexcept;
-    const std::string& error_msg() noexcept;
     const std::string& version() noexcept;
     const QuoteSources& sources() noexcept;
     GList* sources_as_glist ();
diff --git a/libgnucash/app-utils/test/CMakeLists.txt b/libgnucash/app-utils/test/CMakeLists.txt
index 799e655ad..8616bb5f1 100644
--- a/libgnucash/app-utils/test/CMakeLists.txt
+++ b/libgnucash/app-utils/test/CMakeLists.txt
@@ -44,6 +44,7 @@ set(test_gnc_quotes_LIBS
         gnc-engine
         gtest
         ${Boost_FILESYSTEM_LIBRARY}
+        ${Boost_LOCALE_LIBRARY}
         ${Boost_PROPERTY_TREE_LIBRARY}
         ${Boost_SYSTEM_LIBRARY}
         )

commit d3072950763c5dc5afad5f656e082869e487bf36
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Sep 10 13:30:15 2022 -0700

    [price-quotes] Paramaterize GncMockQuoteSource construction.
    
    So we can have different results passed back for different tests.

diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index 1a7d89509..666d783bb 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -47,11 +47,12 @@ class GncMockQuoteSource final : public GncQuoteSource
 {
     const std::string m_version{"9.99"};
     const StrVec m_sources{"currency", "yahoo_json"};
-    const StrVec m_quotes{
-              "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004},\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}}"
-    };
-    const StrVec m_errors{};
+    const StrVec m_quotes;
+    const StrVec m_errors;
 public:
+    GncMockQuoteSource(StrVec&& quotes, StrVec&& errors) :
+        m_quotes{std::move(quotes)}, m_errors{std::move(errors)}{}
+    ~GncMockQuoteSource() override = default;
     virtual const std::string& get_version() const noexcept override { return m_version; }
     virtual const StrVec& get_sources() const noexcept override { return m_sources; }
     virtual QuoteResult get_quotes(const std::string&) const override;
@@ -145,7 +146,11 @@ TEST_F(GncQuotesTest, online_wiggle)
 
 TEST_F(GncQuotesTest, offline_wiggle)
 {
-    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>());
+    StrVec quote_vec{
+        "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004},\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}}"
+    };
+    StrVec err_vec;
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
     quotes.fetch(m_book);
     auto pricedb{gnc_pricedb_get_db(m_book)};
     EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));

commit 2b870666871f07d79f83b21ee7fad69890e7e7ae
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Sep 9 16:31:45 2022 -0700

    [price-quotes] Extract some static functions.
    
    To get  GncQuoteImpl::parse_one_quote to a reasonable size.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index b743e5acb..6d595edf1 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -352,143 +352,191 @@ GncQuotesImpl::query_fq (void)
 
 }
 
-GNCPrice*
-GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
+struct PriceParams
 {
-    auto comm_ns = gnc_commodity_get_namespace (comm);
-    auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
-    if (gnc_commodity_equiv(comm, m_dflt_curr) ||
-        (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
-        return nullptr;
-    auto comm_pt_ai{pt.find(comm_mnemonic)};
-    if (comm_pt_ai == pt.not_found())
-    {
-        PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
-              comm_ns, comm_mnemonic);
-        return nullptr;
-    }
+    const char* ns;
+    const char* mnemonic;
+    bool success;
+    std::string type;
+    boost::optional<std::string> price;
+    bool inverted;
+    boost::optional<std::string> date;
+    boost::optional<std::string> time;
+    boost::optional<std::string> currency;
+    boost::optional<std::string> errormsg;
+};
 
-    auto comm_pt{comm_pt_ai->second};
-    auto success = comm_pt.get_optional<bool> ("success");
-    std::string price_type = "last";
-    auto price_str = comm_pt.get_optional<std::string> (price_type);
-    if (!price_str)
+static void
+get_price_and_type(PriceParams& p, const bpt::ptree& comm_pt)
+{
+    p.type = "last";
+    p.price = comm_pt.get_optional<std::string> (p.type);
+    if (!p.price)
     {
-        price_type = "nav";
-        price_str = comm_pt.get_optional<std::string> (price_type);
+        p.type = "nav";
+        p.price = comm_pt.get_optional<std::string> (p.type);
     }
-    if (!price_str)
+    if (!p.price)
     {
-        price_type = "price";
-        price_str = comm_pt.get_optional<std::string> (price_type);
+        p.type = "price";
+        p.price = comm_pt.get_optional<std::string> (p.type);
         /* guile wrapper used "unknown" as price type when "price" was found,
          * reproducing here to keep same result for users in the pricedb */
-        price_type = "unknown";
+        p.type = p.price ? "unknown" : "missing";
     }
+}
 
-    auto inverted_tmp = comm_pt.get_optional<bool> ("inverted");
-    auto inverted = inverted_tmp ? *inverted_tmp : false;
-    auto date_str = comm_pt.get_optional<std::string> ("date");
-    auto time_str = comm_pt.get_optional<std::string> ("time");
-    auto currency_str = comm_pt.get_optional<std::string> ("currency");
-
-
-    PINFO("Commodity: %s", comm_mnemonic);
-    PINFO("     Date: %s", (date_str ? date_str->c_str() : "missing"));
-    PINFO("     Time: %s", (time_str ? time_str->c_str() : "missing"));
-    PINFO(" Currency: %s", (currency_str ? currency_str->c_str() : "missing"));
-    PINFO("    Price: %s", (price_str ? price_str->c_str() : "missing"));
+static void
+parse_quote_json(PriceParams& p, const bpt::ptree& comm_pt)
+{
+    auto success = comm_pt.get_optional<bool> ("success");
+    p.success = success && *success;
+    if (!p.success)
+        p.errormsg = comm_pt.get_optional<std::string> ("errormsg");
+    get_price_and_type(p, comm_pt);
+    auto inverted = comm_pt.get_optional<bool> ("inverted");
+    p.inverted = inverted && *inverted;
+    p.date = comm_pt.get_optional<std::string> ("date");
+    p.time = comm_pt.get_optional<std::string> ("time");
+    p.currency = comm_pt.get_optional<std::string> ("currency");
+
+
+    PINFO("Commodity: %s", p.mnemonic);
+    PINFO("  Success: %s", (inverted ? "yes" : "no"));
+    PINFO("     Date: %s", (p.date ? p.date->c_str() : "missing"));
+    PINFO("     Time: %s", (p.time ? p.time->c_str() : "missing"));
+    PINFO(" Currency: %s", (p.currency ? p.currency->c_str() : "missing"));
+    PINFO("    Price: %s", (p.price ? p.price->c_str() : "missing"));
     PINFO(" Inverted: %s\n", (inverted ? "yes" : "no"));
+}
 
-    if (!success || !*success)
+static time64
+calc_price_time(const PriceParams& p)
+{
+    time64 quote_time;
+    std::string iso_date_str = GncDate().format ("%Y-%m-%d");
+    if (p.date)
     {
-        auto errmsg = comm_pt.get_optional<std::string> ("errormsg");
-        PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
-              comm_ns, comm_mnemonic,
-              (errmsg ? errmsg->c_str() : "unknown"));
-        return nullptr;
+        // Returned date is always in MM/DD/YYYY format according to F::Q man page, transform it to simplify conversion to GncDateTime
+        auto date_tmp = *p.date;
+        iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) + "-" + date_tmp.substr (3, 2);
     }
+    else
+        PINFO("Info: no date  was returned for %s:%s - will use today %s",
+              p.ns, p.mnemonic,
+              (iso_date_str += " " + (p.time ? *p.time : "12:00:00")).c_str());
 
-    if (!price_str)
+    auto can_convert = true;
+    try
     {
-        PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
-              comm_ns, comm_mnemonic);
-        return nullptr;
+        quote_time = static_cast<time64>(GncDateTime{iso_date_str});
+    }
+    catch (...)
+    {
+        PINFO("Warning: failed to parse quote date and time '%s' for %s:%s - will use today",
+              iso_date_str.c_str(),  p.ns, p.mnemonic);
+        quote_time = static_cast<time64>(GncDateTime());
     }
 
-    GncNumeric price;
+    return quote_time;
+}
+
+static boost::optional<GncNumeric>
+get_price(const PriceParams& p)
+{
+    boost::optional<GncNumeric> price;
     try
     {
-        price = GncNumeric { *price_str };
+        price = GncNumeric { *p.price };
     }
     catch (...)
     {
         PWARN("Skipped %s:%s - failed to parse returned price '%s'",
-              comm_ns, comm_mnemonic, price_str->c_str());
-        return nullptr;
+              p.ns, p.mnemonic, p.price->c_str());
     }
 
-    if (inverted)
-        price = price.inv();
+    if (price && p.inverted)
+        *price = price->inv();
+
+    return price;
+}
 
-    if (!currency_str)
+static gnc_commodity*
+get_currency(const PriceParams& p, QofBook* book)
+{
+    if (!p.currency)
     {
         PWARN("Skipped %s:%s - Finance::Quote didn't return a currency",
-              comm_ns, comm_mnemonic);
+              p.ns, p.mnemonic);
         return nullptr;
     }
-    boost::to_upper (*currency_str);
-    auto commodity_table = gnc_commodity_table_get_table (m_book);
-    auto currency = gnc_commodity_table_lookup (commodity_table, "ISO4217", currency_str->c_str());
+    std::string curr_str = *p.currency;
+    boost::to_upper (curr_str);
+    auto commodity_table = gnc_commodity_table_get_table (book);
+    auto currency = gnc_commodity_table_lookup (commodity_table, "ISO4217", curr_str.c_str());
 
     if (!currency)
     {
         PWARN("Skipped %s:%s  - failed to parse returned currency '%s'",
-              comm_ns, comm_mnemonic, currency_str->c_str());
+              p.ns, p.mnemonic, p.currency->c_str());
         return nullptr;
     }
 
-    std::string iso_date_str = GncDate().format ("%Y-%m-%d");
-    if (date_str)
+    return currency;
+}
+
+GNCPrice*
+GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
+{
+    PriceParams p;
+    p.ns = gnc_commodity_get_namespace (comm);
+    p.mnemonic = gnc_commodity_get_mnemonic (comm);
+    if (gnc_commodity_equiv(comm, m_dflt_curr) ||
+        (!p.mnemonic || (strcmp (p.mnemonic, "XXX") == 0)))
+        return nullptr;
+    auto comm_pt_ai{pt.find(p.mnemonic)};
+    if (comm_pt_ai == pt.not_found())
     {
-        // Returned date is always in MM/DD/YYYY format according to F::Q man page, transform it to simplify conversion to GncDateTime
-        auto date_tmp = *date_str;
-        iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) + "-" + date_tmp.substr (3, 2);
+        PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
+              p.ns, p.mnemonic);
+        return nullptr;
     }
-    else
-        PINFO("Info: no date  was returned for %s:%s - will use today %s",
-              comm_ns, comm_mnemonic,
-              (iso_date_str += " " + (time_str ? *time_str : "12:00:00")).c_str());
 
-    auto can_convert = true;
-    try
+    auto comm_pt{comm_pt_ai->second};
+    parse_quote_json(p, comm_pt);
+
+    if (!p.success)
     {
-        GncDateTime testdt {iso_date_str};
+        PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
+              p.ns, p.mnemonic,
+              (p.errormsg ? p.errormsg->c_str() : "unknown"));
+        return nullptr;
     }
-    catch (...)
+
+    if (!p.price)
     {
-        PINFO("Warning: failed to parse quote date and time '%s' for %s:%s - will use today",
-              iso_date_str.c_str(),  comm_ns, comm_mnemonic);
-        can_convert = false;
+        PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
+              p.ns, p.mnemonic);
+        return nullptr;
     }
 
-    /*  Bit of an odd construct: GncDateTimes can't be copied,
-        which makes it impossible to first create a temporary GncDateTime
-        based on whether the string is parsable and then assign that temporary
-        to our final GncDateTime. The creation has to happen in one go, so
-        below construct will pass a different constructor argument based on
-        whether a test conversion worked or not.
-    */
-    GncDateTime quotedt {can_convert ? iso_date_str : GncDateTime()};
+    auto price{get_price(p)};
+    if (!price)
+        return nullptr;
+
+    auto currency{get_currency(p, m_book)};
+    if (!currency)
+        return nullptr;
 
+    auto quotedt{calc_price_time(p)};
     auto gnc_price = gnc_price_create (m_book);
     gnc_price_begin_edit (gnc_price);
     gnc_price_set_commodity (gnc_price, comm);
     gnc_price_set_currency (gnc_price, currency);
     gnc_price_set_time64 (gnc_price, static_cast<time64> (quotedt));
     gnc_price_set_source (gnc_price, PRICE_SOURCE_FQ);
-    gnc_price_set_typestr (gnc_price, price_type.c_str());
-    gnc_price_set_value (gnc_price, price);
+    gnc_price_set_typestr (gnc_price, p.type.c_str());
+    gnc_price_set_value (gnc_price, *price);
     gnc_price_commit_edit (gnc_price);
     return gnc_price;
 }

commit a82c72cfb9c53f5f8d2b93a87cd707e489a7b4f5
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Sep 9 14:57:51 2022 -0700

    [price-quotes] Remove level of indirection when parsing quote data.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 90a4072df..b743e5acb 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -360,36 +360,37 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
     if (gnc_commodity_equiv(comm, m_dflt_curr) ||
         (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
         return nullptr;
-    if (pt.find (comm_mnemonic) == pt.not_found())
+    auto comm_pt_ai{pt.find(comm_mnemonic)};
+    if (comm_pt_ai == pt.not_found())
     {
         PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
               comm_ns, comm_mnemonic);
         return nullptr;
     }
 
-    std::string key = comm_mnemonic;
-    auto success = pt.get_optional<bool> (key + ".success");
+    auto comm_pt{comm_pt_ai->second};
+    auto success = comm_pt.get_optional<bool> ("success");
     std::string price_type = "last";
-    auto price_str = pt.get_optional<std::string> (key + "." + price_type);
+    auto price_str = comm_pt.get_optional<std::string> (price_type);
     if (!price_str)
     {
         price_type = "nav";
-        price_str = pt.get_optional<std::string> (key + "." + price_type);
+        price_str = comm_pt.get_optional<std::string> (price_type);
     }
     if (!price_str)
     {
         price_type = "price";
-        price_str = pt.get_optional<std::string> (key + "." + price_type);
+        price_str = comm_pt.get_optional<std::string> (price_type);
         /* guile wrapper used "unknown" as price type when "price" was found,
          * reproducing here to keep same result for users in the pricedb */
         price_type = "unknown";
     }
 
-    auto inverted_tmp = pt.get_optional<bool> (key + ".inverted");
+    auto inverted_tmp = comm_pt.get_optional<bool> ("inverted");
     auto inverted = inverted_tmp ? *inverted_tmp : false;
-    auto date_str = pt.get_optional<std::string> (key + ".date");
-    auto time_str = pt.get_optional<std::string> (key + ".time");
-    auto currency_str = pt.get_optional<std::string> (key + ".currency");
+    auto date_str = comm_pt.get_optional<std::string> ("date");
+    auto time_str = comm_pt.get_optional<std::string> ("time");
+    auto currency_str = comm_pt.get_optional<std::string> ("currency");
 
 
     PINFO("Commodity: %s", comm_mnemonic);
@@ -401,7 +402,7 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
 
     if (!success || !*success)
     {
-        auto errmsg = pt.get_optional<std::string> (key + ".errormsg");
+        auto errmsg = comm_pt.get_optional<std::string> ("errormsg");
         PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
               comm_ns, comm_mnemonic,
               (errmsg ? errmsg->c_str() : "unknown"));

commit 37dfab7f31cc541c5458c1204cd5a308914f9e92
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Sep 2 11:16:06 2022 -0700

    [price-quotes] Convert long quote parsing lambda to a regular function.
    
    To begin separating price parsing from inserting in the price db.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 368671b63..90a4072df 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -95,6 +95,7 @@ private:
     void query_fq (void);
     void parse_quotes (void);
     std::string comm_vec_to_json_string(void) const;
+    GNCPrice* parse_one_quote(const bpt::ptree&, gnc_commodity*);
 
     std::unique_ptr<GncQuoteSource> m_quotesource;
     CommVec m_comm_vec;
@@ -351,6 +352,146 @@ GncQuotesImpl::query_fq (void)
 
 }
 
+GNCPrice*
+GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
+{
+    auto comm_ns = gnc_commodity_get_namespace (comm);
+    auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
+    if (gnc_commodity_equiv(comm, m_dflt_curr) ||
+        (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
+        return nullptr;
+    if (pt.find (comm_mnemonic) == pt.not_found())
+    {
+        PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
+              comm_ns, comm_mnemonic);
+        return nullptr;
+    }
+
+    std::string key = comm_mnemonic;
+    auto success = pt.get_optional<bool> (key + ".success");
+    std::string price_type = "last";
+    auto price_str = pt.get_optional<std::string> (key + "." + price_type);
+    if (!price_str)
+    {
+        price_type = "nav";
+        price_str = pt.get_optional<std::string> (key + "." + price_type);
+    }
+    if (!price_str)
+    {
+        price_type = "price";
+        price_str = pt.get_optional<std::string> (key + "." + price_type);
+        /* guile wrapper used "unknown" as price type when "price" was found,
+         * reproducing here to keep same result for users in the pricedb */
+        price_type = "unknown";
+    }
+
+    auto inverted_tmp = pt.get_optional<bool> (key + ".inverted");
+    auto inverted = inverted_tmp ? *inverted_tmp : false;
+    auto date_str = pt.get_optional<std::string> (key + ".date");
+    auto time_str = pt.get_optional<std::string> (key + ".time");
+    auto currency_str = pt.get_optional<std::string> (key + ".currency");
+
+
+    PINFO("Commodity: %s", comm_mnemonic);
+    PINFO("     Date: %s", (date_str ? date_str->c_str() : "missing"));
+    PINFO("     Time: %s", (time_str ? time_str->c_str() : "missing"));
+    PINFO(" Currency: %s", (currency_str ? currency_str->c_str() : "missing"));
+    PINFO("    Price: %s", (price_str ? price_str->c_str() : "missing"));
+    PINFO(" Inverted: %s\n", (inverted ? "yes" : "no"));
+
+    if (!success || !*success)
+    {
+        auto errmsg = pt.get_optional<std::string> (key + ".errormsg");
+        PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
+              comm_ns, comm_mnemonic,
+              (errmsg ? errmsg->c_str() : "unknown"));
+        return nullptr;
+    }
+
+    if (!price_str)
+    {
+        PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
+              comm_ns, comm_mnemonic);
+        return nullptr;
+    }
+
+    GncNumeric price;
+    try
+    {
+        price = GncNumeric { *price_str };
+    }
+    catch (...)
+    {
+        PWARN("Skipped %s:%s - failed to parse returned price '%s'",
+              comm_ns, comm_mnemonic, price_str->c_str());
+        return nullptr;
+    }
+
+    if (inverted)
+        price = price.inv();
+
+    if (!currency_str)
+    {
+        PWARN("Skipped %s:%s - Finance::Quote didn't return a currency",
+              comm_ns, comm_mnemonic);
+        return nullptr;
+    }
+    boost::to_upper (*currency_str);
+    auto commodity_table = gnc_commodity_table_get_table (m_book);
+    auto currency = gnc_commodity_table_lookup (commodity_table, "ISO4217", currency_str->c_str());
+
+    if (!currency)
+    {
+        PWARN("Skipped %s:%s  - failed to parse returned currency '%s'",
+              comm_ns, comm_mnemonic, currency_str->c_str());
+        return nullptr;
+    }
+
+    std::string iso_date_str = GncDate().format ("%Y-%m-%d");
+    if (date_str)
+    {
+        // Returned date is always in MM/DD/YYYY format according to F::Q man page, transform it to simplify conversion to GncDateTime
+        auto date_tmp = *date_str;
+        iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) + "-" + date_tmp.substr (3, 2);
+    }
+    else
+        PINFO("Info: no date  was returned for %s:%s - will use today %s",
+              comm_ns, comm_mnemonic,
+              (iso_date_str += " " + (time_str ? *time_str : "12:00:00")).c_str());
+
+    auto can_convert = true;
+    try
+    {
+        GncDateTime testdt {iso_date_str};
+    }
+    catch (...)
+    {
+        PINFO("Warning: failed to parse quote date and time '%s' for %s:%s - will use today",
+              iso_date_str.c_str(),  comm_ns, comm_mnemonic);
+        can_convert = false;
+    }
+
+    /*  Bit of an odd construct: GncDateTimes can't be copied,
+        which makes it impossible to first create a temporary GncDateTime
+        based on whether the string is parsable and then assign that temporary
+        to our final GncDateTime. The creation has to happen in one go, so
+        below construct will pass a different constructor argument based on
+        whether a test conversion worked or not.
+    */
+    GncDateTime quotedt {can_convert ? iso_date_str : GncDateTime()};
+
+    auto gnc_price = gnc_price_create (m_book);
+    gnc_price_begin_edit (gnc_price);
+    gnc_price_set_commodity (gnc_price, comm);
+    gnc_price_set_currency (gnc_price, currency);
+    gnc_price_set_time64 (gnc_price, static_cast<time64> (quotedt));
+    gnc_price_set_source (gnc_price, PRICE_SOURCE_FQ);
+    gnc_price_set_typestr (gnc_price, price_type.c_str());
+    gnc_price_set_value (gnc_price, price);
+    gnc_price_commit_edit (gnc_price);
+    return gnc_price;
+}
+
 void
 GncQuotesImpl::parse_quotes (void)
 {
@@ -376,148 +517,17 @@ GncQuotesImpl::parse_quotes (void)
         return;
     }
 
-    auto pricedb = gnc_pricedb_get_db (m_book);
-    std::for_each(m_comm_vec.begin(), m_comm_vec.end(),
-                  [this, &pt, &pricedb] (gnc_commodity *comm)
-                {
-                    auto comm_ns = gnc_commodity_get_namespace (comm);
-                    auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
-                    if (gnc_commodity_equiv(comm, m_dflt_curr) ||
-                       (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
-                        return;
-                    if (pt.find (comm_mnemonic) == pt.not_found())
-                    {
-                        PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
-                              comm_ns, comm_mnemonic);
-                        return;
-                    }
-
-                    std::string key = comm_mnemonic;
-                    auto success = pt.get_optional<bool> (key + ".success");
-                    std::string price_type = "last";
-                    auto price_str = pt.get_optional<std::string> (key + "." + price_type);
-                    if (!price_str)
-                    {
-                        price_type = "nav";
-                        price_str = pt.get_optional<std::string> (key + "." + price_type);
-                    }
-                    if (!price_str)
-                    {
-                        price_type = "price";
-                        price_str = pt.get_optional<std::string> (key + "." + price_type);
-                        /* guile wrapper used "unknown" as price type when "price" was found,
-                         * reproducing here to keep same result for users in the pricedb */
-                        price_type = "unknown";
-                    }
-
-                    auto inverted_tmp = pt.get_optional<bool> (key + ".inverted");
-                    auto inverted = inverted_tmp ? *inverted_tmp : false;
-                    auto date_str = pt.get_optional<std::string> (key + ".date");
-                    auto time_str = pt.get_optional<std::string> (key + ".time");
-                    auto currency_str = pt.get_optional<std::string> (key + ".currency");
-
-
-                    PINFO("Commodity: %s", comm_mnemonic);
-                    PINFO("     Date: %s", (date_str ? date_str->c_str() : "missing"));
-                    PINFO("     Time: %s", (time_str ? time_str->c_str() : "missing"));
-                    PINFO(" Currency: %s", (currency_str ? currency_str->c_str() : "missing"));
-                    PINFO("    Price: %s", (price_str ? price_str->c_str() : "missing"));
-                    PINFO(" Inverted: %s\n", (inverted ? "yes" : "no"));
-
-                    if (!success || !*success)
-                    {
-                        auto errmsg = pt.get_optional<std::string> (key + ".errormsg");
-                        PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
-                              comm_ns, comm_mnemonic,
-                              (errmsg ? errmsg->c_str() : "unknown"));
-                        return;
-                    }
-
-                    if (!price_str)
-                    {
-                        PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
-                              comm_ns, comm_mnemonic);
-                        return;
-                    }
-
-                    GncNumeric price;
-                    try
-                    {
-                        price = GncNumeric { *price_str };
-                    }
-                    catch (...)
-                    {
-                        PWARN("Skipped %s:%s - failed to parse returned price '%s'",
-                              comm_ns, comm_mnemonic, price_str->c_str());
-                        return;
-                    }
-
-                    if (inverted)
-                        price = price.inv();
-
-                    if (!currency_str)
-                    {
-                        PWARN("Skipped %s:%s - Finance::Quote didn't return a currency",
-                              comm_ns, comm_mnemonic);
-                        return;
-                    }
-                    boost::to_upper (*currency_str);
-                    auto commodity_table = gnc_commodity_table_get_table (m_book);
-                    auto currency = gnc_commodity_table_lookup (commodity_table, "ISO4217", currency_str->c_str());
-
-                    if (!currency)
-                    {
-                        PWARN("Skipped %s:%s  - failed to parse returned currency '%s'",
-                              comm_ns, comm_mnemonic, currency_str->c_str());
-                        return;
-                    }
-
-                    std::string iso_date_str = GncDate().format ("%Y-%m-%d");
-                    if (date_str)
-                    {
-                    // Returned date is always in MM/DD/YYYY format according to F::Q man page, transform it to simplify conversion to GncDateTime
-                        auto date_tmp = *date_str;
-                        iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) + "-" + date_tmp.substr (3, 2);
-                    }
-                    else
-                        PINFO("Info: no date  was returned for %s:%s - will use today %s",
-                              comm_ns, comm_mnemonic,
-                              (iso_date_str += " " + (time_str ? *time_str : "12:00:00")).c_str());
-
-                    auto can_convert = true;
-                    try
-                    {
-                        GncDateTime testdt {iso_date_str};
-                    }
-                    catch (...)
-                    {
-                        PINFO("Warning: failed to parse quote date and time '%s' for %s:%s - will use today",
-                              iso_date_str.c_str(),  comm_ns, comm_mnemonic);
-                        can_convert = false;
-                    }
-
-                    /*  Bit of an odd construct: GncDateTimes can't be copied,
-                        which makes it impossible to first create a temporary GncDateTime
-                        based on whether the string is parsable and then assign that temporary
-                        to our final GncDateTime. The creation has to happen in one go, so
-                        below construct will pass a different constructor argument based on
-                        whether a test conversion worked or not.
-                    */
-                    GncDateTime quotedt {can_convert ? iso_date_str : GncDateTime()};
-
-                    auto gnc_price = gnc_price_create (m_book);
-                    gnc_price_begin_edit (gnc_price);
-                    gnc_price_set_commodity (gnc_price, comm);
-                    gnc_price_set_currency (gnc_price, currency);
-                    gnc_price_set_time64 (gnc_price, static_cast<time64> (quotedt));
-                    gnc_price_set_source (gnc_price, PRICE_SOURCE_FQ);
-                    gnc_price_set_typestr (gnc_price, price_type.c_str());
-                    gnc_price_set_value (gnc_price, price);
-                    gnc_pricedb_add_price (pricedb, gnc_price);
-                    gnc_price_commit_edit (gnc_price);
-                    gnc_price_unref (gnc_price);
-                });
-
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    for (auto comm : m_comm_vec)
+    {
+        auto price{parse_one_quote(pt, comm)};
+        if (!price)
+            continue;
+        gnc_price_begin_edit (price);
+        gnc_pricedb_add_price(pricedb, price);
+        gnc_price_commit_edit(price);
+        gnc_price_unref (price);
+    }
 }
 
 

commit dd8316714bf1042bea03b11565afaebef7c96669
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Sep 2 10:13:09 2022 -0700

    [price-quotes] Extract function GncQuotesImpl::comm_vec_to_json_string.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index fe913e6c6..368671b63 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -94,6 +94,7 @@ public:
 private:
     void query_fq (void);
     void parse_quotes (void);
+    std::string comm_vec_to_json_string(void) const;
 
     std::unique_ptr<GncQuoteSource> m_quotesource;
     CommVec m_comm_vec;
@@ -298,8 +299,8 @@ format_quotes (const std::vector<gnc_commodity*>)
     return std::vector <std::string>();
 }
 
-void
-GncQuotesImpl::query_fq (void)
+std::string
+GncQuotesImpl::comm_vec_to_json_string (void) const
 {
     bpt::ptree pt, pt_child;
     pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
@@ -325,8 +326,14 @@ GncQuotesImpl::query_fq (void)
 
     std::ostringstream result;
     bpt::write_json(result, pt);
+    return result.str();
+}
 
-    auto [rv, quotes, errors] = m_quotesource->get_quotes(result.str());
+void
+GncQuotesImpl::query_fq (void)
+{
+    auto json_str{comm_vec_to_json_string()};
+    auto [rv, quotes, errors] = m_quotesource->get_quotes(json_str);
     m_fq_answer.clear();
     m_cmd_result = rv;
     if (rv == 0)

commit e3ab384504d2d7fb2c36229536ddb3ec743f4269
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Sep 2 09:58:48 2022 -0700

    [price-quotes] Log messages instead of writing them to std::streams.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index f251f79a1..fe913e6c6 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -331,16 +331,16 @@ GncQuotesImpl::query_fq (void)
     m_cmd_result = rv;
     if (rv == 0)
         for (auto line : quotes)
-            m_fq_answer.append(std::move(line) + "\n");
+            m_fq_answer.append(line + "\n");
     else
         for (auto line : errors)
-            m_error_msg.append(std::move(line) + "\n");
+            m_error_msg.append(line + "\n");
 
-//     for (auto line : cmd_out.first)
-//         std::cerr << "Output line retrieved from wrapper:\n" << line << std::endl;
+//        for (auto line : quotes)
+//            PINFO("Output line retrieved from wrapper:\n%s", line.c_str());
 //
-//     for (auto line : cmd_out.second)
-//         std::cerr << "Error line retrieved from wrapper:\n" << line << std::endl;
+//     for (auto line : errors)
+//         PINFO("Error line retrieved from wrapper:\n%s",line.c_str());Ëš
 
 }
 
@@ -380,7 +380,8 @@ GncQuotesImpl::parse_quotes (void)
                         return;
                     if (pt.find (comm_mnemonic) == pt.not_found())
                     {
-                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote didn't return any data.\n";
+                        PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
+                              comm_ns, comm_mnemonic);
                         return;
                     }
 
@@ -409,24 +410,26 @@ GncQuotesImpl::parse_quotes (void)
                     auto currency_str = pt.get_optional<std::string> (key + ".currency");
 
 
-                    std::cout << "Commodity: " << comm_mnemonic << "\n";
-                    std::cout << "     Date: " << (date_str ? *date_str : "missing") << "\n";
-                    std::cout << "     Time: " << (time_str ? *time_str : "missing") << "\n";
-                    std::cout << " Currency: " << (currency_str ? *currency_str : "missing") << "\n";
-                    std::cout << "    Price: " << (price_str ? *price_str : "missing") << "\n";
-                    std::cout << " Inverted: " << (inverted ? "yes" : "no") << "\n\n";
+                    PINFO("Commodity: %s", comm_mnemonic);
+                    PINFO("     Date: %s", (date_str ? date_str->c_str() : "missing"));
+                    PINFO("     Time: %s", (time_str ? time_str->c_str() : "missing"));
+                    PINFO(" Currency: %s", (currency_str ? currency_str->c_str() : "missing"));
+                    PINFO("    Price: %s", (price_str ? price_str->c_str() : "missing"));
+                    PINFO(" Inverted: %s\n", (inverted ? "yes" : "no"));
 
                     if (!success || !*success)
                     {
                         auto errmsg = pt.get_optional<std::string> (key + ".errormsg");
-                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote returned fetch failure.\n";
-                        std::cerr << "Reason: " << (errmsg ? *errmsg : "unknown") << "\n";
+                        PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
+                              comm_ns, comm_mnemonic,
+                              (errmsg ? errmsg->c_str() : "unknown"));
                         return;
                     }
 
                     if (!price_str)
                     {
-                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote didn't return a valid price\n";
+                        PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
+                              comm_ns, comm_mnemonic);
                         return;
                     }
 
@@ -437,7 +440,8 @@ GncQuotesImpl::parse_quotes (void)
                     }
                     catch (...)
                     {
-                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - failed to parse returned price '" << *price_str << "'\n";
+                        PWARN("Skipped %s:%s - failed to parse returned price '%s'",
+                              comm_ns, comm_mnemonic, price_str->c_str());
                         return;
                     }
 
@@ -446,7 +450,8 @@ GncQuotesImpl::parse_quotes (void)
 
                     if (!currency_str)
                     {
-                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote didn't return a currency\n";
+                        PWARN("Skipped %s:%s - Finance::Quote didn't return a currency",
+                              comm_ns, comm_mnemonic);
                         return;
                     }
                     boost::to_upper (*currency_str);
@@ -455,7 +460,8 @@ GncQuotesImpl::parse_quotes (void)
 
                     if (!currency)
                     {
-                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - failed to parse returned currency '" << *currency_str << "'\n";
+                        PWARN("Skipped %s:%s  - failed to parse returned currency '%s'",
+                              comm_ns, comm_mnemonic, currency_str->c_str());
                         return;
                     }
 
@@ -467,8 +473,9 @@ GncQuotesImpl::parse_quotes (void)
                         iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) + "-" + date_tmp.substr (3, 2);
                     }
                     else
-                        std::cerr << "Info: no date  was returned for " << comm_ns << ":" << comm_mnemonic << " - will use today\n";
-                    iso_date_str += " " + (time_str ? *time_str : "12:00:00");
+                        PINFO("Info: no date  was returned for %s:%s - will use today %s",
+                              comm_ns, comm_mnemonic,
+                              (iso_date_str += " " + (time_str ? *time_str : "12:00:00")).c_str());
 
                     auto can_convert = true;
                     try
@@ -477,7 +484,8 @@ GncQuotesImpl::parse_quotes (void)
                     }
                     catch (...)
                     {
-                        std::cerr << "Warning: failed to parse quote date and time '" << iso_date_str << "' for " << comm_ns << ":" << comm_mnemonic << " - will use today\n";
+                        PINFO("Warning: failed to parse quote date and time '%s' for %s:%s - will use today",
+                              iso_date_str.c_str(),  comm_ns, comm_mnemonic);
                         can_convert = false;
                     }
 

commit b8642e55d9da9671dae90ef7f3b5d8adc22b3854
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Sep 1 17:11:40 2022 -0700

    [price-quotes] Implement mock quote source.
    
    Note that because the non-default constructor exists only on GncQuotesImpl
    we must use that directly and violate the pimpl.

diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index 2cc95456d..1a7d89509 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -43,6 +43,26 @@ gnc_default_currency(void)
 #include <gtest/gtest.h>
 #include "../gnc-quotes.cpp"
 
+class GncMockQuoteSource final : public GncQuoteSource
+{
+    const std::string m_version{"9.99"};
+    const StrVec m_sources{"currency", "yahoo_json"};
+    const StrVec m_quotes{
+              "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004},\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\"      129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\"        12.4 - 17.76\",\"eps\":2.79,\"success\":1},\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}}"
+    };
+    const StrVec m_errors{};
+public:
+    virtual const std::string& get_version() const noexcept override { return m_version; }
+    virtual const StrVec& get_sources() const noexcept override { return m_sources; }
+    virtual QuoteResult get_quotes(const std::string&) const override;
+    virtual bool usable() const noexcept override { return true; }
+};
+
+QuoteResult
+GncMockQuoteSource::get_quotes(const std::string& json_string) const
+{
+    return {0, m_quotes, m_errors};
+}
 class GncQuotesTest : public ::testing::Test
 {
 protected:
@@ -114,11 +134,19 @@ TEST_F(GncQuotesTest, quotable_commodities)
 }
 
 #ifdef HAVE_F_Q
-TEST_F(GncQuotesTest, wiggle)
+TEST_F(GncQuotesTest, online_wiggle)
 {
     GncQuotes quotes;
     quotes.fetch(m_book);
     auto pricedb{gnc_pricedb_get_db(m_book)};
     EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
 }
-#endif
\ No newline at end of file
+#endif
+
+TEST_F(GncQuotesTest, offline_wiggle)
+{
+    GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>());
+    quotes.fetch(m_book);
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
+}

commit 4dd39228711aea2382a53661ccc488242c00acda
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Sep 1 15:46:47 2022 -0700

    [price-quotes] Make wiggle test conditional on F::Q being installed.

diff --git a/CMakeLists.txt b/CMakeLists.txt
index fd6122131..727a83337 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -512,6 +512,14 @@ if (NOT PERL_FOUND)
   message(SEND_ERROR "Perl executable not found. Please set PERL_EXECUTABLE.")
 endif()
 
+execute_process(COMMAND
+        ${PERL_EXECUTABLE} -MFinance::Quote -e ""
+        ERROR_QUIET
+        RESULT_VARIABLE have_f_q)
+if (${have_f_q} EQUAL 0)
+  set(HAVE_F_Q 1)
+endif()
+
 get_filename_component(PERL_DIR ${PERL_EXECUTABLE} DIRECTORY)
 
 find_program(POD2MAN_EXECUTABLE pod2man HINTS ${PERL_DIR})
diff --git a/common/config.h.cmake.in b/common/config.h.cmake.in
index f08fb703b..6188080eb 100644
--- a/common/config.h.cmake.in
+++ b/common/config.h.cmake.in
@@ -129,6 +129,9 @@
 /* Define to 1 if you have the `pthread' library (-lpthread). */
 #cmakedefine HAVE_LIBPTHREAD 1
 
+/* Define to 1 if Finance::Quote is installed in perl. */
+#cmakedefine HAVE_F_Q 1
+
 /* System has libsecret 0.18 or better */
 #cmakedefine HAVE_LIBSECRET 1
 
diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
index c219f2cb9..2cc95456d 100644
--- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -112,6 +112,8 @@ TEST_F(GncQuotesTest, quotable_commodities)
     auto commodities{gnc_quotes_get_quotable_commodities(gnc_commodity_table_get_table(m_book))};
     EXPECT_EQ(4u, commodities.size());
 }
+
+#ifdef HAVE_F_Q
 TEST_F(GncQuotesTest, wiggle)
 {
     GncQuotes quotes;
@@ -119,3 +121,4 @@ TEST_F(GncQuotesTest, wiggle)
     auto pricedb{gnc_pricedb_get_db(m_book)};
     EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
 }
+#endif
\ No newline at end of file

commit 784aca5a4c8ad99378b39eb554bfc493b8f68a6c
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Sep 1 15:07:44 2022 -0700

    [price-quotes] Extract class GncQuoteSource.
    
    Provide a specialization GncFQQuoteSource and move the F::Q command
    construction and query functions to GncFQQuoteSource.
    
    This allows for dependency injection to provide testing that doesn't
    need F::Q to be installed.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 39775da09..f251f79a1 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -26,10 +26,10 @@
 #include <vector>
 #include <string>
 #include <iostream>
-#include <sstream>
 #include <boost/algorithm/string.hpp>
 #include <boost/filesystem.hpp>
 #include <boost/process.hpp>
+#include <boost/regex.hpp>
 #include <boost/property_tree/ptree.hpp>
 #include <boost/property_tree/json_parser.hpp>
 #include <boost/iostreams/device/array.hpp>
@@ -42,53 +42,60 @@
 #include "gnc-quotes.hpp"
 
 extern "C" {
-#include "gnc-commodity.h"
-#include "gnc-path.h"
+#include <gnc-commodity.h>
+#include <gnc-path.h>
 #include "gnc-ui-util.h"
 #include <gnc-prefs.h>
+#include <gnc-session.h>
 #include <regex.h>
 #include <qofbook.h>
 }
 
+static const QofLogModule log_module = "gnc.price-quotes";
+
 namespace bp = boost::process;
 namespace bfs = boost::filesystem;
 namespace bpt = boost::property_tree;
 namespace bio = boost::iostreams;
 
+using QuoteResult = std::tuple<int, StrVec, StrVec>;
 
 CommVec
 gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
 
+class GncQuoteSource
+{
+public:
+    virtual ~GncQuoteSource() = default;
+    virtual const StrVec& get_sources() const noexcept = 0;
+    virtual const std::string & get_version() const noexcept = 0;
+    virtual QuoteResult get_quotes(const std::string& json_str) const = 0;
+    virtual bool usable() const noexcept = 0;
+};
+
 class GncQuotesImpl
 {
 public:
     // Constructor - checks for presence of Finance::Quote and import version and quote sources
     GncQuotesImpl ();
-    GncQuotesImpl (QofBook *book);
+    explicit GncQuotesImpl (QofBook *book);
+    GncQuotesImpl(QofBook*, std::unique_ptr<GncQuoteSource>);
 
     void fetch (QofBook *book);
     void fetch (CommVec& commodities);
     void fetch (gnc_commodity *comm);
 
-    const int cmd_result() noexcept { return m_cmd_result; }
+    int cmd_result() const noexcept { return m_cmd_result; }
     const std::string& error_msg() noexcept { return m_error_msg; }
     const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
     const QuoteSources& sources() noexcept { return m_sources; }
     GList* sources_as_glist ();
 
 private:
-    // Check if Finance::Quote is properly installed
-    void check (QofBook *book);
-    // Run the command specified. Returns two vectors for further processing by the caller
-    // - one with the contents of stdout
-    // - one with the contents of stderr
-    // Will also set m_cmd_result
-    template <typename BufferT> CmdOutput run_cmd (const bfs::path &cmd_name, StrVec args, BufferT input);
-
     void query_fq (void);
     void parse_quotes (void);
 
-
+    std::unique_ptr<GncQuoteSource> m_quotesource;
     CommVec m_comm_vec;
     std::string m_version;
     QuoteSources m_sources;
@@ -99,34 +106,144 @@ private:
     gnc_commodity *m_dflt_curr;
 };
 
-/* GncQuotes implementation */
+class GncFQQuoteSource final : public GncQuoteSource
+{
+    const bfs::path c_cmd;
+    const std::string c_fq_wrapper;
+    bool m_ready;
+    std::string m_version;
+    StrVec m_sources;
+public:
+    GncFQQuoteSource();
+    ~GncFQQuoteSource() = default;
+    virtual const std::string& get_version() const noexcept override { return m_version; }
+    virtual const StrVec& get_sources() const noexcept override { return m_sources; }
+    virtual QuoteResult get_quotes(const std::string&) const override;
+    virtual bool usable() const noexcept override { return m_ready; }
+private:
+    QuoteResult run_cmd (const StrVec& args, const std::string& json_string) const;
 
-GncQuotesImpl::GncQuotesImpl ()
+};
+
+GncFQQuoteSource::GncFQQuoteSource() :
+c_cmd{bp::search_path("perl")},
+c_fq_wrapper{std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper"},
+m_ready{false},
+m_version{}, m_sources{}
 {
-    m_version.clear();
-    m_sources.clear();
-    m_error_msg.clear();
-    m_cmd_result  = 0;
-    m_book = nullptr;
-    m_dflt_curr = gnc_default_currency();
+    StrVec args{"-w", c_fq_wrapper, "-v"};
+    const std::string empty_string;
+    auto [rv, sources, errors] = run_cmd(args, empty_string);
+    if (rv)
+    {
+        PERR("Failed to initialize Finance::Quote %s", errors.front().c_str());
+        return;
+    }
+    if (!errors.empty())
+    {
+        for(const auto& err : errors)
+            PERR("Finance::Quote check returned error %s", err.empty() ? "" : err.c_str());
+        return;
+    }
+    static const boost::regex version_fmt{"[0-9]\\.[0-9][0-9]"};
+    auto version{sources.front()};
+    if (version.empty() || !boost::regex_match(version, version_fmt))
+    {
+        PERR("Invalid Finance::Quote Version %s", version.empty() ? "" : version.c_str());
+        return;
+    }
+    m_ready = true;
+    sources.erase(sources.begin());
+    m_sources = std::move(sources);
+}
 
-    auto perl_executable = bp::search_path("perl");
-    auto fq_wrapper = std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper";
-    StrVec args { "-w", fq_wrapper, "-v" };
+QuoteResult
+GncFQQuoteSource::get_quotes(const std::string& json_str) const
+{
+    StrVec args{"-w", c_fq_wrapper, "-f" };
+    return run_cmd(args, json_str);
+}
 
-    auto cmd_out = run_cmd (perl_executable.string(), args, StrVec());
+QuoteResult
+GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) const
+{
+    StrVec out_vec, err_vec;
+    int cmd_result;
 
-    for (auto line : cmd_out.first)
-        if (m_version.empty())
-            std::swap (m_version, line);
-        else
-            m_sources.push_back (std::move(line));
+    auto av_key = gnc_prefs_get_string ("general.finance-quote", "alphavantage-api-key");
+    if (!av_key)
+        PWARN("No Alpha Vantage API key set, currency quotes and other AlphaVantage based quotes won't work.");
 
-    for (auto line : cmd_out.second)
-        m_error_msg.append(std::move(line) + "\n");
+    try
+    {
+        std::future<std::vector<char> > out_buf, err_buf;
+        boost::asio::io_service svc;
 
-    if (m_cmd_result == 0)
-        std::sort (m_sources.begin(), m_sources.end());
+        auto input_buf = bp::buffer (json_string);
+        bp::child process (c_cmd, args,
+                           bp::std_out > out_buf,
+                           bp::std_err > err_buf,
+                           bp::std_in < input_buf,
+                           bp::env["ALPHAVANTAGE_API_KEY"]= (av_key ? av_key : ""),
+                           svc);
+        svc.run();
+        process.wait();
+
+        {
+            auto raw = out_buf.get();
+            std::vector<std::string> data;
+            std::string line;
+            bio::stream_buffer<bio::array_source> sb(raw.data(), raw.size());
+            std::istream is(&sb);
+
+            while (std::getline(is, line) && !line.empty())
+                out_vec.push_back (std::move(line));
+
+            raw = err_buf.get();
+            bio::stream_buffer<bio::array_source> eb(raw.data(), raw.size());
+            std::istream es(&eb);
+
+            while (std::getline(es, line) && !line.empty())
+                err_vec.push_back (std::move(line));
+        }
+        cmd_result = process.exit_code();
+    }
+    catch (std::exception &e)
+    {
+        cmd_result = -1;
+        err_vec.push_back(e.what());
+    };
+
+    return QuoteResult (cmd_result, std::move(out_vec), std::move(err_vec));
+}
+
+/* GncQuotes implementation */
+GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
+m_version{}, m_sources{}, m_cmd_result{}, m_error_msg{}, m_book{qof_session_get_book(gnc_get_current_session())},
+m_dflt_curr{gnc_default_currency()}
+{
+    if (!m_quotesource->usable())
+        return;
+    m_sources = m_quotesource->get_sources();
+}
+
+GncQuotesImpl::GncQuotesImpl(QofBook* book) : m_quotesource{new GncFQQuoteSource},
+m_version{}, m_sources{}, m_cmd_result{}, m_error_msg{}, m_book{book},
+m_dflt_curr{gnc_default_currency()}
+{
+    if (!m_quotesource->usable())
+        return;
+    m_sources = m_quotesource->get_sources();
+}
+
+GncQuotesImpl::GncQuotesImpl(QofBook* book, std::unique_ptr<GncQuoteSource> quote_source) :
+m_quotesource{std::move(quote_source)},
+m_version{}, m_sources{}, m_cmd_result{}, m_error_msg{}, m_book{book},
+m_dflt_curr{gnc_default_currency()}
+{
+    if (!m_quotesource->usable())
+        return;
+    m_sources = m_quotesource->get_sources();
 }
 
 GList*
@@ -181,59 +298,6 @@ format_quotes (const std::vector<gnc_commodity*>)
     return std::vector <std::string>();
 }
 
-
-template <typename BufferT> CmdOutput
-GncQuotesImpl::run_cmd (const bfs::path &cmd_name, StrVec args, BufferT input)
-{
-    StrVec out_vec, err_vec;
-
-    auto av_key = gnc_prefs_get_string ("general.finance-quote", "alphavantage-api-key");
-    if (!av_key)
-        std::cerr << "No Alpha Vantage API key set, currency quotes and other AlphaVantage based quotes won't work.\n";
-
-    try
-    {
-        std::future<std::vector<char> > out_buf, err_buf;
-        boost::asio::io_service svc;
-
-        auto input_buf = bp::buffer (input);
-        bp::child process (cmd_name, args,
-                           bp::std_out > out_buf,
-                           bp::std_err > err_buf,
-                           bp::std_in < input_buf,
-                           bp::env["ALPHAVANTAGE_API_KEY"]= (av_key ? av_key : ""),
-                           svc);
-        svc.run();
-        process.wait();
-
-        {
-            auto raw = out_buf.get();
-            std::vector<std::string> data;
-            std::string line;
-            bio::stream_buffer<bio::array_source> sb(raw.data(), raw.size());
-            std::istream is(&sb);
-
-            while (std::getline(is, line) && !line.empty())
-                out_vec.push_back (std::move(line));
-
-            raw = err_buf.get();
-            bio::stream_buffer<bio::array_source> eb(raw.data(), raw.size());
-            std::istream es(&eb);
-
-            while (std::getline(es, line) && !line.empty())
-                err_vec.push_back (std::move(line));
-        }
-        m_cmd_result = process.exit_code();
-    }
-    catch (std::exception &e)
-    {
-        m_cmd_result = -1;
-        m_error_msg = e.what();
-    };
-
-    return CmdOutput (std::move(out_vec), std::move(err_vec));
-}
-
 void
 GncQuotesImpl::query_fq (void)
 {
@@ -262,18 +326,14 @@ GncQuotesImpl::query_fq (void)
     std::ostringstream result;
     bpt::write_json(result, pt);
 
-    auto perl_executable = bp::search_path("perl");
-    auto fq_wrapper = std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper";
-    StrVec args { "-w", fq_wrapper, "-f" };
-
-    auto cmd_out = run_cmd (perl_executable.string(), args, result.str());
-
+    auto [rv, quotes, errors] = m_quotesource->get_quotes(result.str());
     m_fq_answer.clear();
-    if (m_cmd_result == 0)
-        for (auto line : cmd_out.first)
+    m_cmd_result = rv;
+    if (rv == 0)
+        for (auto line : quotes)
             m_fq_answer.append(std::move(line) + "\n");
     else
-        for (auto line : cmd_out.second)
+        for (auto line : errors)
             m_error_msg.append(std::move(line) + "\n");
 
 //     for (auto line : cmd_out.first)
@@ -451,6 +511,9 @@ GncQuotesImpl::parse_quotes (void)
  * gnc_quotes_get_quotable_commodities
  * list commodities in a given namespace that get price quotes
  ********************************************************************/
+/* Helper function to be passed to g_list_for_each applied to the result
+ * of gnc_commodity_namespace_get_commodity_list.
+ */
 static void
 get_quotables_helper1 (gpointer value, gpointer data)
 {
@@ -466,6 +529,7 @@ get_quotables_helper1 (gpointer value, gpointer data)
     l->push_back (comm);
 }
 
+// Helper function to be passed to gnc_commodity_table_for_each
 static gboolean
 get_quotables_helper2 (gnc_commodity *comm, gpointer data)
 {

commit e9577b7996b9faaf1858b649218d696afb11fda6
Author: John Ralls <jralls at ceridwen.us>
Date:   Thu Sep 1 12:40:50 2022 -0700

    [price-quotes] Basic wiggle test.

diff --git a/libgnucash/app-utils/test/CMakeLists.txt b/libgnucash/app-utils/test/CMakeLists.txt
index 49841f0a0..799e655ad 100644
--- a/libgnucash/app-utils/test/CMakeLists.txt
+++ b/libgnucash/app-utils/test/CMakeLists.txt
@@ -31,6 +31,23 @@ gnc_add_test_with_guile(test-sx test-sx.cpp
   APP_UTILS_TEST_INCLUDE_DIRS APP_UTILS_TEST_LIBS
 )
 
+set(test_gnc_quotes_SOURCES
+        gtest-gnc-quotes.cpp
+        )
+
+set(test_gnc_quotes_INCLUDES
+        ${CMAKE_BINARY_DIR}/common # for config.h
+        ${MODULEPATH}
+        )
+
+set(test_gnc_quotes_LIBS
+        gnc-engine
+        gtest
+        ${Boost_FILESYSTEM_LIBRARY}
+        ${Boost_PROPERTY_TREE_LIBRARY}
+        ${Boost_SYSTEM_LIBRARY}
+        )
+gnc_add_test(test-gnc-quotes "${test_gnc_quotes_SOURCES}" test_gnc_quotes_INCLUDES test_gnc_quotes_LIBS)
 
 set(GUILE_DEPENDS
   scm-test-engine
@@ -43,6 +60,7 @@ set(GUILE_DEPENDS
 
 set_dist_list(test_app_utils_DIST
   CMakeLists.txt
+  gtest-gnc-quotes.cpp
   test-exp-parser.c
   test-print-parse-amount.cpp
   test-sx.cpp
diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
new file mode 100644
index 000000000..c219f2cb9
--- /dev/null
+++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp
@@ -0,0 +1,121 @@
+/********************************************************************\
+ * test-gnc-price-quotes.cpp -- Unit tests for GncQuotes            *
+ *                                                                  *
+ * Copyright 2022 John Ralls <jralls at ceridwen.us>                   *
+ *                                                                  *
+ * 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                   *
+ *                                                                  *
+\********************************************************************/
+
+extern "C"
+{
+#include <config.h>
+#include <gnc-session.h>
+#include <gnc-commodity.h>
+#include <gnc-pricedb-p.h>
+#include <qof.h>
+
+/* gnc-quotes normally gets this from gnc-ui-util, but let's avoid the dependency. */
+static gnc_commodity*
+gnc_default_currency(void)
+{
+    auto book{qof_session_get_book(gnc_get_current_session())};
+    auto table{gnc_commodity_table_get_table(book)};
+    return gnc_commodity_table_lookup(table, GNC_COMMODITY_NS_CURRENCY, "USD");
+}
+
+} // extern "C"
+#include <gtest/gtest.h>
+#include "../gnc-quotes.cpp"
+
+class GncQuotesTest : public ::testing::Test
+{
+protected:
+    GncQuotesTest() : m_session{gnc_get_current_session()},
+    m_book{qof_session_get_book(gnc_get_current_session())}
+    {
+        qof_init();
+        gnc_commodity_table_register();
+        gnc_pricedb_register();
+        auto comm_table{gnc_commodity_table_new()};
+        qof_book_set_data(m_book, GNC_COMMODITY_TABLE, comm_table);
+        auto eur = gnc_commodity_new(m_book, "Euro", "ISO4217", "EUR", NULL, 100);
+        auto source{gnc_quote_source_lookup_by_internal("currency")};
+        gnc_commodity_begin_edit(eur);
+        gnc_commodity_set_quote_flag(eur, TRUE);
+        gnc_commodity_set_quote_source(eur, source);
+        gnc_commodity_commit_edit(eur);
+        gnc_commodity_table_insert(comm_table, eur);
+        auto usd = gnc_commodity_new(m_book, "United States Dollar", "CURRENCY",
+                                  "USD", NULL, 100);
+        gnc_commodity_table_insert(comm_table, usd);
+        source = gnc_quote_source_lookup_by_internal("yahoo_json");
+        auto aapl = gnc_commodity_new(m_book, "Apple", "NASDAQ", "AAPL", NULL, 1);
+        gnc_commodity_begin_edit(aapl);
+        gnc_commodity_set_quote_flag(aapl, TRUE);
+        gnc_commodity_set_quote_source(aapl, source);
+        gnc_commodity_commit_edit(aapl);
+        gnc_commodity_table_insert(comm_table, aapl);
+        auto hpe = gnc_commodity_new(m_book, "Hewlett Packard", "NYSE", "HPE",
+                                  NULL, 1);
+        gnc_commodity_begin_edit(hpe);
+        gnc_commodity_set_quote_flag(hpe, TRUE);
+        gnc_commodity_set_quote_source(hpe, source);
+        gnc_commodity_commit_edit(hpe);
+        gnc_commodity_table_insert(comm_table, hpe);
+        auto fkcm = gnc_commodity_new(m_book, "Fake Company", "NASDAQ", "FKCM", NULL, 1);
+        gnc_commodity_begin_edit(fkcm);
+        gnc_commodity_set_quote_flag(fkcm, TRUE);
+        gnc_commodity_set_quote_source(fkcm, source);
+        gnc_commodity_commit_edit(fkcm);
+        gnc_commodity_table_insert(comm_table, fkcm);
+        gnc_quote_source_set_fq_installed("TestSuite", g_list_prepend(nullptr, (void*)"yahoo_json"));
+    }
+    ~GncQuotesTest() {
+        gnc_clear_current_session();
+    }
+
+    QofSession* m_session;
+    QofBook* m_book;
+};
+
+TEST_F(GncQuotesTest, quote_sources)
+{
+    auto qs_cur{gnc_quote_source_lookup_by_internal("currency")};
+    auto qs_yahoo{gnc_quote_source_lookup_by_internal("yahoo_json")};
+    auto qs_alpha{gnc_quote_source_lookup_by_internal("alphavantage")};
+    EXPECT_TRUE(qs_cur != nullptr);
+    EXPECT_TRUE(qs_yahoo != nullptr);
+    EXPECT_TRUE(qs_alpha != nullptr);
+    EXPECT_TRUE(gnc_quote_source_get_supported(qs_cur));
+    EXPECT_TRUE(gnc_quote_source_get_supported(qs_yahoo));
+    EXPECT_FALSE(gnc_quote_source_get_supported(qs_alpha));
+}
+
+TEST_F(GncQuotesTest, quotable_commodities)
+{
+    auto commodities{gnc_quotes_get_quotable_commodities(gnc_commodity_table_get_table(m_book))};
+    EXPECT_EQ(4u, commodities.size());
+}
+TEST_F(GncQuotesTest, wiggle)
+{
+    GncQuotes quotes;
+    quotes.fetch(m_book);
+    auto pricedb{gnc_pricedb_get_db(m_book)};
+    EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
+}

commit 277f299ad625daa641baaced2b723063d8eafb34
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Mar 19 18:06:23 2021 +0100

    GncQuotes - cleanups
    
    - make more use of auto
    - mark user visible strings as translatable
    - return early on input errors
    - fix date conversion fallback to actually fall back to today

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 37e89c01a..39775da09 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -297,14 +297,16 @@ GncQuotesImpl::parse_quotes (void)
     catch (bpt::json_parser_error &e) {
         m_cmd_result = -1;
         m_error_msg = m_error_msg +
-                      "Failed to parse quotes results." + "\n" +
-                      "Error message:" + "\n" +
-                      e.what() + "\n";
+                      _("Failed to parse result returned by Finance::Quote.") + "\n" +
+                      _("Error message:") + "\n" +
+                       e.what() + "\n";
+        return;
     }
     catch (...) {
         m_cmd_result = -1;
         m_error_msg = m_error_msg +
-                      "Failed to parse quotes results." + "\n";
+                      _("Failed to parse result returned by Finance::Quote.") + "\n";
+        return;
     }
 
     auto pricedb = gnc_pricedb_get_db (m_book);
@@ -323,9 +325,9 @@ GncQuotesImpl::parse_quotes (void)
                     }
 
                     std::string key = comm_mnemonic;
-                    boost::optional<bool> success = pt.get_optional<bool> (key + ".success");
+                    auto success = pt.get_optional<bool> (key + ".success");
                     std::string price_type = "last";
-                    boost::optional<std::string> price_str = pt.get_optional<std::string> (key + "." + price_type);
+                    auto price_str = pt.get_optional<std::string> (key + "." + price_type);
                     if (!price_str)
                     {
                         price_type = "nav";
@@ -340,11 +342,11 @@ GncQuotesImpl::parse_quotes (void)
                         price_type = "unknown";
                     }
 
-                    boost::optional<bool> inverted_tmp = pt.get_optional<bool> (key + ".inverted");
-                    bool inverted = inverted_tmp ? *inverted_tmp : false;
-                    boost::optional<std::string> date_str = pt.get_optional<std::string> (key + ".date");
-                    boost::optional<std::string> time_str = pt.get_optional<std::string> (key + ".time");
-                    boost::optional<std::string> currency_str = pt.get_optional<std::string> (key + ".currency");
+                    auto inverted_tmp = pt.get_optional<bool> (key + ".inverted");
+                    auto inverted = inverted_tmp ? *inverted_tmp : false;
+                    auto date_str = pt.get_optional<std::string> (key + ".date");
+                    auto time_str = pt.get_optional<std::string> (key + ".time");
+                    auto currency_str = pt.get_optional<std::string> (key + ".currency");
 
 
                     std::cout << "Commodity: " << comm_mnemonic << "\n";
@@ -356,7 +358,7 @@ GncQuotesImpl::parse_quotes (void)
 
                     if (!success || !*success)
                     {
-                        boost::optional<std::string> errmsg = pt.get_optional<std::string> (key + ".errormsg");
+                        auto errmsg = pt.get_optional<std::string> (key + ".errormsg");
                         std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote returned fetch failure.\n";
                         std::cerr << "Reason: " << (errmsg ? *errmsg : "unknown") << "\n";
                         return;
@@ -416,7 +418,7 @@ GncQuotesImpl::parse_quotes (void)
                     catch (...)
                     {
                         std::cerr << "Warning: failed to parse quote date and time '" << iso_date_str << "' for " << comm_ns << ":" << comm_mnemonic << " - will use today\n";
-                        return;
+                        can_convert = false;
                     }
 
                     /*  Bit of an odd construct: GncDateTimes can't be copied,

commit 585de5d1349acd178044c7ef208e9e6eb2f0b32c
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Mar 19 17:46:46 2021 +0100

    GncQuotes - break actual interaction with Finance::Quote out into a separate function query_fq
    
    That allows for later reuse and easier testing.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 05a53dd0d..37e89c01a 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -85,7 +85,8 @@ private:
     // Will also set m_cmd_result
     template <typename BufferT> CmdOutput run_cmd (const bfs::path &cmd_name, StrVec args, BufferT input);
 
-    void parse_quotes (const std::string &quotes);
+    void query_fq (void);
+    void parse_quotes (void);
 
 
     CommVec m_comm_vec;
@@ -93,6 +94,7 @@ private:
     QuoteSources m_sources;
     int m_cmd_result;
     std::string m_error_msg;
+    std::string m_fq_answer;
     QofBook *m_book;
     gnc_commodity *m_dflt_curr;
 };
@@ -137,74 +139,14 @@ GncQuotesImpl::sources_as_glist()
 }
 
 
-void
-GncQuotesImpl::fetch (CommVec& commodities)
-{
-    if (commodities.empty())
-        return;
-
-    m_comm_vec = std::move (commodities);  // Store for later use
-    m_book = qof_instance_get_book (m_comm_vec[0]);
-
-    bpt::ptree pt, pt_child;
-    pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
-
-    std::for_each (m_comm_vec.cbegin(), m_comm_vec.cend(),
-        [this, &pt] (auto comm)
-        {
-            auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
-            auto comm_ns = std::string("currency");
-            if (gnc_commodity_is_currency (comm))
-            {
-                if (gnc_commodity_equiv(comm, m_dflt_curr) ||
-                    (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
-                    return;
-            }
-            else
-                comm_ns = gnc_quote_source_get_internal_name (gnc_commodity_get_quote_source (comm));
-
-            auto key = comm_ns + "." + comm_mnemonic;
-            pt.put (key, "");
-        }
-    );
-
-    std::ostringstream result;
-    bpt::write_json(result, pt);
-    //std::cerr << "GncQuotes fetch_all - resulting json object\n" << result.str() << std::endl;
-
-    auto perl_executable = bp::search_path("perl");
-    auto fq_wrapper = std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper";
-    StrVec args { "-w", fq_wrapper, "-f" };
-
-    auto cmd_out = run_cmd (perl_executable.string(), args, result.str());
-
-    if (m_cmd_result == 0)
-    {
-        std::string resultstr;
-        for (auto line : cmd_out.first)
-            resultstr.append(std::move(line) + "\n");
-        parse_quotes (resultstr);
-    }
-    else
-        for (auto line : cmd_out.second)
-            m_error_msg.append(std::move(line) + "\n");
-
-    for (auto line : cmd_out.first)
-        std::cerr << "Output line retrieved from wrapper:\n" << line << std::endl;
-
-    for (auto line : cmd_out.second)
-        std::cerr << "Error line retrieved from wrapper:\n" << line << std::endl;
-
-}
-
-
 void
 GncQuotesImpl::fetch (QofBook *book)
 {
     if (!book)
     {
         m_cmd_result = 1;
-        m_error_msg = "No book set\n";
+        m_error_msg = _("No book set");
+        m_error_msg += "\n";
         return;
     }
     auto commodities = gnc_quotes_get_quotable_commodities (
@@ -212,7 +154,6 @@ GncQuotesImpl::fetch (QofBook *book)
     fetch (commodities);
 }
 
-
 void
 GncQuotesImpl::fetch (gnc_commodity *comm)
 {
@@ -220,6 +161,20 @@ GncQuotesImpl::fetch (gnc_commodity *comm)
     fetch (commodities);
 }
 
+void
+GncQuotesImpl::fetch (CommVec& commodities)
+{
+    if (commodities.empty())
+        return;
+
+    m_comm_vec = std::move (commodities);  // Store for later use
+    m_book = qof_instance_get_book (m_comm_vec[0]);
+
+    query_fq ();
+    if (m_cmd_result == 0)
+        parse_quotes ();
+}
+
 static const std::vector <std::string>
 format_quotes (const std::vector<gnc_commodity*>)
 {
@@ -234,7 +189,7 @@ GncQuotesImpl::run_cmd (const bfs::path &cmd_name, StrVec args, BufferT input)
 
     auto av_key = gnc_prefs_get_string ("general.finance-quote", "alphavantage-api-key");
     if (!av_key)
-        std::cerr << "No AlphaVantage API key set, currency quotes and other AlphaVantage based quotes won't work." << std::endl;
+        std::cerr << "No Alpha Vantage API key set, currency quotes and other AlphaVantage based quotes won't work.\n";
 
     try
     {
@@ -280,10 +235,60 @@ GncQuotesImpl::run_cmd (const bfs::path &cmd_name, StrVec args, BufferT input)
 }
 
 void
-GncQuotesImpl::parse_quotes (const std::string &quotes_str)
+GncQuotesImpl::query_fq (void)
+{
+    bpt::ptree pt, pt_child;
+    pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
+
+    std::for_each (m_comm_vec.cbegin(), m_comm_vec.cend(),
+                   [this, &pt] (auto comm)
+                   {
+                       auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
+                       auto comm_ns = std::string("currency");
+                       if (gnc_commodity_is_currency (comm))
+                       {
+                           if (gnc_commodity_equiv(comm, m_dflt_curr) ||
+                               (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
+                               return;
+                       }
+                       else
+                           comm_ns = gnc_quote_source_get_internal_name (gnc_commodity_get_quote_source (comm));
+
+                       auto key = comm_ns + "." + comm_mnemonic;
+                       pt.put (key, "");
+                   }
+    );
+
+    std::ostringstream result;
+    bpt::write_json(result, pt);
+
+    auto perl_executable = bp::search_path("perl");
+    auto fq_wrapper = std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper";
+    StrVec args { "-w", fq_wrapper, "-f" };
+
+    auto cmd_out = run_cmd (perl_executable.string(), args, result.str());
+
+    m_fq_answer.clear();
+    if (m_cmd_result == 0)
+        for (auto line : cmd_out.first)
+            m_fq_answer.append(std::move(line) + "\n");
+    else
+        for (auto line : cmd_out.second)
+            m_error_msg.append(std::move(line) + "\n");
+
+//     for (auto line : cmd_out.first)
+//         std::cerr << "Output line retrieved from wrapper:\n" << line << std::endl;
+//
+//     for (auto line : cmd_out.second)
+//         std::cerr << "Error line retrieved from wrapper:\n" << line << std::endl;
+
+}
+
+void
+GncQuotesImpl::parse_quotes (void)
 {
     bpt::ptree pt;
-    std::istringstream ss {quotes_str};
+    std::istringstream ss {m_fq_answer};
 
     try
     {

commit 70ab8a8a462b2bf99215ddfcea131ae8e0d6cd98
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Thu Mar 18 19:12:23 2021 +0100

    GncQuotes - drop parameterized constructor
    
    The book parameter is only needed while fetching quotes.
    In case the user passes one or more commodities to process
    the book can be readily derived from the commodity/commodities.
    In the other case (fetch all quotes) the user now is
    required to pass a book to the call.

diff --git a/gnucash/gnome-utils/dialog-transfer.cpp b/gnucash/gnome-utils/dialog-transfer.cpp
index 9c5d2602b..9a818d558 100644
--- a/gnucash/gnome-utils/dialog-transfer.cpp
+++ b/gnucash/gnome-utils/dialog-transfer.cpp
@@ -1785,7 +1785,7 @@ gnc_xfer_dialog_fetch (GtkButton *button, XferDialog *xferData)
 
     ENTER(" ");
 
-    GncQuotes quotes (xferData->book);
+    GncQuotes quotes;
     if (quotes.cmd_result() != 0)
     {
         if (!quotes.error_msg().empty())
@@ -1795,7 +1795,7 @@ gnc_xfer_dialog_fetch (GtkButton *button, XferDialog *xferData)
     }
 
     gnc_set_busy_cursor (nullptr, TRUE);
-    quotes.fetch();
+    quotes.fetch (xferData->book);
     gnc_unset_busy_cursor (nullptr);
 
     /*the results should be in the price db now, but don't crash if not. */
diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index d6b9614c3..bec4fdb31 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -559,7 +559,7 @@ gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data)
     auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
-    GncQuotes quotes (pdb_dialog->book);
+    GncQuotes quotes;
     if (quotes.cmd_result() != 0)
     {
         if (!quotes.error_msg().empty())
@@ -569,7 +569,7 @@ gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data)
     }
 
     gnc_set_busy_cursor (NULL, TRUE);
-    quotes.fetch();
+    quotes.fetch (pdb_dialog->book);
     gnc_unset_busy_cursor (NULL);
 
     /* Without this, the summary bar on the accounts tab
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index afab139c7..145cd009c 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -341,7 +341,7 @@ Gnucash::add_quotes (const bo_str& uri)
     if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
         cleanup_and_exit_with_failure (session);
 
-    GncQuotes quotes (qof_session_get_book(session));
+    GncQuotes quotes;
     if (quotes.cmd_result() == 0)
     {
         std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
@@ -356,7 +356,7 @@ Gnucash::add_quotes (const bo_str& uri)
         std::cerr << bl::translate ("Error message:") << std::endl;
         std::cerr << quotes.error_msg() << std::endl;
     }
-    quotes.fetch ();
+    quotes.fetch (qof_session_get_book(session));
 
     qof_session_save(session, NULL);
     if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 27d5aeb56..05a53dd0d 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -66,7 +66,7 @@ public:
     GncQuotesImpl ();
     GncQuotesImpl (QofBook *book);
 
-    void fetch ();
+    void fetch (QofBook *book);
     void fetch (CommVec& commodities);
     void fetch (gnc_commodity *comm);
 
@@ -100,23 +100,12 @@ private:
 /* GncQuotes implementation */
 
 GncQuotesImpl::GncQuotesImpl ()
-{
-    check (nullptr);
-}
-
-GncQuotesImpl::GncQuotesImpl (QofBook *book)
-{
-    check (book);
-}
-
-void
-GncQuotesImpl::check (QofBook *book)
 {
     m_version.clear();
     m_sources.clear();
     m_error_msg.clear();
     m_cmd_result  = 0;
-    m_book = book;
+    m_book = nullptr;
     m_dflt_curr = gnc_default_currency();
 
     auto perl_executable = bp::search_path("perl");
@@ -151,7 +140,11 @@ GncQuotesImpl::sources_as_glist()
 void
 GncQuotesImpl::fetch (CommVec& commodities)
 {
+    if (commodities.empty())
+        return;
+
     m_comm_vec = std::move (commodities);  // Store for later use
+    m_book = qof_instance_get_book (m_comm_vec[0]);
 
     bpt::ptree pt, pt_child;
     pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
@@ -206,11 +199,16 @@ GncQuotesImpl::fetch (CommVec& commodities)
 
 
 void
-GncQuotesImpl::fetch ()
+GncQuotesImpl::fetch (QofBook *book)
 {
+    if (!book)
+    {
+        m_cmd_result = 1;
+        m_error_msg = "No book set\n";
+        return;
+    }
     auto commodities = gnc_quotes_get_quotable_commodities (
-        gnc_commodity_table_get_table (m_book));
-
+        gnc_commodity_table_get_table (book));
     fetch (commodities);
 }
 
@@ -219,7 +217,6 @@ void
 GncQuotesImpl::fetch (gnc_commodity *comm)
 {
     auto commodities = CommVec {comm};
-
     fetch (commodities);
 }
 
@@ -533,15 +530,10 @@ GncQuotes::GncQuotes ()
     m_impl = std::make_unique<GncQuotesImpl> ();
 }
 
-GncQuotes::GncQuotes (QofBook *book)
-{
-    m_impl = std::make_unique<GncQuotesImpl> (book);
-}
-
 void
-GncQuotes::fetch (void)
+GncQuotes::fetch (QofBook *book)
 {
-    m_impl->fetch ();
+    m_impl->fetch (book);
 }
 
 void GncQuotes::fetch (CommVec& commodities)
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 7ab51bffb..3c2ec0f5c 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -45,11 +45,10 @@ class GncQuotes
 public:
     // Constructor - checks for presence of Finance::Quote and import version and quote sources
     GncQuotes ();
-    GncQuotes (QofBook *book);
     ~GncQuotes ();
 
     // Fetch quotes for all commodities in our db that have a quote source set
-    void fetch (void);
+    void fetch (QofBook *book);
     // Only fetch quotes for the commodities passed that have a quote source  set
     void fetch (CommVec& commodities);
     // Fetch quote for the commodity if it has a quote source  set

commit 4c2863966b20f48a18c2b202d98ab142ed0679b2
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Thu Mar 18 16:33:16 2021 +0100

    GncQuotes - rename fetch_all to be an overload of fetch
    
    And add a third overload to fetch only a single quote

diff --git a/gnucash/gnome-utils/dialog-transfer.cpp b/gnucash/gnome-utils/dialog-transfer.cpp
index 354e982fb..9c5d2602b 100644
--- a/gnucash/gnome-utils/dialog-transfer.cpp
+++ b/gnucash/gnome-utils/dialog-transfer.cpp
@@ -1795,7 +1795,7 @@ gnc_xfer_dialog_fetch (GtkButton *button, XferDialog *xferData)
     }
 
     gnc_set_busy_cursor (nullptr, TRUE);
-    quotes.fetch_all();
+    quotes.fetch();
     gnc_unset_busy_cursor (nullptr);
 
     /*the results should be in the price db now, but don't crash if not. */
diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index aca814585..d6b9614c3 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -569,7 +569,7 @@ gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data)
     }
 
     gnc_set_busy_cursor (NULL, TRUE);
-    quotes.fetch_all();
+    quotes.fetch();
     gnc_unset_busy_cursor (NULL);
 
     /* Without this, the summary bar on the accounts tab
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index f57cc62b0..afab139c7 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -356,7 +356,7 @@ Gnucash::add_quotes (const bo_str& uri)
         std::cerr << bl::translate ("Error message:") << std::endl;
         std::cerr << quotes.error_msg() << std::endl;
     }
-    quotes.fetch_all ();
+    quotes.fetch ();
 
     qof_session_save(session, NULL);
     if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 42bbbacbb..27d5aeb56 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -66,8 +66,9 @@ public:
     GncQuotesImpl ();
     GncQuotesImpl (QofBook *book);
 
-    void fetch_all ();
-    void fetch (const CommVec& commodities);
+    void fetch ();
+    void fetch (CommVec& commodities);
+    void fetch (gnc_commodity *comm);
 
     const int cmd_result() noexcept { return m_cmd_result; }
     const std::string& error_msg() noexcept { return m_error_msg; }
@@ -148,9 +149,9 @@ GncQuotesImpl::sources_as_glist()
 
 
 void
-GncQuotesImpl::fetch (const CommVec& commodities)
+GncQuotesImpl::fetch (CommVec& commodities)
 {
-    m_comm_vec = commodities;  // Store for later use
+    m_comm_vec = std::move (commodities);  // Store for later use
 
     bpt::ptree pt, pt_child;
     pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
@@ -205,7 +206,7 @@ GncQuotesImpl::fetch (const CommVec& commodities)
 
 
 void
-GncQuotesImpl::fetch_all ()
+GncQuotesImpl::fetch ()
 {
     auto commodities = gnc_quotes_get_quotable_commodities (
         gnc_commodity_table_get_table (m_book));
@@ -213,6 +214,15 @@ GncQuotesImpl::fetch_all ()
     fetch (commodities);
 }
 
+
+void
+GncQuotesImpl::fetch (gnc_commodity *comm)
+{
+    auto commodities = CommVec {comm};
+
+    fetch (commodities);
+}
+
 static const std::vector <std::string>
 format_quotes (const std::vector<gnc_commodity*>)
 {
@@ -529,16 +539,21 @@ GncQuotes::GncQuotes (QofBook *book)
 }
 
 void
-GncQuotes::fetch_all ()
+GncQuotes::fetch (void)
 {
-    m_impl->fetch_all ();
+    m_impl->fetch ();
 }
 
-void GncQuotes::fetch (const CommVec& commodities)
+void GncQuotes::fetch (CommVec& commodities)
 {
     m_impl->fetch (commodities);
 }
 
+void GncQuotes::fetch (gnc_commodity *comm)
+{
+    m_impl->fetch (comm);
+}
+
 const int GncQuotes::cmd_result() noexcept
 {
     return m_impl->cmd_result ();
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 87ea68a11..7ab51bffb 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -26,9 +26,9 @@
 #include <string>
 #include <vector>
 #include <gnc-commodity.hpp>  // For CommVec alias
+#include <glib.h>
 
 extern  "C" {
-#include <glib.h>
 #include <qofbook.h>
 }
 
@@ -48,8 +48,12 @@ public:
     GncQuotes (QofBook *book);
     ~GncQuotes ();
 
-    void fetch_all ();
-    void fetch (const CommVec& commodities);
+    // Fetch quotes for all commodities in our db that have a quote source set
+    void fetch (void);
+    // Only fetch quotes for the commodities passed that have a quote source  set
+    void fetch (CommVec& commodities);
+    // Fetch quote for the commodity if it has a quote source  set
+    void fetch (gnc_commodity *comm);
 
     const int cmd_result() noexcept;
     const std::string& error_msg() noexcept;

commit 7765e1370489c6b4d31a36956082a19ae740d736
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Wed Mar 17 15:04:02 2021 +0100

    Bindings - move log wrappers into the swig interface files
    
    They are only used for guile (and possibly python, so there's
    no need to carry them around in core-utils.

diff --git a/bindings/core-utils.i b/bindings/core-utils.i
index 6cef88a98..e950532ab 100644
--- a/bindings/core-utils.i
+++ b/bindings/core-utils.i
@@ -90,11 +90,6 @@ gchar * gnc_build_stdreports_path(const gchar *);
 %newobject gnc_build_reports_path;
 gchar * gnc_build_reports_path(const gchar *);
 
-void gnc_scm_log_warn(const gchar *);
-void gnc_scm_log_error(const gchar *);
-void gnc_scm_log_msg(const gchar *);
-void gnc_scm_log_debug(const gchar *);
-
 %newobject gnc_utf8_strip_invalid_strdup;
 gchar * gnc_utf8_strip_invalid_strdup(const gchar *);
 
diff --git a/bindings/engine.i b/bindings/engine.i
index a93e573a9..7bd3f274e 100644
--- a/bindings/engine.i
+++ b/bindings/engine.i
@@ -111,6 +111,15 @@ engine-common.i */
 %include "qoflog.h"
 
 %inline %{
+static void gnc_log_warn(const char *msg)
+{ g_log("gnc.scm", G_LOG_LEVEL_WARNING, "%s", msg); }
+static void gnc_log_error(const char *msg)
+{ g_log("gnc.scm", G_LOG_LEVEL_CRITICAL, "%s", msg); }
+static void gnc_log_msg(const char *msg)
+{ g_log("gnc.scm", G_LOG_LEVEL_MESSAGE, "%s", msg); }
+static void gnc_log_debug(const char *msg)
+{ g_log("gnc.scm", G_LOG_LEVEL_DEBUG, "%s", msg); }
+
 static const GncGUID * gncPriceGetGUID(GNCPrice *x)
 { return qof_instance_get_guid(QOF_INSTANCE(x)); }
 static const GncGUID * gncBudgetGetGUID(GncBudget *x)
diff --git a/bindings/guile/utilities.scm b/bindings/guile/utilities.scm
index 0ac565dac..e83b917ba 100644
--- a/bindings/guile/utilities.scm
+++ b/bindings/guile/utilities.scm
@@ -40,17 +40,17 @@
   (string-join (map (lambda (x) (format #f "~A" x)) items) ""))
 
 (define (gnc:warn . items)
-  (gnc-scm-log-warn (strify items)))
+  (gnc-log-warn (strify items)))
 
 (define (gnc:error . items)
-  (gnc-scm-log-error (strify items )))
+  (gnc-log-error (strify items )))
 
 (define (gnc:msg . items)
-  (gnc-scm-log-msg (strify items)))
+  (gnc-log-msg (strify items)))
 
 (define (gnc:debug . items)
   (when (qof-log-check "gnc.scm" QOF-LOG-DEBUG)
-    (gnc-scm-log-debug (strify items))))
+    (gnc-log-debug (strify items))))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; the following functions are initialized to log message to tracefile
diff --git a/libgnucash/core-utils/gnc-glib-utils.c b/libgnucash/core-utils/gnc-glib-utils.c
index 0dd36b9bc..b2e55af29 100644
--- a/libgnucash/core-utils/gnc-glib-utils.c
+++ b/libgnucash/core-utils/gnc-glib-utils.c
@@ -287,30 +287,6 @@ gnc_g_list_cut(GList **list, GList *cut_point)
     cut_point->prev = NULL;
 }
 
-void
-gnc_scm_log_warn(const gchar *msg)
-{
-    g_log("gnc.scm", G_LOG_LEVEL_WARNING, "%s", msg);
-}
-
-void
-gnc_scm_log_error(const gchar *msg)
-{
-    g_log("gnc.scm", G_LOG_LEVEL_CRITICAL, "%s", msg);
-}
-
-void
-gnc_scm_log_msg(const gchar *msg)
-{
-    g_log("gnc.scm", G_LOG_LEVEL_MESSAGE, "%s", msg);
-}
-
-void
-gnc_scm_log_debug(const gchar *msg)
-{
-    g_log("gnc.scm", G_LOG_LEVEL_DEBUG, "%s", msg);
-}
-
 
 gchar *
 gnc_g_list_stringjoin (GList *list_of_strings, const gchar *sep)
diff --git a/libgnucash/core-utils/gnc-glib-utils.h b/libgnucash/core-utils/gnc-glib-utils.h
index dc3046f4c..eb291336b 100644
--- a/libgnucash/core-utils/gnc-glib-utils.h
+++ b/libgnucash/core-utils/gnc-glib-utils.h
@@ -169,16 +169,6 @@ void gnc_g_list_cut(GList **list, GList *cut_point);
 
 /** @} */
 
-/** @name Message Logging
- @{
-*/
-void gnc_scm_log_warn(const gchar *msg);
-void gnc_scm_log_error(const gchar *msg);
-void gnc_scm_log_msg(const gchar *msg);
-void gnc_scm_log_debug(const gchar *msg);
-
-/** @} */
-
 
 /**
  * @brief Return a string joining a GList whose elements are gchar*

commit e5c6f6026b1132f0e6b76f621c82d9a2802fba7c
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Wed Mar 17 09:32:32 2021 +0100

    Remove support code that was only used by price-quotes.scm

diff --git a/bindings/app-utils.i b/bindings/app-utils.i
index b2a8b7ec6..fe141ac14 100644
--- a/bindings/app-utils.i
+++ b/bindings/app-utils.i
@@ -74,22 +74,6 @@ Account * gnc_get_current_root_account (void);
 
 
 #if defined(SWIGGUILE)
-%typemap(out) GncCommodityList * {
-  SCM list = SCM_EOL;
-  GList *node;
-
-  for (node = $1; node; node = node->next)
-      list = scm_cons(gnc_quoteinfo2scm(static_cast<gnc_commodity*>(node->data)), list);
-
-  $result = scm_reverse(list);
-}
-
-%inline %{
-typedef GList GncCommodityList;
-
-GncCommodityList *
-gnc_commodity_table_get_quotable_commodities(const gnc_commodity_table * table);
-%}
 
 gnc_commodity * gnc_default_currency (void);
 gnc_commodity * gnc_default_report_currency (void);
diff --git a/bindings/engine.i b/bindings/engine.i
index 4a94bf059..a93e573a9 100644
--- a/bindings/engine.i
+++ b/bindings/engine.i
@@ -248,34 +248,8 @@ SplitList * qof_query_run_subquery (QofQuery *q, const QofQuery *q);
 time64 time64CanonicalDayTime(time64 t);
 
 %include <gnc-budget.h>
-
-%typemap(in) GList * {
-  SCM path_scm = $input;
-  GList *path = NULL;
-
-  while (!scm_is_null (path_scm))
-  {
-    SCM key_scm = SCM_CAR (path_scm);
-    char *key;
-    if (!scm_is_string (key_scm))
-      break;
-
-    key = scm_to_locale_string (key_scm);
-    path = g_list_prepend (path, key);
-
-    path_scm = SCM_CDR (path_scm);
-  }
-
-  $1 = g_list_reverse (path);
-}
-
 %typemap (freearg) GList * "g_list_free_full ($1, g_free);"
 
-void gnc_quote_source_set_fq_installed (const char* version_string,
-                                        GList *sources_list);
-%clear GList *;
-%ignore gnc_quote_source_set_fq_installed;
-%ignore gnc_commodity_table_get_quotable_commodities;
 %include <gnc-commodity.h>
 
 void gnc_hook_add_scm_dangler (const gchar *name, SCM proc);
@@ -475,8 +449,3 @@ void qof_book_set_string_option(QofBook* book, const char* opt_name, const char*
     }
     $1 = g_list_reverse (path);
 }
-Process *gnc_spawn_process_async(GList *argl, const gboolean search_path);
-%clear GList *;
-
-gint gnc_process_get_fd(const Process *proc, const guint std_fd);
-void gnc_detach_process(Process *proc, const gboolean kill_it);
diff --git a/bindings/guile/glib-guile.c b/bindings/guile/glib-guile.c
index b38c476dd..849c069ab 100644
--- a/bindings/guile/glib-guile.c
+++ b/bindings/guile/glib-guile.c
@@ -202,139 +202,3 @@ gnc_glist_string_p(SCM list)
 {
     return scm_is_list(list);
 }
-
-struct _Process
-{
-    GPid pid;
-    gint fd_stdin;
-    gint fd_stdout;
-    gint fd_stderr;
-    gboolean dead;
-    gboolean detached;
-};
-
-static void
-on_child_exit (GPid pid, gint status, gpointer data)
-{
-    Process *proc = data;
-    g_return_if_fail (proc && proc->pid == pid);
-
-    g_spawn_close_pid (proc->pid);
-
-    /* free if the process is both dead and detached */
-    if (!proc->detached)
-        proc->dead = TRUE;
-    else
-        g_free (proc);
-}
-
-Process *
-gnc_spawn_process_async (GList *argl, const gboolean search_path)
-{
-    gboolean retval;
-    Process *proc;
-    GList *l_iter;
-    guint argc;
-    gchar **argv, **v_iter;
-    GSpawnFlags flags;
-    GError *error = NULL;
-
-    proc = g_new0 (Process, 1);
-
-    argc = g_list_length (argl);
-    argv = g_malloc ((argc + 1) * sizeof(gchar*));
-
-    for (l_iter = argl, v_iter = argv; l_iter; l_iter = l_iter->next, v_iter++)
-    {
-        *v_iter = (gchar*) l_iter->data;
-    }
-    *v_iter = NULL;
-    g_list_free (argl);
-
-    flags = G_SPAWN_DO_NOT_REAP_CHILD;
-    if (search_path)
-        flags |= G_SPAWN_SEARCH_PATH;
-
-    retval = g_spawn_async_with_pipes (
-        NULL, argv, NULL, flags, NULL, NULL, &proc->pid,
-        &proc->fd_stdin, &proc->fd_stdout, &proc->fd_stderr, &error);
-
-    if (retval)
-    {
-        g_child_watch_add (proc->pid, on_child_exit, proc);
-    }
-    else
-    {
-        PWARN ("Could not spawn %s: %s", *argv ? *argv : "(null)",
-                   error->message ? error->message : "(null)");
-        g_free (proc);
-        proc = NULL;
-    }
-    g_strfreev (argv);
-
-    return proc;
-}
-
-gint
-gnc_process_get_fd (const Process *proc, const gint std_fd)
-{
-    const gint *retptr = NULL;
-    g_return_val_if_fail (proc, -1);
-
-    if (std_fd == 0)
-        retptr = &proc->fd_stdin;
-    else if (std_fd == 1)
-        retptr = &proc->fd_stdout;
-    else if (std_fd == 2)
-        retptr = &proc->fd_stderr;
-    else
-        g_return_val_if_reached (-1);
-
-    if (*retptr == -1)
-        PWARN ("Pipe to child's file descriptor %d is -1", std_fd);
-    return *retptr;
-}
-
-void
-gnc_detach_process (Process *proc, const gboolean kill_it)
-{
-    g_return_if_fail (proc && proc->pid);
-
-    errno = 0;
-    close (proc->fd_stdin);
-    if (errno)
-    {
-        PINFO ("Close of child's stdin (%d) failed: %s", proc->fd_stdin,
-                   g_strerror (errno));
-        errno = 0;
-    }
-    close (proc->fd_stdout);
-    if (errno)
-    {
-        PINFO ("Close of child's stdout (%d) failed: %s", proc->fd_stdout,
-                   g_strerror(errno));
-        errno = 0;
-    }
-    close (proc->fd_stderr);
-    if (errno)
-    {
-        PINFO ("Close of child's stderr (%d) failed: %s", proc->fd_stderr,
-                   g_strerror(errno));
-        errno = 0;
-    }
-
-    if (kill_it && !proc->dead)
-    {
-        /* give it a chance to die */
-        while (g_main_context_iteration (NULL, FALSE) && !proc->dead)
-            ;
-        if (!proc->dead)
-            gnc_gpid_kill (proc->pid);
-    }
-
-    /* free if the process is both dead and detached */
-    if (!proc->dead)
-        proc->detached = TRUE;
-    else
-        g_free (proc);
-}
diff --git a/bindings/guile/glib-guile.h b/bindings/guile/glib-guile.h
index fd642a428..d22578eaf 100644
--- a/bindings/guile/glib-guile.h
+++ b/bindings/guile/glib-guile.h
@@ -37,40 +37,4 @@ int     gnc_glist_string_p(SCM list);
 
 GSList * gnc_scm_to_gslist_string(SCM list);
 
-/** An opaque process structure returned by gnc_spawn_process_async. */
-typedef struct _Process Process;
-
-/** Wraps g_spawn_async_with_pipes minimally.  Use gnc_process_get_fd to access
- *  the file descriptors to the child.  To close them and free the memory
- *  allocated for the process once it has exited, call gnc_detach_process.
- *
- *  @param argl A list of null-terminated strings used as arguments for spawning,
- *  i.e. "perl" "-w" "my-perl-script".  Will be freed inside.
- *
- *  @param search_path Determines whether the first element of argl will be
- *  looked for in the user's PATH.
- *
- *  @return A pointer to a structure representing the process or NULL on failure.
- */
-Process *gnc_spawn_process_async(GList *argl, const gboolean search_path);
-
-/** Accesses a given process structure and returns the file descriptor connected
- *  to the childs stdin, stdout or stderr.
- *
- *  @param proc A process structure returned by gnc_spawn_process_async.
- *
- *  @param std_fd 0, 1 or 2.
- *
- *  @return The file descriptor to write to the child on 0, or read from the
- *  childs output or error on 1 or 2, resp. */
-gint gnc_process_get_fd(const Process *proc, const gint std_fd);
-
-/** Close the file descriptors to a given process and declare it as detached.  If
- *  it is both dead and detached, the allocated memory will be freed.
- *
- *  @param proc A process structure returned by gnc_spawn_process_async.
- *
- *  @param kill_it If TRUE, kill the process. */
-void gnc_detach_process(Process *proc, const gboolean kill_it);
-
 #endif
diff --git a/bindings/guile/gnc-engine-guile.cpp b/bindings/guile/gnc-engine-guile.cpp
index e61027a97..7028bd33c 100644
--- a/bindings/guile/gnc-engine-guile.cpp
+++ b/bindings/guile/gnc-engine-guile.cpp
@@ -32,7 +32,6 @@ extern "C"
 #include "Account.h"
 #include "engine-helpers.h"
 #include "gnc-engine-guile.h"
-#include "glib-guile.h"
 #include "gnc-date.h"
 #include "gnc-engine.h"
 #include "gnc-session.h"
diff --git a/bindings/guile/gnc-helpers.c b/bindings/guile/gnc-helpers.c
index b1db73683..356b0fc29 100644
--- a/bindings/guile/gnc-helpers.c
+++ b/bindings/guile/gnc-helpers.c
@@ -92,44 +92,3 @@ gnc_scm2printinfo(SCM info_scm)
 
     return info;
 }
-
-/* This is a scaled down version of the routine that would be needed
- * to fully convert a gnc-commodity to a scheme data structure.  In an
- * attempt to optimize the speed of price quote retrieval, this
- * routine only converts the fields that price-quotes.scm uses. Since
- * it converts these fields all at once, it should prevent multiple
- * transitions back and forth from Scheme to C to extract
- * the data from a pointers to a gnc-commodity (the older method).
- * This is *not* a reversible conversion as it drops data.
- *
- * When this routine was written, gnucash retrieved all quotes into
- * the user's default currency.  (Did earlier version do any
- * different?)  This routine inserts that default currency into the
- * returned structure as another optimization.
- */
-SCM
-gnc_quoteinfo2scm(gnc_commodity *comm)
-{
-    gnc_quote_source *source;
-    const char *name, *tz;
-    SCM info_scm = SCM_EOL, comm_scm, def_comm_scm;
-
-    if (!comm)
-        return SCM_EOL;
-
-    source = gnc_commodity_get_quote_source (comm);
-    name = gnc_quote_source_get_internal_name (source);
-    tz = gnc_commodity_get_quote_tz (comm);
-    comm_scm = SWIG_NewPointerObj(comm, SWIG_TypeQuery("_p_gnc_commodity"), 0);
-    def_comm_scm = SWIG_NewPointerObj(gnc_default_currency (),
-                                      SWIG_TypeQuery("_p_gnc_commodity"), 0);
-
-    if (tz)
-        info_scm = scm_cons (scm_from_utf8_string (tz), info_scm);
-    else
-        info_scm = scm_cons (SCM_BOOL_F, info_scm);
-    info_scm = scm_cons (def_comm_scm, info_scm);
-    info_scm = scm_cons (comm_scm, info_scm);
-    info_scm = scm_cons (name ? scm_from_utf8_string (name) : SCM_BOOL_F, info_scm);
-    return info_scm;
-}
diff --git a/bindings/guile/gnc-helpers.h b/bindings/guile/gnc-helpers.h
index 63cf68edf..763037e61 100644
--- a/bindings/guile/gnc-helpers.h
+++ b/bindings/guile/gnc-helpers.h
@@ -31,16 +31,4 @@
 SCM  gnc_printinfo2scm(GNCPrintAmountInfo info);
 GNCPrintAmountInfo gnc_scm2printinfo(SCM info_scm);
 
-/** Given a pointer to a gnc-commodity data structure, build a Scheme
- *  list containing the data needed by the code in price-quotes.scm.
- *  This prevents flipping back and forth from Scheme to C while
- *  extracting values from a pointer.
- *
- * @param com A pointer to the commodity to convert.
- *
- * @return A pointer to a Scheme list, or SCM_EOL on error.
- */
-SCM  gnc_quoteinfo2scm(gnc_commodity *com);
-
-
 #endif
diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index 1dc04e73e..aca814585 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -139,7 +139,7 @@ gnc_prices_dialog_close_cb (GtkDialog *dialog, gpointer data)
 void
 gnc_prices_dialog_help_cb (GtkDialog *dialog, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog{static_cast<PricesDialog*>(data)};
 
     gnc_gnome_help (GTK_WINDOW (pdb_dialog->window), HF_HELP, HL_PRICE_DB);
 }
diff --git a/libgnucash/core-utils/gnc-glib-utils.c b/libgnucash/core-utils/gnc-glib-utils.c
index 4c10c6fae..0dd36b9bc 100644
--- a/libgnucash/core-utils/gnc-glib-utils.c
+++ b/libgnucash/core-utils/gnc-glib-utils.c
@@ -311,22 +311,6 @@ gnc_scm_log_debug(const gchar *msg)
     g_log("gnc.scm", G_LOG_LEVEL_DEBUG, "%s", msg);
 }
 
-void gnc_gpid_kill(GPid pid)
-{
-#ifdef G_OS_WIN32
-    if (!TerminateProcess((HANDLE) pid, 0))
-    {
-        gchar *msg = g_win32_error_message(GetLastError());
-        g_warning("Could not kill child process: %s", msg ? msg : "(null)");
-        g_free(msg);
-    }
-#else /* !G_OS_WIN32 */
-    if (kill(pid, SIGKILL))
-    {
-        g_warning("Could not kill child process: %s", g_strerror(errno));
-    }
-#endif /* G_OS_WIN32 */
-}
 
 gchar *
 gnc_g_list_stringjoin (GList *list_of_strings, const gchar *sep)
diff --git a/libgnucash/core-utils/gnc-glib-utils.h b/libgnucash/core-utils/gnc-glib-utils.h
index c1a9a37b3..dc3046f4c 100644
--- a/libgnucash/core-utils/gnc-glib-utils.h
+++ b/libgnucash/core-utils/gnc-glib-utils.h
@@ -179,10 +179,6 @@ void gnc_scm_log_debug(const gchar *msg);
 
 /** @} */
 
-/** @name glib Miscellaneous Functions
- @{
-*/
-
 
 /**
  * @brief Return a string joining a GList whose elements are gchar*
@@ -198,7 +194,6 @@ void gnc_scm_log_debug(const gchar *msg);
  * caller.
  **/
 gchar * gnc_g_list_stringjoin (GList *list_of_strings, const gchar *sep);
-
 /**
  * @brief Scans the GList elements the minimum number of iterations
  * required to test it against a specified size. Returns -1, 0 or 1
@@ -213,12 +208,6 @@ gchar * gnc_g_list_stringjoin (GList *list_of_strings, const gchar *sep);
  **/
 gint gnc_list_length_cmp (const GList *list, size_t len);
 
-/** Kill a process.  On UNIX send a SIGKILL, on Windows call TerminateProcess.
- *
- *  @param pid The process ID. */
-void gnc_gpid_kill(GPid pid);
-
-/** @} */
 
 #ifdef __cplusplus
 } /* extern "C" */

commit e97fc3e4081aa513efe641cba06e0282c4ba1335
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Tue Mar 16 14:18:01 2021 +0100

    Drop price-quotes.scm, gnc-fq-helper.in and  gnc-fq-check.in - no longer used

diff --git a/bindings/python/example_scripts/priceDB_test.py b/bindings/python/example_scripts/priceDB_test.py
index a1ce64784..1e3cd6b28 100644
--- a/bindings/python/example_scripts/priceDB_test.py
+++ b/bindings/python/example_scripts/priceDB_test.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # Test file for price database stuff
 # To update the price database call
-# $PATH/gnucash  --add-price-quotes $PATHTOFILE
+# $PATH/gnucash-cli --quotes get $PATHTOFILE
 # before running this.
 # Adding to a calling bash script would be better
 # Although calling it from here would be even better!
diff --git a/bindings/python/example_scripts/price_database_example.py b/bindings/python/example_scripts/price_database_example.py
index 2df02b5eb..032f97385 100755
--- a/bindings/python/example_scripts/price_database_example.py
+++ b/bindings/python/example_scripts/price_database_example.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # Another test file for price database stuff
 # To update the price database call
-# $PATH/gnucash  --add-price-quotes $PATHTOFILE
+# $PATH/gnucash-cli --quotes get $PATHTOFILE
 # before running this.
 # Adding to a calling bash script would be better
 # Although calling it from here would be even better!
diff --git a/gnucash/CMakeLists.txt b/gnucash/CMakeLists.txt
index 8e6e339d1..7cd30a83d 100644
--- a/gnucash/CMakeLists.txt
+++ b/gnucash/CMakeLists.txt
@@ -267,15 +267,10 @@ foreach(gres_file ${gresource_files})
 endforeach()
 
 
-gnc_add_scheme_targets(price-quotes
-    SOURCES price-quotes.scm
-    OUTPUT_DIR gnucash
-    DEPENDS "scm-engine;scm-app-utils;scm-gnome-utils")
-
 set_local_dist(gnucash_DIST_local CMakeLists.txt environment.in generate-gnc-script
     gnucash.cpp gnucash-commands.cpp gnucash-cli.cpp gnucash-core-app.cpp
     gnucash-locale-macos.mm gnucash-locale-windows.c gnucash.rc.in gnucash-valgrind.in
-    gnucash-gresources.xml ${gresource_files} price-quotes.scm
+    gnucash-gresources.xml ${gresource_files}
     ${gnucash_noinst_HEADERS} ${gnucash_EXTRA_DIST})
 
 set (gnucash_DIST ${gnucash_DIST_local} ${gnome_DIST} ${gnome_search_DIST} ${gnome_utils_DIST}
diff --git a/gnucash/gnucash.cpp b/gnucash/gnucash.cpp
index 544fdeb4c..fe7513f1a 100644
--- a/gnucash/gnucash.cpp
+++ b/gnucash/gnucash.cpp
@@ -183,7 +183,6 @@ scm_run_gnucash (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **
         auto quote_sources = quotes.sources_as_glist();
         gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
         g_list_free (quote_sources);
-        scm_c_use_module("gnucash price-quotes");
     }
     else
     {
diff --git a/gnucash/price-quotes.scm b/gnucash/price-quotes.scm
deleted file mode 100644
index 511c62981..000000000
--- a/gnucash/price-quotes.scm
+++ /dev/null
@@ -1,504 +0,0 @@
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;; price-quotes.scm - manage sub-processes.
-;;; Copyright 2001 Rob Browning <rlb at cs.utexas.edu>
-;;;
-;;; 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
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(define-module (gnucash price-quotes))
-
-(export gnc:book-add-quotes) ;; called from gnome/dialog-price-edit-db.c
-
-(use-modules (gnucash engine))
-(use-modules (gnucash utilities))
-(use-modules (gnucash core-utils))
-(use-modules (gnucash app-utils))
-(use-modules (gnucash gnome-utils))
-(use-modules (srfi srfi-11)
-             (srfi srfi-1))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; Finance::Quote based instantaneous quotes -- used by the
-;; --add-price-quotes command line option, etc.
-;;
-;; Note From: Dave Peticolas <dave at krondo.com> Date: Sun, 01 Apr 2001
-;; Those aren't pricedb functions, those are online quote functions,
-;; i.e., low-level functions for getting online-quotes and putting
-;; them into the price db.  Reports should not be using those
-;; functions, they should be using the price db. See
-;; src/engine/gnc-pricedb.h
-
-(define gnc:*finance-quote-helper*
-  (string-append (gnc-path-get-bindir) "/gnc-fq-helper"))
-
-(define (gnc:fq-get-quotes requests)
-  ;; requests should be a list where each item is of the form
-  ;;
-  ;; (<fq-method> sym sym ...)
-  ;;
-  ;; i.e. (alphavantage "RHAT" "LNUX" "IBM")
-  ;;
-  ;; for currencies, we have
-  ;;
-  ;; (currency "USD" "AUD") for the conversion from USD to AUD,
-  ;;                        i.e., the price of USD in AUD.
-  ;;
-  ;; This function will return #f on catastrophic failure or a list
-  ;; where, for each element in requests, the output list will contain
-  ;; a quote-result element. This element will be #f or an error
-  ;; symbol if the corresponding method call fails, or a list
-  ;; otherwise. A quote-result list will contain the symbol
-  ;; representing the item being quoted, followed by an alist
-  ;; detailing the quote data from gnc-fq-helper.
-  ;;
-  ;; Possible error symbols and their meanings are:
-  ;;   missing-lib    One of the required perl libs is missing
-  ;;
-  ;; So for the example method call above, the resulting item in the
-  ;; output list might look like this:
-  ;;
-  ;; (("RHAT" (symbol . "RHAT") (gnc:time-no-zone . "...")
-  ;;   (last . 6.59375) (currency . "USD"))
-  ;;  ("LNUX" (symbol . "LNUX") (gnc:time-no-zone . "...")
-  ;;   (last . 3.5) (currency . "USD"))
-  ;;  ("IBM" (symbol . "IBM") (gnc:time-no-zone . "...")
-  ;;   (last . 104.42) (currency . "USD")))
-  ;;
-  ;; Also note that any given value in the alist might be
-  ;; 'failed-conversion if the Finance::Quote result for that field
-  ;; was unparsable.  See the gnc-fq-helper for more details
-  ;; about it's output.
-
-  (let ((quoter #f))
-
-    (define (start-quoter)
-      (set! quoter
-        (gnc-spawn-process-async (list "perl" "-w" gnc:*finance-quote-helper*) #t)))
-
-    (define (get-quotes)
-      (when quoter
-        (map
-         (lambda (request)
-           (catch #t
-             (lambda ()
-               (gnc:debug "handling-request: " request)
-               (and (member (car request) '("currency" "alphavantage" "vanguard"))
-                    (not (getenv "ALPHAVANTAGE_API_KEY"))
-                    (throw 'need-alphavantage-key))
-               ;; we need to display the first element (the method,
-               ;; so it won't be quoted) and then write the rest
-               (with-output-to-port (fdes->outport (gnc-process-get-fd quoter 0))
-                 (lambda ()
-                   (display #\()
-                   (display (car request))
-                   (display " ")
-                   (for-each write (cdr request))
-                   (display #\))
-                   (newline)
-                   (force-output)))
-
-               (let ((results (read (fdes->inport (gnc-process-get-fd quoter 1)))))
-                 (gnc:debug "results: " results)
-                 results))
-             (lambda (key . args) key)))
-         requests)))
-
-    (define (kill-quoter)
-      (when quoter
-        (gnc-detach-process quoter #t)
-        (set! quoter #f)))
-
-    (dynamic-wind start-quoter get-quotes kill-quoter)))
-
-(define (gnc:book-add-quotes window book)
-
-  (define (book->commodity->fq-call-data book)
-    ;; Call helper that walks all of the defined commodities to see if
-    ;; any are marked for quote retrieval.  This function returns a
-    ;; list of info needed for the relevant Finance::Quote calls, and
-    ;; a list of the corresponding commodities.  Also perform a bit of
-    ;; optimization, merging calls for symbols to the same
-    ;; Finance::Quote method.
-    ;;
-    ;; Returns a list of the info needed for a set of calls to
-    ;; gnc-fq-helper.  Each item will of the list will be of the
-    ;; form:
-    ;;
-    ;; (("alphavantage" (commodity-1 currency-1 tz-1)
-    ;;                  (commodity-2 currency-2 tz-2) ...)
-    ;;  ("fidelity_direct" (commodity-3 currency-3 tz-3)
-    ;;                     (commodity-4 currency-4 tz-4) ...)
-    ;;  ("currency" curr-1 curr-2 tz)
-    ;;  ("currency" curr-3 curr-4 tz) ...)
-
-    (let-values (((currency-list commodity-list)
-                  (partition (lambda (a) (string=? (car a) "currency"))
-                             (gnc-commodity-table-get-quotable-commodities
-                              (gnc-commodity-table-get-table book)))))
-
-      (let ((commodity-hash (make-hash-table))
-            (currency-list-filtered
-             (filter
-              (lambda (a)
-                (and (not (gnc-commodity-equiv (cadr a) (caddr a)))
-                     (not (string=? (gnc-commodity-get-mnemonic (cadr a)) "XXX"))))
-              currency-list)))
-
-        ;; Now collect symbols going to the same backend.
-        (for-each
-         (lambda (item)
-           (let ((key (car item))
-                 (val (cdr item)))
-             (hash-set! commodity-hash key
-                        (cons val (hash-ref commodity-hash key '())))))
-         commodity-list)
-
-        ;; Now translate to just what gnc-fq-helper expects.
-        (and (or (pair? currency-list-filtered) (pair? commodity-list))
-             (append
-              (hash-map->list cons commodity-hash)
-              (map (lambda (cmd) (cons (car cmd) (list (cdr cmd))))
-                   currency-list-filtered))))))
-
-  (define (fq-call-data->fq-calls fq-call-data)
-    ;; take an output element from book->commodity->fq-call-data and
-    ;; return a list where the gnc_commodities have been converted to
-    ;; their fq-suitable symbol strings.  i.e. turn the former into
-    ;; the latter:
-    ;;
-    ;; ("alphavantage" (commodity-1 currency-1 tz-1)
-    ;;                 (commodity-2 currency-2 tz-2) ...)
-    ;;
-    ;; ("alphavantage" "IBM" "AMD" ...)
-    ;;
-
-    (if (equal? (car fq-call-data) "currency")
-        (map (lambda (quote-item-info)
-               (list (car fq-call-data)
-                     (gnc-commodity-get-mnemonic (car quote-item-info))
-                     (gnc-commodity-get-mnemonic (cadr quote-item-info))))
-             (cdr fq-call-data))
-        (list
-         (cons (car fq-call-data)
-               (map
-                (lambda (quote-item-info)
-                  (gnc-commodity-get-mnemonic (car quote-item-info)))
-                (cdr fq-call-data))))))
-
-  (define (fq-results->commod-tz-quote-triples fq-call-data fq-results)
-    ;; Change output of gnc:fq-get-quotes to a list of (commod
-    ;; timezone quote) triples using the matching commodity items from
-    ;; fq-call-data.
-    ;;
-    ;; This function presumes that fq-call-data is "correct" -- it
-    ;; contains the correct number of calls, and has the commodity
-    ;; pointers in all the right places.  If not, then the results of
-    ;; this function are undefined.
-    ;;
-    ;; If there's a catatstrophic error, this function might return
-    ;; #f.  If there's an error for any given input element, there
-    ;; will be a pair like this in the output (#f . <commodity>)
-    ;; indicating the commodity for which the quote failed.
-    ;;
-    ;; If this function doesn't return #f, it will return a list with
-    ;; as many elements as there were commodities in the fq-call-data.
-    ;;
-    ;; We might want more sophisticated error handling later, but this
-    ;; will do for now .
-    (let ((result-list '()))
-
-      (define (process-a-quote call-data call-result)
-        ;; data -> (commod-1 currency-1 tz-1)
-        ;; result -> (commod-1-sym . result-alist) or some kind of garbage.
-        (if (and (list? call-result)
-                 (not (null? call-result))
-                 (list? (cdr call-result))
-                 (every
-                  (lambda (alist-item)
-                    (and (pair? alist-item)
-                         (not (eq? 'failed-conversion (cdr alist-item)))))
-                  (cdr call-result)))
-            ;; OK, data is good (as far as we can tell).
-            (set! result-list
-                  (cons (list (car call-data)
-                              (caddr call-data)
-                              (cdr call-result))
-                        result-list))
-            (set! result-list
-                  (cons (cons #f (car call-data))
-                        result-list))))
-
-      (define (process-call-result-pair call-data call-result)
-        (if (and (list? call-result)
-                 (= (length call-data) (+ 1 (length call-result))))
-
-            ;; OK, continue.
-	    (for-each
-	     (lambda (call-data-item call-result-item)
-	       (if (and (list? call-result-item) (list? (car call-result-item)))
-		   (for-each
-		    (lambda (result-subitem)
-		      (gnc:debug "call-data-item: " call-data-item)
-		      (gnc:debug "result-subitem: " result-subitem)
-		      (process-a-quote call-data-item result-subitem))
-		    call-result-item)
-		   (process-a-quote call-data-item call-result-item)))
-	     (cdr call-data) call-result)
-
-            ;; else badly formed result, must assume all garbage.
-            (for-each
-             (lambda (call-item)
-               (set! result-list (cons (cons #f (car call-item)) result-list)))
-             (cdr call-data))))
-
-      (and (list? fq-call-data)
-           (list? fq-results)
-           (= (length fq-call-data) (length fq-results))
-           (begin
-             (for-each process-call-result-pair
-                       fq-call-data
-                       fq-results)
-             (reverse result-list)))))
-
-  (define (timestr->time64 timestr time-zone)
-    ;; time-zone is ignored currently
-    (gnc-parse-time-to-time64 timestr "%Y-%m-%d %H:%M:%S"))
-
-  (define (commodity-tz-quote-triple->price book c-tz-quote-triple)
-    ;; return a string like "NASDAQ:CSCO" on error, or a price on
-    ;; success.  Presume that F::Q currencies are ISO4217 currencies.
-    (let* ((commodity (first c-tz-quote-triple))
-           (time-zone (second c-tz-quote-triple))
-           (quote-data (third c-tz-quote-triple))
-           (gnc-time (assq-ref quote-data 'gnc:time-no-zone))
-           (price #f)
-           (price-type #f)
-           (currency-str (assq-ref quote-data 'currency))
-           (commodity-table (gnc-commodity-table-get-table book))
-           (currency
-            (and commodity-table
-                 (string? currency-str)
-                 (gnc-commodity-table-lookup commodity-table
-                                             "ISO4217"
-                                             (string-upcase currency-str))))
-           (pricedb (gnc-pricedb-get-db book))
-           (saved-price #f)
-           (commodity-str (gnc-commodity-get-printname commodity))
-           )
-      (if (equal? (gnc-commodity-get-printname currency) commodity-str)
-          (let* ((symbol (assq-ref quote-data 'symbol))
-                 (other-curr
-                  (and commodity-table
-                       (string? symbol)
-                       (gnc-commodity-table-lookup commodity-table "ISO4217"
-                                                   (string-upcase symbol)))))
-            (set! commodity other-curr)))
-
-      (let lp ((price-syms '(last nav price))
-               (price-types '("last" "nav" "unknown")))
-        (unless (null? price-syms)
-          (cond
-           ((assq-ref quote-data (car price-syms)) =>
-            (lambda (p)
-              ;; The OpenExchange exchange rate source in Finance::Quote produces
-              ;; some ridiculously precise prices like #e6.95253159056541e-5 which 
-              ;; produce a denominator greater than INT64_MAX.  Use the rationalize
-              ;; function to bring them back to reality.  The precision parameter is
-              ;; chosen empirically to give the best results.
-              (set! price (gnc-scm-to-numeric 
-                            (rationalize p 1/100000000000000)))
-              (set! price-type (car price-types))))
-           (else (lp (cdr price-syms) (cdr price-types))))))
-
-      (if gnc-time
-          (set! gnc-time (timestr->time64 gnc-time time-zone))
-          (set! gnc-time (gnc:get-today)))
-
-      (if (not (and commodity currency gnc-time price price-type))
-          (string-append
-           currency-str ":" (gnc-commodity-get-mnemonic commodity))
-          (begin
-            (set! saved-price (gnc-pricedb-lookup-day-t64 pricedb
-                                                          commodity currency
-                                                          gnc-time))
-            (if (not (null? saved-price))
-                (begin
-                  (if (gnc-commodity-equiv (gnc-price-get-currency saved-price)
-                                           commodity)
-                      (set! price (gnc-numeric-invert price)))
-                  (if (>= (gnc-price-get-source saved-price) PRICE-SOURCE-FQ)
-                      (begin
-                        (gnc-price-begin-edit saved-price)
-                        (gnc-price-set-time64 saved-price gnc-time)
-                        (gnc-price-set-source saved-price PRICE-SOURCE-FQ)
-                        (gnc-price-set-typestr saved-price price-type)
-                        (gnc-price-set-value saved-price price)
-                        (gnc-price-commit-edit saved-price)
-                        #f)
-                      #f))
-                (let ((gnc-price (gnc-price-create book)))
-                  (if (not gnc-price)
-                      (string-append
-                       currency-str ":" (gnc-commodity-get-mnemonic commodity))
-                      (begin
-                        (gnc-price-begin-edit gnc-price)
-                        (gnc-price-set-commodity gnc-price commodity)
-                        (gnc-price-set-currency gnc-price currency)
-                        (gnc-price-set-time64 gnc-price gnc-time)
-                        (gnc-price-set-source gnc-price PRICE-SOURCE-FQ)
-                        (gnc-price-set-typestr gnc-price price-type)
-                        (gnc-price-set-value gnc-price price)
-                        (gnc-price-commit-edit gnc-price)
-                        gnc-price))))
-            ))
-      ))
-
-  (define (book-add-prices! book prices)
-    (let ((pricedb (gnc-pricedb-get-db book)))
-      (for-each
-       (lambda (price)
-         (when price
-           (gnc-pricedb-add-price pricedb price)
-           (gnc-price-unref price)))
-       prices)))
-
-  (define (show-error msg)
-    (gnc:gui-error msg (G_ msg)))
-
-  ;; Add the alphavantage api key to the environment. This value is taken from
-  ;; the Online Quotes preference tab
-  (let ((alphavantage-api-key
-         (gnc-prefs-get-string "general.finance-quote" "alphavantage-api-key")))
-    (gnc:debug "ALPHAVANTAGE_API_KEY=" alphavantage-api-key)
-    (unless (string-null? alphavantage-api-key)
-      (setenv "ALPHAVANTAGE_API_KEY" alphavantage-api-key)))
-
-  (let* ((fq-call-data (book->commodity->fq-call-data book))
-         (fq-calls (and fq-call-data
-                        (append-map fq-call-data->fq-calls fq-call-data)))
-         (fq-results (and fq-calls (gnc:fq-get-quotes fq-calls)))
-         (commod-tz-quote-triples (and fq-results (list? (car fq-results))
-                                       (fq-results->commod-tz-quote-triples
-                                        fq-call-data fq-results)))
-         ;; At this point commod-tz-quote-triples will either be #f or a
-         ;; list of items. Each item will either be (commodity
-         ;; timezone quote-data) or (#f . problem-commodity)
-         (problem-syms (and commod-tz-quote-triples
-                            (filter-map
-                             (lambda (cq-pair)
-                               (and (not (car cq-pair))
-                                    (string-append
-                                     (gnc-commodity-get-namespace (cdr cq-pair))
-                                     ":"
-                                     (gnc-commodity-get-mnemonic (cdr cq-pair)))))
-                             commod-tz-quote-triples)))
-         ;; strip out the "bad" ones from above.
-         (ok-syms (and commod-tz-quote-triples (filter car commod-tz-quote-triples)))
-         (keep-going? #t))
-
-    (cond
-     ((not fq-call-data)
-      (set! keep-going? #f)
-      (show-error (N_ "No commodities marked for quote retrieval.")))
-
-     ((not fq-results)
-      (set! keep-going? #f)
-      (show-error (N_ "Unable to get quotes or diagnose the problem.")))
-
-     ((memq 'missing-lib fq-results)
-      (set! keep-going? #f)
-      (show-error (N_ "You are missing some needed Perl libraries.
-Run 'gnc-fq-update' as root to install them.")))
-
-     ((memq 'need-alphavantage-key fq-results)
-      (set! keep-going? #f)
-      (show-error (format #f (G_ "ERROR: ALPHAVANTAGE_API_KEY must be set for currency and quotes; see ~A")
-                          "https://wiki.gnucash.org/wiki/Online_Quotes#Source_Alphavantage.2C_US")))
-
-     ((memq 'system-error fq-results)
-      (set! keep-going? #f)
-      (show-error (N_ "There was a system error while retrieving the price quotes.")))
-
-     ((not (list? (car fq-results)))
-      (set! keep-going? #f)
-      (show-error (N_ "There was an unknown error while retrieving the price quotes.")))
-
-     ((not commod-tz-quote-triples)
-      (set! keep-going? #f)
-      (show-error (N_ "Unable to get quotes or diagnose the problem.")))
-
-     ((pair? problem-syms)
-      (cond
-       ((not (gnucash-ui-is-running))
-        (gnc:warn
-         (with-output-to-string
-           (lambda ()
-             (display "Unable to retrieve quotes for these items:\n")
-             (display (string-join problem-syms "\n  "))
-             (newline)
-             (display "Continuing with good quotes.")
-             (newline)))))
-
-       ((and ok-syms (not (null? ok-syms)))
-        (set! keep-going?
-          (gnc-verify-dialog
-           window #t (with-output-to-string
-                       (lambda ()
-                         (display (G_ "Unable to retrieve quotes for these items:"))
-                         (display "\n  ")
-                         (display (string-join problem-syms "\n  "))
-                         (newline)
-                         (display (G_ "Continue using only the good quotes?")))))))
-
-       (else
-        (set! keep-going? #f)
-        (gnc-error-dialog
-         window (with-output-to-string
-                  (lambda ()
-                    (display (G_ "Unable to retrieve quotes for these items:"))
-                    (display "\n  ")
-                    (display (string-join problem-syms "\n  ")))))))))
-
-    (when keep-going?
-      (let ((prices (map (lambda (triple)
-                           (commodity-tz-quote-triple->price book triple))
-                         ok-syms)))
-        (when (any string? prices)
-          (if (gnucash-ui-is-running)
-              (set! keep-going?
-                (gnc-verify-dialog
-                 window #t
-                 (with-output-to-string
-                   (lambda ()
-                     (display (G_ "Unable to create prices for these items:"))
-                     (display "\n  ")
-                     (display (string-join (filter string? prices) "\n  "))
-                     (newline)
-                     (display (G_ "Add remaining good quotes?"))))))
-              (gnc:warn
-               (with-output-to-string
-                 (lambda ()
-                   (display "Unable to create prices for these items:\n  ")
-                   (display (string-join (filter string? prices) "\n  "))
-                   (newline)
-                   (display "Adding remaining good quotes.")
-                   (newline))))))
-
-        (when keep-going?
-          (book-add-prices! book (filter (negate string?) prices)))))))
diff --git a/libgnucash/engine/gnc-pricedb.c b/libgnucash/engine/gnc-pricedb.c
index 777e9f8a4..13819ce81 100644
--- a/libgnucash/engine/gnc-pricedb.c
+++ b/libgnucash/engine/gnc-pricedb.c
@@ -118,7 +118,6 @@ static const char* source_names[(size_t)PRICE_SOURCE_INVALID + 1] =
 {
     /* sync with price_to_gui in dialog-price-editor.c */
     "user:price-editor",
-    /* sync with commidity-tz-quote->price in price-quotes.scm */
     "Finance::Quote",
     "user:price",
     /* String retained for backwards compatibility. */
diff --git a/libgnucash/quotes/CMakeLists.txt b/libgnucash/quotes/CMakeLists.txt
index fcc256f24..6f5174b14 100644
--- a/libgnucash/quotes/CMakeLists.txt
+++ b/libgnucash/quotes/CMakeLists.txt
@@ -1,6 +1,6 @@
 
 set(_BIN_FILES "")
-foreach(file gnc-fq-check.in gnc-fq-helper.in gnc-fq-update.in gnc-fq-dump.in finance-quote-wrapper.in)
+foreach(file gnc-fq-update.in gnc-fq-dump.in finance-quote-wrapper.in)
   string(REPLACE ".in" "" _OUTPUT_FILE_NAME ${file})
   set(_ABS_OUTPUT_FILE ${BINDIR_BUILD}/${_OUTPUT_FILE_NAME})
   configure_file( ${file} ${_ABS_OUTPUT_FILE} @ONLY)
@@ -9,7 +9,7 @@ endforeach(file)
 
 
 set(_MAN_FILES "")
-foreach(file gnc-fq-dump gnc-fq-helper)
+foreach(file gnc-fq-dump finance-quote-wrapper)
   set(_POD_INPUT ${BINDIR_BUILD}/${file})
   set(_MAN_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${file}.1)
   list(APPEND _MAN_FILES ${_MAN_OUTPUT})
@@ -26,4 +26,4 @@ add_custom_target(quotes-bin ALL DEPENDS ${_BIN_FILES})
 install(FILES ${_MAN_FILES} DESTINATION  ${CMAKE_INSTALL_MANDIR}/man1)
 install(PROGRAMS ${_BIN_FILES} DESTINATION ${CMAKE_INSTALL_BINDIR})
 
-set_dist_list(quotes_DIST CMakeLists.txt gnc-fq-check.in gnc-fq-dump.in gnc-fq-helper.in gnc-fq-update.in  finance-quote-wrapper.in Quote_example.pl README)
+set_dist_list(quotes_DIST CMakeLists.txt gnc-fq-dump.in gnc-fq-update.in finance-quote-wrapper.in Quote_example.pl README)
diff --git a/libgnucash/quotes/README b/libgnucash/quotes/README
index 44c8fcc11..3f594fb59 100644
--- a/libgnucash/quotes/README
+++ b/libgnucash/quotes/README
@@ -2,11 +2,11 @@
 
 This directory contains assorted stock quote scripts.
 
-gnc-fq-check.in:
+finance-quote-wrapper.in:
 
-  Source file for gnc-fq-check which is a perl script that allows
-  gnucash to determine if Finance::Quote is installed properly.  The
-  responses is a scheme form.
+  Source file for finance-quote-wrapper which is a perl script that
+  allows gnucash to communicate with Finance::Quote.
+  The requests and responses are in json format.
 
 gnc-fq-dump.in:
 
@@ -14,12 +14,6 @@ gnc-fq-dump.in:
   a quote from Finance::Quote and dumps the response to the terminal.
   Its useful for determining problems with F::Q.
 
-gnc-fq-helper.in:
-
-  Source file for gnc-fq-helper which is a perl script that
-  allows gnucash to communicate with Finance::Quote over pipes from
-  guile.  The requests and responses are scheme forms.
-
 gnc-fq-update.in:
 
   Source file for gnc-fq-update which is a perl script that updates
diff --git a/libgnucash/quotes/gnc-fq-check.in b/libgnucash/quotes/gnc-fq-check.in
deleted file mode 100755
index 8eb768458..000000000
--- a/libgnucash/quotes/gnc-fq-check.in
+++ /dev/null
@@ -1,103 +0,0 @@
-#!@PERL@ -w
-######################################################################
-### gnc-fq-check - check for the presence of  Finance::Quote
-### From gnc-fq-helper.
-### Copyright 2001 Rob Browning <rlb at cs.utexas.edu>
-### 
-### 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
-######################################################################
-
-use strict;
-use English;
-use FileHandle;
-
-=head1 NAME
-
-gnc-fq-check  -  check for the presence of Finance::Quote
-                 From gnc-fq-helper
-
-=head1 SYNOPSIS
-
-gnc-fq-check
-
-=head1 DESCRIPTION
-
-Input: <none>
-
-Output (on standard output, one output form per input line):
-
-A list of quote sources supported by Finance::Quote, or the single
-term missing-lib if finance quote could not be executed.
-
-Exit status
-
-0 - success
-non-zero - failure
-
-=cut
-
-sub check_modules {
-  my @modules = qw(Finance::Quote);
-  my @missing;
-
-  foreach my $mod (@modules) {
-    if (eval "require $mod") {
-      $mod->import();
-    }
-    else {
-      push (@missing, $mod);
-    }
-  }
-
-  return unless @missing;
-
-  print STDERR "\n";
-  print STDERR "You need to install the following Perl modules:\n";
-  foreach my $mod (@missing) {
-    print STDERR "  ".$mod."\n";
-  }
-
-  print STDERR "\n";
-  print STDERR "Use your system's package manager to install them,\n";
-  print STDERR "or run 'gnc-fq-update' as root.\n";
-
-  print "missing-lib\n";
-
-  exit 1;
-}
-
-#---------------------------------------------------------------------------
-# Runtime.
-
-# Check for and load non-standard modules
-check_modules ();
-
-# Create a stockquote object.
-my $quoter = Finance::Quote->new();
-my $prgnam = "gnc-fq-check";
-
-print "$Finance::Quote::VERSION\n";
-my @qsources;
-my @sources = $quoter->sources();
-foreach my $source (@sources) {
-  print "$source\n";
-}
-
-## Local Variables:
-## mode: perl
-## End:
diff --git a/libgnucash/quotes/gnc-fq-helper.in b/libgnucash/quotes/gnc-fq-helper.in
deleted file mode 100755
index ab9f496b3..000000000
--- a/libgnucash/quotes/gnc-fq-helper.in
+++ /dev/null
@@ -1,435 +0,0 @@
-#!@PERL@ -w
-######################################################################
-### gnc-fq-helper - present a scheme interface to Finance::Quote
-### Copyright 2001 Rob Browning <rlb at cs.utexas.edu>
-###
-### 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
-######################################################################
-
-use strict;
-use English;
-
-=head1 NAME
-
-gnc-fq-helper  -  allows gnucash to communicate with Finance::Quote
-                  over pipes from guile. The requests and responses
-                  are scheme forms.
-
-=head1 SYNOPSIS
-
-gnc-fq-helper
-
-=head1 DESCRIPTION
-
-Input: (on standard input - one entry per line and one line per
-entry, and double quotes must only be delimiters, not string
-content -- remember, we don't have a real scheme parser on the perl
-side :>).
-
-(<method-name> symbol symbol symbol ...)
-
-where <method-name> indicates the desired Finance::Quote method.
-One can list the many methods by running gnc-fq-check.
-
-For currency quotes, the symbols alternate between the 'from'
-and 'to' currencies.
-
-For example:
-
-(alphavantage "IBM" "LNUX")
-(fidelity_direct "FBIOX" "FSELX")
-(currency "USD" "AUD")
-
-Output (on standard output, one output form per input line):
-
-Schemified version of gnc-fq's output, basically an alist of
-alists, as in the example below.  Right now, only the fields that
-this script knows about (and knows how to convert to scheme) are
-returned, so the conversion function will have to be updated
-whenever Finance::Quote changes.  Currently you'll get symbol,
-gnc:time-no-zone, and currency, and either last, nav, or price.
-Fields with gnc: prefixes are non-Finance::Quote fields.
-gnc:time-no-zone is returned as a string of the form "YYYY-MM-DD
-HH:MM:SS", basically the unmolested (and underspecified) output of
-the quote source.  It's up to you to know what it's proper timezone
-really is.  i.e. if you know the time was in America/Chicago, you'll
-need to convert it to that.
-
-For example:
-
- $ echo '(alphavantage "CSCO" "JDSU" "^IXIC")' | ./gnc-fq-helper
-(("CSCO" (symbol . "CSCO")
-         (gnc:time-no-zone . "2001-03-13 19:27:00")
-         (last . 20.375)
-         (currency . "USD"))
- ("JDSU" (symbol . "JDSU")
-         (gnc:time-no-zone . "2001-03-13 19:27:00")
-         (last . 23.5625)
-         (currency . "USD"))
-("^IXIC" (symbol . ^IXIC)
-         (gnc:time-no-zone . 2002-12-04 17:16:00)
-         (last . 1430.35)
-         (currency . failed-conversion)))
-
-On error, the overall result may be #f, or on individual errors, the
-list sub-item for a given symbol may be #f, like this:
-
- $ echo '(alphavantage "CSCO" "JDSU")' | ./gnc-fq-helper
-(#f
- ("JDSU" (symbol . "JDSU")
-         (gnc:time-no-zone . "2001-03-13 19:27:00")
-         (last . 23.5625)
-         (currency . "USD")))
-
-further, errors may be stored with each quote as indicated in
-Finance::Quote, and whenever the conversion to scheme data fails,
-the field will have the value 'failed-conversion, and accordingly
-this symbol will never be a legitimate conversion.
-
-Exit status
-
-0 - success
-non-zero - failure
-
-=cut
-
-# The methods we know about.  For now we assume they all have the same
-# signature so this works OK.
-
-sub check_modules {
-
-# Date::Manip provides ParseDate, ParseDateString, and UnixTime.
-
-  my @modules = qw(FileHandle Finance::Quote Date::Manip);
-  my @missing;
-
-  foreach my $mod (@modules) {
-    if (eval "require $mod") {
-      $mod->import();
-    }
-    else {
-      push (@missing, $mod);
-    }
-  }
-
-  return unless @missing;
-
-  print STDERR "\n";
-  print STDERR "You need to install the following Perl modules:\n";
-  foreach my $mod (@missing) {
-    print STDERR "  ".$mod."\n";
-  }
-
-  print STDERR "\n";
-  print STDERR "Use your system's package manager to install them,\n";
-  print STDERR "or run 'gnc-fq-update' as root.\n";
-
-  print "missing-lib";
-
-  exit 1;
-}
-
-# Check for and load non-standard modules
-check_modules ();
-
-# Set a base date with the current time in the current TZ:
-my $base_date = new Date::Manip::Date;
-$base_date->parse("now");
-
-sub schemify_string {
-  my($str) = @_;
-
-  if(!$str) { return "failed-conversion"; }
-
-  # FIXME: Is this safe?  Can we just double all backslashes and backslash
-  # escape all double quotes and get the right answer?
-
-  # double all backslashes.
-  my $bs = "\\";
-  $str =~ s/$bs$bs/$bs$bs/gmo;
-
-  # escape all double quotes.
-  # Have to do this because the perl-mode parser freaks out otherwise.
-  my $dq = '"';
-  $str =~ s/$dq/$bs$dq/gmo;
-  return '"' . $str . '"';
-}
-
-sub schemify_boolean {
-  my($bool) = @_;
-
-  if($bool) {
-    return "#t";
-  } else {
-    return "#f";
-  }
-}
-
-sub schemify_num {
-  my($numstr) = @_;
-  # This is for normal numbers, not the funny ones like "2.346B".
-  # For now we don't need to do anything.
-
-  if(!$numstr) { return "failed-conversion"; }
-
-  if($numstr =~ /^\s*(\d+(\.\d+)?([eE][+-]?\d+)?)$/o) {
-    return "#e" . $1;
-  } else {
-    return "failed-conversion";
-  }
-}
-
-# sub schemify_range {
-#   #convert range in form ``num1 - num2'' to ``(num1 num2)''.
-# }
-
-sub get_quote_time {
-  # return the date.
-  my ($item, $quotehash) = @_;
-
-  my $datestr = $$quotehash{$item, 'date'};
-  my $timestr = $$quotehash{$item, 'time'};
-  my $format = "%Y-%m-%d %H:%M:%S";
-  my $result;
-
-  if ($datestr) {
-      my $parsestr = $datestr . " " . ($timestr ? $timestr : "12:00:00");
-      my $date = $base_date->new();
-      my $err = $date->parse($parsestr);
-      if ($err) {
-          print $date->err(), " $parsestr\n";
-          $result = $base_date->printf($format);
-      }
-      else {
-          $result = $date->printf($format);
-      }
-  } else {
-      $result = $base_date->printf($format);
-  }
-  return("\"$result\"");
-}
-
-sub schemify_quote {
-  my($itemname, $quotehash, $indentlevel) = @_;
-  my $scmname = schemify_string($itemname);
-  my $quotedata = "";
-  my $field;
-  my $data;
-
-  if (!$$quotehash{$itemname, "success"}) {
-    return schemify_boolean(0);
-  }
-
-  $field = 'symbol';
-  if (($$quotehash{$itemname, $field})) {
-    $data = schemify_string($$quotehash{$itemname, $field});
-  } else {
-    # VWD and a few others don't set the symbol field
-    $data = schemify_string($itemname);
-  }
-  $quotedata .= "($field . $data)";
-
-  $field = 'gnc:time-no-zone';
-  $data = get_quote_time($itemname, $quotehash);
-  $quotedata .= " ($field . $data)" if $data;
-
-  $field = 'last';
-  if (!($$quotehash{$itemname, $field})) {
-    $field = 'nav';
-  }
-  if (!($$quotehash{$itemname, $field})) {
-    $field = 'price';
-  }
-
-  $data = schemify_num($$quotehash{$itemname, $field});
-  $quotedata .= " ($field . $data)";
-
-  $field = 'currency';
-  $data = schemify_string($$quotehash{$itemname, $field});
-  $quotedata .= " ($field . $data)";
-
-  return "($scmname $quotedata)";
-}
-
-sub schemify_quotes {
-  my($symbols, $quotehash) = @_;
-  my $resultstr = "";
-  my $sym;
-  my $separator = "";
-
-  # we have to pass in @$items because Finance::Quote just uses the
-  # mangled "$name$field string as the key, so there's no way (I know
-  # of) to find out which stocks are in a given quotehash, just given
-  # the quotehash.
-
-  foreach $sym (@$symbols) {
-    $resultstr .= $separator . schemify_quote($sym, $quotehash, 2);
-    if(!$separator) { $separator = "\n "; }
-  }
-  return "($resultstr)\n";
-}
-
-sub parse_input_line {
-
-  # FIXME: we need to rewrite parsing to handle commands modularly.
-  # Right now all we do is hard-code "fetch".
-
-  my($input) = @_;
-  # Have to do this because the perl-mode parser freaks out otherwise.
-  my $dq = '"';
-  my @symbols;
-
-  # Make sure we have an opening ( preceded only by whitespace.
-  # and followed by a one word method name composed of [a-z_]+.
-  # Also allow the '.' and '^' characters for stock indices.
-  # Kill off the whitespace if we do and grab the command.
-  if($input !~ s/^\s*\(\s*([\.\^a-z_]+)\s+//o) { return 0; }
-
-  my $quote_method_name = $1;
-
-  # Make sure we have an ending ) followed only by whitespace
-  # and kill it off if we do...
-  if($input !~ s/\s*\)\s*$//o) { return 0; }
-
-  while($input) {
-    # Items should look like "RHAT"
-    # Grab RHAT and delete "RHAT"\s*
-    if($input !~ s/^$dq([^$dq]+)$dq\s*//o) { return 0; }
-    my $symbol = $1;
-    push @symbols, $symbol;
-  }
-
-  my @result = ($quote_method_name, \@symbols);
-  return \@result;
-}
-
-#---------------------------------------------------------------------------
-# Runtime.
-
-# Create a stockquote object.
-my $quoter = Finance::Quote->new();
-my $prgnam = "gnc-fq-helper";
-
-# Disable default currency conversions.
-$quoter->set_currency();
-
-while(<>) {
-
-  my $result = parse_input_line($_);
-
-  if(!$result) {
-    print STDERR "$prgnam: bad input line ($_)\n";
-    exit 1;
-  }
-
-  my($quote_method_name, $symbols) = @$result;
-  my %quote_data;
-
-  if($quote_method_name =~ m/^currency$/) {
-    my ($from_currency, $to_currency) = @$symbols;
-
-    last unless $from_currency;
-    last unless $to_currency;
-
-    my $price = $quoter->currency($from_currency, $to_currency);
-    my $inv_price = undef;
-    # Sometimes price quotes are available in only one direction.
-    unless (defined($price)) {
-        $inv_price = $quoter->currency($to_currency, $from_currency);
-        if (defined($inv_price)) {
-            my $tmp = $to_currency;
-            $to_currency = $from_currency;
-            $from_currency = $tmp;
-            $price = $inv_price;
-        }
-    }
-
-    $quote_data{$from_currency, "success"} = defined($price);
-    $quote_data{$from_currency, "symbol"} = $from_currency;
-    $quote_data{$from_currency, "currency"} = $to_currency;
-    $quote_data{$from_currency, "last"} = $price;
-
-    my @new_symbols = ($from_currency);
-    $symbols = \@new_symbols;
-  } else {
-    %quote_data = $quoter->fetch($quote_method_name, @$symbols);
-  }
-
-  if (%quote_data) {
-    print schemify_quotes($symbols, \%quote_data);
-  } else {
-    print "#f\n";
-  }
-
-  STDOUT->flush();
-}
-
-exit 0;
-
-__END__
-
-# Keep this around in case we need to go back to complex per-symbol args.
-#
-#    while($input) {
-#      # Items should look like "RHAT" "EST")
-#      # Grab RHAT and delete ("RHAT"\s*
-#      if($input !~ s/^\(\s*$dq([^$dq]+)$dq\s*//o) { return 0; }
-#      my $symbol = $1;
-#      my $timezone;
-#      # Now grab EST or #f and delete \s*"EST") or #f)
-#      if($input =~ s/^\s*$dq([^$dq]+)$dq\)\s*//o) {
-#        $timezone = $1;
-#      } else {
-#        if($input =~ s/^\s*(\#f)\)\s*//o) {
-#          $timezone = 0;
-#        } else {
-#          return 0;
-#        }
-#      }
-
-#  sub get_quote_utc {
-#    # return the date in utc epoch seconds, using $timezone if specified.
-#    my ($item, $timezone, $quotehash) = @_;
-
-#    if(!defined($timezone)) { return "failed-conversion"; }
-
-#    my $datestr = $$quotehash{$item, 'date'};
-#    my $timestr = $$quotehash{$item, 'time'};
-
-#    if(!$datestr) {
-#      return "failed-conversion";
-#    }
-#    my $parsestr = $datestr;
-#    if($timestr) {
-#      $parsestr .= " $timestr";
-#    }
-
-#    if($timezone) {
-#      # Perform a conversion.
-#      $parsestr = Date_ConvTZ(ParseDate($parsestr), $timezone, 'UTC');
-#    }
-#    my $result = UnixDate($parsestr, "%s");
-#    if($result !~ /^(\+|-)?\d+$/) {
-#      $result = "failed-conversion";
-#    }
-#    return $result;
-#  }
-
-## Local Variables:
-## mode: perl
-## End:
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 0738f90ef..a7c6e5da0 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -374,7 +374,6 @@ gnucash/import-export/qif-imp/qif-parse.scm
 gnucash/import-export/qif-imp/qif-to-gnc.scm
 gnucash/import-export/qif-imp/qif-utils.scm
 gnucash/import-export/qif-imp/string.scm
-gnucash/price-quotes.scm
 gnucash/python/gncmod-python.c
 gnucash/python/init.py
 gnucash/python/pycons/console.py

commit bf357315fd5c4757dc36c7878c48713eb8474989
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Tue Mar 16 16:42:01 2021 +0100

    finance-quote-wrapper - implement check and fetch in one file via command line switches
    
    This obsoletes gnc-fq-check as the same function can now be
    performed with 'finance-quote-wrapper -v'

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 28cddccd7..42bbbacbb 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -119,8 +119,8 @@ GncQuotesImpl::check (QofBook *book)
     m_dflt_curr = gnc_default_currency();
 
     auto perl_executable = bp::search_path("perl");
-    auto fq_check = std::string(gnc_path_get_bindir()) + "/gnc-fq-check";
-    StrVec args { "-w", fq_check };
+    auto fq_wrapper = std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper";
+    StrVec args { "-w", fq_wrapper, "-v" };
 
     auto cmd_out = run_cmd (perl_executable.string(), args, StrVec());
 
@@ -180,7 +180,7 @@ GncQuotesImpl::fetch (const CommVec& commodities)
 
     auto perl_executable = bp::search_path("perl");
     auto fq_wrapper = std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper";
-    StrVec args { "-w", fq_wrapper };
+    StrVec args { "-w", fq_wrapper, "-f" };
 
     auto cmd_out = run_cmd (perl_executable.string(), args, result.str());
 
diff --git a/libgnucash/quotes/finance-quote-wrapper.in b/libgnucash/quotes/finance-quote-wrapper.in
index 5089dec74..4f05516ad 100755
--- a/libgnucash/quotes/finance-quote-wrapper.in
+++ b/libgnucash/quotes/finance-quote-wrapper.in
@@ -72,7 +72,7 @@ non-zero - failure
 =cut
 
 sub check_modules {
-  my @modules = qw(Finance::Quote JSON::Parse);
+  my @modules = qw(Finance::Quote JSON::Parse Getopt::Std);
   my @missing;
 
   foreach my $mod (@modules) {
@@ -101,6 +101,26 @@ sub check_modules {
   exit 1;
 }
 
+sub print_version  {
+    my $quoter = Finance::Quote->new();
+    my @sources = $quoter->sources();
+    print "$Finance::Quote::VERSION\n";
+    foreach my $source (@sources) {
+        print "$source\n";
+    }
+    exit 0;
+}
+
+sub print_usage {
+    print STDERR
+"Usage:
+  Check proper installation and version:
+    finance-quote-wrapper -v
+  Fetch quotes (input should be passed as JSON via stdin):
+    finance-quote-wrapper -f
+";
+}
+
 sub sanitize_hash {
 
     my (%quotehash) = @_;
@@ -160,6 +180,30 @@ sub parse_commodities {
 
 # Check for and load non-standard modules
 check_modules ();
+
+my %opts;
+my $status = getopts('hvf', \%opts);
+if (!$status)
+{
+    print_usage();
+    exit 1;
+}
+
+if (exists $opts{'v'})
+{
+    print_version();
+}
+elsif (exists $opts{'h'})
+{
+    print_usage();
+    exit 0;
+}
+elsif (!exists $opts{'f'})
+{
+    print_usage();
+    exit 1;
+}
+
 JSON::Parse->import(qw(valid_json parse_json));
 
 my $json_input = do { local $/; <STDIN> };

commit 8c08fedaa1dc9f319e99ed70166b5d7ac2df3a18
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Tue Mar 16 13:54:01 2021 +0100

    Use GncQuotes in price db window

diff --git a/gnucash/gnome/CMakeLists.txt b/gnucash/gnome/CMakeLists.txt
index 37d13f8f6..1e88ad68a 100644
--- a/gnucash/gnome/CMakeLists.txt
+++ b/gnucash/gnome/CMakeLists.txt
@@ -133,6 +133,7 @@ target_link_libraries(gnc-gnome
     gnc-register-gnome
     gnc-register-core
     gnc-gnome-utils
+    gnc-app-utils
     gnc-engine
     gnc-expressions
     gnc-html
@@ -148,6 +149,7 @@ target_compile_options(gnc-gnome PRIVATE -Wno-deprecated-declarations)
 target_include_directories(gnc-gnome
   PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
   PRIVATE
+    ${CMAKE_SOURCE_DIR}/libgnucash/app-utils
     ${CMAKE_SOURCE_DIR}/libgnucash/app-utils/calculation
     ${CMAKE_SOURCE_DIR}/gnucash/html
     ${CMAKE_BINARY_DIR}/gnucash/gnome-utils # for gnc-warnings.h
diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index d1e86d8b8..1dc04e73e 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -27,8 +27,8 @@
 
 #include <gtk/gtk.h>
 #include <glib/gi18n.h>
-#include <libguile.h>
 #include <time.h>
+#include <gnc-quotes.hpp>
 
 extern "C" {
 #include "dialog-utils.h"
@@ -47,9 +47,6 @@ extern "C" {
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
 #include "gnc-warnings.h"
-#include "swig-runtime.h"
-#include "guile-mappings.h"
-#include "gnc-engine-guile.h"
 #include <gnc-glib-utils.h>
 }
 
@@ -560,30 +557,19 @@ void
 gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data)
 {
     auto pdb_dialog = static_cast<PricesDialog *> (data);
-    SCM quotes_func;
-    SCM book_scm;
-    SCM scm_window;
 
     ENTER(" ");
-    quotes_func = scm_c_eval_string ("gnc:book-add-quotes");
-    if (!scm_is_procedure (quotes_func))
+    GncQuotes quotes (pdb_dialog->book);
+    if (quotes.cmd_result() != 0)
     {
-        LEAVE(" no procedure");
+        if (!quotes.error_msg().empty())
+            PWARN ("%s", quotes.error_msg().c_str());
+        LEAVE("quote retrieval failed");
         return;
     }
 
-    book_scm = gnc_book_to_scm (pdb_dialog->book);
-    if (scm_is_true (scm_not (book_scm)))
-    {
-        LEAVE("no book");
-        return;
-    }
-
-    scm_window =  SWIG_NewPointerObj(pdb_dialog->window,
-                                     SWIG_TypeQuery("_p_GtkWindow"), 0);
-
     gnc_set_busy_cursor (NULL, TRUE);
-    scm_call_2 (quotes_func, scm_window, book_scm);
+    quotes.fetch_all();
     gnc_unset_busy_cursor (NULL);
 
     /* Without this, the summary bar on the accounts tab

commit 8896d61c7aa505857286c81a24ad37260ddf26eb
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Tue Mar 16 13:44:56 2021 +0100

    Build dialog-price-edit-db as C++

diff --git a/gnucash/gnome/CMakeLists.txt b/gnucash/gnome/CMakeLists.txt
index 825f55827..37d13f8f6 100644
--- a/gnucash/gnome/CMakeLists.txt
+++ b/gnucash/gnome/CMakeLists.txt
@@ -90,7 +90,7 @@ set (gnc_gnome_SOURCES
   dialog-order.c
   dialog-payment.c
   dialog-price-editor.c
-  dialog-price-edit-db.c
+  dialog-price-edit-db.cpp
   dialog-print-check.c
   dialog-progress.c
   dialog-report-column-view.cpp
diff --git a/gnucash/gnome/dialog-price-edit-db.c b/gnucash/gnome/dialog-price-edit-db.cpp
similarity index 80%
rename from gnucash/gnome/dialog-price-edit-db.c
rename to gnucash/gnome/dialog-price-edit-db.cpp
index 721446fc6..d1e86d8b8 100644
--- a/gnucash/gnome/dialog-price-edit-db.c
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -30,6 +30,7 @@
 #include <libguile.h>
 #include <time.h>
 
+extern "C" {
 #include "dialog-utils.h"
 #include "gnc-accounting-period.h"
 #include "gnc-amount-edit.h"
@@ -50,6 +51,7 @@
 #include "guile-mappings.h"
 #include "gnc-engine-guile.h"
 #include <gnc-glib-utils.h>
+}
 
 
 #define DIALOG_PRICE_DB_CM_CLASS "dialog-price-edit-db"
@@ -60,6 +62,7 @@
 static QofLogModule log_module = GNC_MOD_GUI;
 
 
+extern "C" {
 void gnc_prices_dialog_destroy_cb (GtkWidget *object, gpointer data);
 void gnc_prices_dialog_close_cb (GtkDialog *dialog, gpointer data);
 void gnc_prices_dialog_help_cb (GtkDialog *dialog, gpointer data);
@@ -71,6 +74,7 @@ void gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data);
 static gboolean gnc_prices_dialog_key_press_cb (GtkWidget *widget,
                                                 GdkEventKey *event,
                                                 gpointer data);
+}
 
 
 typedef struct
@@ -88,14 +92,14 @@ typedef struct
 
     GtkWidget *remove_dialog;
     GtkTreeView *remove_view;
-    gint remove_source;
+    int remove_source;
 } PricesDialog;
 
 
 void
 gnc_prices_dialog_destroy_cb (GtkWidget *object, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
     gnc_unregister_gui_component_by_data (DIALOG_PRICE_DB_CM_CLASS, pdb_dialog);
@@ -116,7 +120,7 @@ gnc_prices_dialog_delete_event_cb (GtkWidget *widget,
                                    GdkEvent  *event,
                                    gpointer   data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
     // this cb allows the window size to be saved on closing with the X
     gnc_save_window_size (GNC_PREFS_GROUP,
                           GTK_WINDOW(pdb_dialog->window));
@@ -127,7 +131,7 @@ gnc_prices_dialog_delete_event_cb (GtkWidget *widget,
 void
 gnc_prices_dialog_close_cb (GtkDialog *dialog, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
     gnc_close_gui_component_by_data (DIALOG_PRICE_DB_CM_CLASS, pdb_dialog);
@@ -147,11 +151,10 @@ gnc_prices_dialog_help_cb (GtkDialog *dialog, gpointer data)
 void
 gnc_prices_dialog_edit_clicked (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
-    GList *price_list;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
-    price_list = gnc_tree_view_price_get_selected_prices(pdb_dialog->price_tree);
+    auto price_list = gnc_tree_view_price_get_selected_prices (pdb_dialog->price_tree);
     if (!price_list)
     {
         LEAVE("no price selected");
@@ -164,9 +167,10 @@ gnc_prices_dialog_edit_clicked (GtkWidget *widget, gpointer data)
         return;
     }
 
+    auto price = static_cast<GNCPrice *> (price_list->data);
     gnc_price_edit_dialog (pdb_dialog->window, pdb_dialog->session,
-                           price_list->data, GNC_PRICE_EDIT);
-    g_list_free(price_list);
+                           price, GNC_PRICE_EDIT);
+    g_list_free (price_list);
     LEAVE(" ");
 }
 
@@ -181,20 +185,18 @@ remove_helper(GNCPrice *price, GNCPriceDB *pdb)
 void
 gnc_prices_dialog_remove_clicked (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
-    GList *price_list;
-    gint length, response;
-    GtkWidget *dialog;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
-    price_list = gnc_tree_view_price_get_selected_prices(pdb_dialog->price_tree);
+    auto price_list = gnc_tree_view_price_get_selected_prices (pdb_dialog->price_tree);
     if (!price_list)
     {
         LEAVE("no price selected");
         return;
     }
 
-    length = g_list_length(price_list);
+    gint response;
+    auto length = g_list_length(price_list);
     if (length > 0)
     {
         gchar *message;
@@ -205,11 +207,11 @@ gnc_prices_dialog_remove_clicked (GtkWidget *widget, gpointer data)
                                "Are you sure you want to delete the %d selected prices?",
                                length),
                       length);
-        dialog = gtk_message_dialog_new(GTK_WINDOW(pdb_dialog->window),
-                                        GTK_DIALOG_DESTROY_WITH_PARENT,
-                                        GTK_MESSAGE_QUESTION,
-                                        GTK_BUTTONS_NONE,
-                                        "%s", _("Delete prices?"));
+        auto dialog = gtk_message_dialog_new (GTK_WINDOW(pdb_dialog->window),
+                                              GTK_DIALOG_DESTROY_WITH_PARENT,
+                                              GTK_MESSAGE_QUESTION,
+                                              GTK_BUTTONS_NONE,
+                                              "%s", _("Delete prices?"));
         gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
                 "%s", message);
         g_free(message);
@@ -242,46 +244,38 @@ enum GncPriceColumn {PRICED_FULL_NAME, PRICED_COMM, PRICED_DATE, PRICED_COUNT};
 static time64
 gnc_prices_dialog_load_view (GtkTreeView *view, GNCPriceDB *pdb)
 {
-    GtkTreeModel *model = gtk_tree_view_get_model (view);
-    const gnc_commodity_table *commodity_table = gnc_get_current_commodities ();
-    GList *namespace_list = gnc_commodity_table_get_namespaces (commodity_table);
-    gnc_commodity *tmp_commodity = NULL;
-    char  *tmp_namespace = NULL;
-    GList *commodity_list = NULL;
-    GtkTreeIter iter;
-
-    time64 oldest = gnc_time (NULL);
+    auto oldest = gnc_time (nullptr);
+    auto model = gtk_tree_view_get_model (view);
+    const auto commodity_table = gnc_get_current_commodities ();
+    auto namespace_list = gnc_commodity_table_get_namespaces (commodity_table);
 
-    namespace_list = g_list_first (namespace_list);
-    while (namespace_list != NULL)
+    while (namespace_list)
     {
-        tmp_namespace = namespace_list->data;
+        auto tmp_namespace = static_cast<char *> (namespace_list->data);
         DEBUG("Looking at namespace %s", tmp_namespace);
-        commodity_list = gnc_commodity_table_get_commodities (commodity_table, tmp_namespace);
-        commodity_list  = g_list_first (commodity_list);
-        while (commodity_list != NULL)
+        auto commodity_list = gnc_commodity_table_get_commodities (commodity_table, tmp_namespace);
+        while (commodity_list)
         {
-            gint num = 0;
-            tmp_commodity = commodity_list->data;
-            num = gnc_pricedb_num_prices (pdb, tmp_commodity);
+            auto tmp_commodity = static_cast<gnc_commodity *> (commodity_list->data);
+            auto num = gnc_pricedb_num_prices (pdb, tmp_commodity);
             DEBUG("Looking at commodity %s, Number of prices %d", gnc_commodity_get_fullname (tmp_commodity), num);
 
             if (num > 0)
             {
-                PriceList *list = gnc_pricedb_get_prices (pdb, tmp_commodity, NULL);
-                GList *node = g_list_last (list);
-                GNCPrice *price = (GNCPrice*)node->data;
-                time64 price_time = gnc_price_get_time64 (price);
-                const gchar *name_str = gnc_commodity_get_printname (tmp_commodity);
-                gchar *date_str, *num_str;
+                auto list = gnc_pricedb_get_prices (pdb, tmp_commodity, NULL);
+                auto node = g_list_last (list);
+                auto price = static_cast<GNCPrice*> (node->data);
+                auto price_time = gnc_price_get_time64 (price);
+                auto name_str = gnc_commodity_get_printname (tmp_commodity);
+
                 if (oldest > price_time)
                     oldest = price_time;
 
-                date_str = qof_print_date (price_time);
-                num_str = g_strdup_printf ("%d", num);
+                auto date_str = qof_print_date (price_time);
+                auto num_str = g_strdup_printf ("%d", num);
 
+                GtkTreeIter iter;
                 gtk_list_store_append (GTK_LIST_STORE(model), &iter);
-
                 gtk_list_store_set (GTK_LIST_STORE(model), &iter, PRICED_FULL_NAME, name_str,
                                     PRICED_COMM, tmp_commodity, PRICED_DATE, date_str, PRICED_COUNT, num_str, -1);
 
@@ -291,9 +285,9 @@ gnc_prices_dialog_load_view (GtkTreeView *view, GNCPriceDB *pdb)
             }
             commodity_list = g_list_next (commodity_list);
         }
+        g_list_free (commodity_list);
         namespace_list = g_list_next (namespace_list);
     }
-    g_list_free (commodity_list);
     g_list_free (namespace_list);
 
     return oldest;
@@ -302,25 +296,24 @@ gnc_prices_dialog_load_view (GtkTreeView *view, GNCPriceDB *pdb)
 static GList *
 gnc_prices_dialog_get_commodities (GtkTreeView *view)
 {
-    GtkTreeModel     *model = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
-    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(view));
-    GList            *list = gtk_tree_selection_get_selected_rows (selection, &model);
-    GList            *row;
-    GList            *comm_list = NULL;
-    GtkTreeIter       iter;
-    gnc_commodity    *comm;
+    auto model = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
+    auto selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(view));
+    auto list = gtk_tree_selection_get_selected_rows (selection, &model);
+    GList *comm_list = nullptr;
 
     // Walk the list
-    for (row = g_list_first (list); row; row = g_list_next (row))
+    for (auto row = g_list_first (list); row; row = g_list_next (row))
     {
-        if (gtk_tree_model_get_iter (model, &iter, row->data))
+        auto path = static_cast<GtkTreePath *> (row->data);
+        GtkTreeIter iter;
+        if (gtk_tree_model_get_iter (model, &iter, path))
         {
+            gnc_commodity *comm;
             gtk_tree_model_get (model, &iter, PRICED_COMM, &comm, -1);
             comm_list = g_list_prepend (comm_list, comm);
         }
     }
-    g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL);
-    g_list_free (list);
+    g_list_free_full (list, (GDestroyNotify) gtk_tree_path_free);
 
     return g_list_reverse (comm_list);
 }
@@ -328,7 +321,7 @@ gnc_prices_dialog_get_commodities (GtkTreeView *view)
 static void
 change_source_flag (PriceRemoveSourceFlags source, gboolean set, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
     GtkWidget *w = gtk_dialog_get_widget_for_response (GTK_DIALOG(pdb_dialog->remove_dialog), GTK_RESPONSE_OK);
     gboolean enable_button;
 
@@ -347,7 +340,7 @@ change_source_flag (PriceRemoveSourceFlags source, gboolean set, gpointer data)
 static void
 check_event_fq_cb (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
     gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(widget));
 
     change_source_flag (PRICE_REMOVE_SOURCE_FQ, active, pdb_dialog);
@@ -356,7 +349,7 @@ check_event_fq_cb (GtkWidget *widget, gpointer data)
 static void
 check_event_user_cb (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
     gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(widget));
 
     change_source_flag (PRICE_REMOVE_SOURCE_USER, active, pdb_dialog);
@@ -365,7 +358,7 @@ check_event_user_cb (GtkWidget *widget, gpointer data)
 static void
 check_event_app_cb (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
     gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(widget));
 
     change_source_flag (PRICE_REMOVE_SOURCE_APP, active, pdb_dialog);
@@ -374,14 +367,13 @@ check_event_app_cb (GtkWidget *widget, gpointer data)
 static void
 selection_changed_cb (GtkTreeSelection *selection, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
-    GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW(pdb_dialog->remove_view));
-    GList *rows = gtk_tree_selection_get_selected_rows (selection, &model);
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
+    auto model = gtk_tree_view_get_model (GTK_TREE_VIEW(pdb_dialog->remove_view));
+    auto rows = gtk_tree_selection_get_selected_rows (selection, &model);
     gboolean have_rows = (gnc_list_length_cmp (rows, 0));
 
     change_source_flag (PRICE_REMOVE_SOURCE_COMM, have_rows, pdb_dialog);
-    g_list_foreach (rows, (GFunc) gtk_tree_path_free, NULL);
-    g_list_free (rows);
+    g_list_free_full (rows, (GDestroyNotify) gtk_tree_path_free);
 }
 
 static GDate
@@ -401,45 +393,36 @@ get_fiscal_end_date (void)
 void
 gnc_prices_dialog_remove_old_clicked (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
-    GtkBuilder *builder;
-    GtkTreeModel *model;
-    GtkWidget *date, *label, *box;
-    GtkWidget *button;
-    GtkTreeSelection *selection;
-    GtkTreeViewColumn *tree_column;
-    GtkCellRenderer   *cr;
-    time64 first;
-    gint result;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
-    builder = gtk_builder_new();
+    auto builder = gtk_builder_new();
     gnc_builder_add_from_file (builder, "dialog-price.glade", "liststore4");
     gnc_builder_add_from_file (builder, "dialog-price.glade", "deletion_date_dialog");
 
     pdb_dialog->remove_dialog = GTK_WIDGET(gtk_builder_get_object (builder, "deletion_date_dialog"));
 
-    box = GTK_WIDGET(gtk_builder_get_object (builder, "date_hbox"));
-    date = gnc_date_edit_new (time (NULL), FALSE, FALSE);
+    auto box = GTK_WIDGET(gtk_builder_get_object (builder, "date_hbox"));
+    auto date = gnc_date_edit_new (time (NULL), FALSE, FALSE);
 
     gtk_box_pack_start (GTK_BOX (box), date, FALSE, FALSE, 0);
     gtk_widget_show (date);
     gtk_entry_set_activates_default(GTK_ENTRY(GNC_DATE_EDIT(date)->date_entry), TRUE);
-    label = GTK_WIDGET(gtk_builder_get_object (builder, "date_label"));
+    auto label = GTK_WIDGET(gtk_builder_get_object (builder, "date_label"));
     gnc_date_make_mnemonic_target (GNC_DATE_EDIT(date), label);
 
     // Setup the commodity view
     pdb_dialog->remove_view = GTK_TREE_VIEW(gtk_builder_get_object (builder, "commodty_treeview"));
-    selection = gtk_tree_view_get_selection (pdb_dialog->remove_view);
+    auto selection = gtk_tree_view_get_selection (pdb_dialog->remove_view);
     gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
 
     // Add Entries column this way as align does not seem to work from builder
-    tree_column = gtk_tree_view_column_new();
+    auto tree_column = gtk_tree_view_column_new();
     gtk_tree_view_column_set_title (tree_column, _("Entries"));
     gtk_tree_view_append_column (GTK_TREE_VIEW(pdb_dialog->remove_view), tree_column);
     gtk_tree_view_column_set_alignment (tree_column, 0.5);
     gtk_tree_view_column_set_expand (tree_column, TRUE);
-    cr = gtk_cell_renderer_text_new();
+    auto cr = gtk_cell_renderer_text_new();
     gtk_tree_view_column_pack_start (tree_column, cr, TRUE);
     // set 'xalign' property of the cell renderer
     gtk_tree_view_column_set_attributes (tree_column, cr, "text", PRICED_COUNT, NULL);
@@ -454,19 +437,19 @@ gnc_prices_dialog_remove_old_clicked (GtkWidget *widget, gpointer data)
 
     gtk_window_set_transient_for (GTK_WINDOW (pdb_dialog->remove_dialog), GTK_WINDOW (pdb_dialog->window));
 
-    pdb_dialog->remove_source = 9; // FQ and Commodities highlighted
-    button = GTK_WIDGET(gtk_builder_get_object (builder, "checkbutton_fq"));
+    pdb_dialog->remove_source = PRICE_REMOVE_SOURCE_FQ + PRICE_REMOVE_SOURCE_COMM; // FQ and Commodities highlighted
+    auto button = GTK_WIDGET(gtk_builder_get_object (builder, "checkbutton_fq"));
     g_signal_connect (button, "toggled", G_CALLBACK (check_event_fq_cb), pdb_dialog);
     button = GTK_WIDGET(gtk_builder_get_object (builder, "checkbutton_user"));
     g_signal_connect (button, "toggled", G_CALLBACK (check_event_user_cb), pdb_dialog);
     button = GTK_WIDGET(gtk_builder_get_object (builder, "checkbutton_app"));
     g_signal_connect (button, "toggled", G_CALLBACK (check_event_app_cb), pdb_dialog);
 
-    result = gtk_dialog_run (GTK_DIALOG (pdb_dialog->remove_dialog));
+    auto result = gtk_dialog_run (GTK_DIALOG (pdb_dialog->remove_dialog));
     if (result == GTK_RESPONSE_OK)
     {
         const char *fmt = _("Are you sure you want to delete these prices?");
-        GList *comm_list = gnc_prices_dialog_get_commodities (pdb_dialog->remove_view);
+        auto comm_list = gnc_prices_dialog_get_commodities (pdb_dialog->remove_view);
 
         // Are you sure you want to delete the entries and we have commodities
         if ((g_list_length (comm_list) != 0) && (gnc_verify_dialog (GTK_WINDOW (pdb_dialog->remove_dialog), FALSE, fmt, NULL)))
@@ -477,7 +460,7 @@ gnc_prices_dialog_remove_old_clicked (GtkWidget *widget, gpointer data)
             PriceRemoveKeepOptions keep = PRICE_REMOVE_KEEP_NONE;
 
             // disconnect the model to the price treeview
-            model = gtk_tree_view_get_model (GTK_TREE_VIEW(pdb_dialog->price_tree));
+            auto model = gtk_tree_view_get_model (GTK_TREE_VIEW(pdb_dialog->price_tree));
             g_object_ref (G_OBJECT(model));
             gtk_tree_view_set_model (GTK_TREE_VIEW(pdb_dialog->price_tree), NULL);
 
@@ -502,19 +485,18 @@ gnc_prices_dialog_remove_old_clicked (GtkWidget *widget, gpointer data)
 
             if (keep != PRICE_REMOVE_KEEP_SCALED)
                 gnc_pricedb_remove_old_prices (pdb_dialog->price_db, comm_list,
-                                               &fiscal_end_date,
-                                               last, pdb_dialog->remove_source,
+                                               &fiscal_end_date, last,
+                                               static_cast<PriceRemoveSourceFlags> (pdb_dialog->remove_source),
                                                keep);
             else
             {
-                time64 tmp;
-                GDate tmp_date = time64_to_gdate (last);
+                auto tmp_date = time64_to_gdate (last);
                 g_date_subtract_months (&tmp_date, 6);
-                tmp = gdate_to_time64 (tmp_date);
+                auto tmp = gdate_to_time64 (tmp_date);
 
                 gnc_pricedb_remove_old_prices (pdb_dialog->price_db, comm_list,
                                                &fiscal_end_date, tmp,
-                                               pdb_dialog->remove_source,
+                                               static_cast<PriceRemoveSourceFlags> (pdb_dialog->remove_source),
                                                PRICE_REMOVE_KEEP_LAST_WEEKLY);
 
                 g_date_subtract_months (&tmp_date, 6);
@@ -522,7 +504,7 @@ gnc_prices_dialog_remove_old_clicked (GtkWidget *widget, gpointer data)
 
                 gnc_pricedb_remove_old_prices (pdb_dialog->price_db, comm_list,
                                                &fiscal_end_date, tmp,
-                                               pdb_dialog->remove_source,
+                                               static_cast<PriceRemoveSourceFlags> (pdb_dialog->remove_source),
                                                PRICE_REMOVE_KEEP_LAST_MONTHLY);
             }
             // reconnect the model to the price treeview
@@ -541,19 +523,17 @@ gnc_prices_dialog_remove_old_clicked (GtkWidget *widget, gpointer data)
 void
 gnc_prices_dialog_add_clicked (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
-    GNCPrice *price = NULL;
-    GList *price_list;
-    GList *comm_list;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
+    GNCPrice *price = nullptr;
     gboolean unref_price = FALSE;
 
     ENTER(" ");
-    price_list = gnc_tree_view_price_get_selected_prices (pdb_dialog->price_tree);
-    comm_list = gnc_tree_view_price_get_selected_commodities (pdb_dialog->price_tree);
+    auto price_list = gnc_tree_view_price_get_selected_prices (pdb_dialog->price_tree);
+    auto comm_list = gnc_tree_view_price_get_selected_commodities (pdb_dialog->price_tree);
 
     if (price_list) // selected row is on a price
     {
-        price = price_list->data;
+        price = static_cast<GNCPrice *> (price_list->data);
         g_list_free (price_list);
     }
     else if (comm_list) // selection contains price parent rows
@@ -561,7 +541,8 @@ gnc_prices_dialog_add_clicked (GtkWidget *widget, gpointer data)
         if (!gnc_list_length_cmp (comm_list, 1)) // make sure it is only one parent
         {
             price = gnc_price_create (pdb_dialog->book);
-            gnc_price_set_commodity (price, comm_list->data);
+            auto comm = static_cast<gnc_commodity *> (comm_list->data);
+            gnc_price_set_commodity (price, comm);
             unref_price = TRUE;
         }
         g_list_free (comm_list);
@@ -578,7 +559,7 @@ gnc_prices_dialog_add_clicked (GtkWidget *widget, gpointer data)
 void
 gnc_prices_dialog_get_quotes_clicked (GtkWidget *widget, gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
     SCM quotes_func;
     SCM book_scm;
     SCM scm_window;
@@ -617,26 +598,21 @@ static void
 gnc_prices_dialog_selection_changed (GtkTreeSelection *treeselection,
                                      gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
-    GtkTreeModel *model;
-    GList *price_list;
-    GList *rows;
-    gint length;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     ENTER(" ");
-    price_list = gnc_tree_view_price_get_selected_prices (pdb_dialog->price_tree);
-    length = g_list_length (price_list);
+    auto price_list = gnc_tree_view_price_get_selected_prices (pdb_dialog->price_tree);
+    auto length = g_list_length (price_list);
     g_list_free (price_list);
 
-    model = gtk_tree_view_get_model (GTK_TREE_VIEW(pdb_dialog->price_tree));
-    rows = gtk_tree_selection_get_selected_rows (treeselection, &model);
+    auto model = gtk_tree_view_get_model (GTK_TREE_VIEW(pdb_dialog->price_tree));
+    auto rows = gtk_tree_selection_get_selected_rows (treeselection, &model);
 
     // if selected rows greater than length, parents must of been selected also
     if (g_list_length (rows) > length)
         length = 0;
 
-    g_list_foreach (rows, (GFunc) gtk_tree_path_free, NULL);
-    g_list_free (rows);
+    g_list_free_full (rows, (GDestroyNotify) gtk_tree_path_free);
 
     gtk_widget_set_sensitive (pdb_dialog->edit_button,
                               length == 1);
@@ -652,29 +628,23 @@ static gboolean
 gnc_price_dialog_filter_ns_func (gnc_commodity_namespace *name_space,
                                  gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
-    const gchar *name;
-    static GList *cm_list;
-    GList *item;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     /* Never show the template list */
-    name = gnc_commodity_namespace_get_name (name_space);
+    auto name = gnc_commodity_namespace_get_name (name_space);
     if (g_strcmp0 (name, GNC_COMMODITY_NS_TEMPLATE) == 0)
         return FALSE;
 
     /* See if this namespace has commodities */
-    cm_list = gnc_commodity_namespace_get_commodity_list(name_space);
-    for (item = cm_list; item; item = g_list_next(item))
+    auto cm_list = gnc_commodity_namespace_get_commodity_list (name_space);
+    for (auto item = cm_list; item; item = g_list_next (item))
     {
-
         /* For each commodity, see if there are prices */
-        if (gnc_pricedb_has_prices(pdb_dialog->price_db, item->data, NULL))
-        {
+        auto comm = static_cast<gnc_commodity *> (item->data);
+        if (gnc_pricedb_has_prices (pdb_dialog->price_db, comm, nullptr))
             return TRUE;
-        }
     }
 
-    //  printf("Namespace %s not visible\n", name);
     return FALSE;
 }
 
@@ -683,7 +653,7 @@ static gboolean
 gnc_price_dialog_filter_cm_func (gnc_commodity *commodity,
                                  gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     /* Show any commodity that has prices */
     return gnc_pricedb_has_prices(pdb_dialog->price_db, commodity, NULL);
@@ -808,11 +778,10 @@ gnc_prices_dialog_create (GtkWidget * parent, PricesDialog *pdb_dialog)
 static void
 close_handler (gpointer user_data)
 {
-    PricesDialog *pdb_dialog = user_data;
+    auto pdb_dialog = static_cast<PricesDialog *> (user_data);
 
     ENTER(" ");
     gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(pdb_dialog->window));
-
     gtk_widget_destroy (GTK_WIDGET (pdb_dialog->window));
     LEAVE(" ");
 }
@@ -830,7 +799,7 @@ static gboolean
 show_handler (const char *klass, gint component_id,
               gpointer user_data, gpointer iter_data)
 {
-    PricesDialog *pdb_dialog = user_data;
+    auto pdb_dialog = static_cast<PricesDialog *> (user_data);
 
     ENTER(" ");
     if (!pdb_dialog)
@@ -849,7 +818,7 @@ gboolean
 gnc_prices_dialog_key_press_cb (GtkWidget *widget, GdkEventKey *event,
                                 gpointer data)
 {
-    PricesDialog *pdb_dialog = data;
+    auto pdb_dialog = static_cast<PricesDialog *> (data);
 
     if (event->keyval == GDK_KEY_Escape)
     {
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 06676f1ed..0738f90ef 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -78,7 +78,7 @@ gnucash/gnome/dialog-lot-viewer.c
 gnucash/gnome/dialog-new-user.c
 gnucash/gnome/dialog-order.c
 gnucash/gnome/dialog-payment.c
-gnucash/gnome/dialog-price-edit-db.c
+gnucash/gnome/dialog-price-edit-db.cpp
 gnucash/gnome/dialog-price-editor.c
 gnucash/gnome/dialog-print-check.c
 gnucash/gnome/dialog-progress.c

commit fbf9aecd25ab033b23dff6996a18969301742cad
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Tue Mar 16 12:16:10 2021 +0100

    Use GncQuotes in transfer dialog

diff --git a/gnucash/gnome-utils/dialog-transfer.cpp b/gnucash/gnome-utils/dialog-transfer.cpp
index 8de4d94de..354e982fb 100644
--- a/gnucash/gnome-utils/dialog-transfer.cpp
+++ b/gnucash/gnome-utils/dialog-transfer.cpp
@@ -27,7 +27,7 @@
 #include <gtk/gtk.h>
 #include <gdk/gdkkeysyms.h>
 #include <glib/gi18n.h>
-#include <libguile.h>
+#include <gnc-quotes.hpp>
 
 extern "C" {
 #include "dialog-transfer.h"
@@ -45,10 +45,7 @@ extern "C" {
 #include "gnc-ui.h"
 #include "Transaction.h"
 #include "Account.h"
-#include "swig-runtime.h"
-#include "guile-mappings.h"
 #include "engine-helpers.h"
-#include "gnc-engine-guile.h"
 #include "QuickFill.h"
 #include <gnc-commodity.h>
 }
@@ -1784,44 +1781,25 @@ gnc_xfer_dialog_close_cb(GtkDialog *dialog, gpointer data)
 void
 gnc_xfer_dialog_fetch (GtkButton *button, XferDialog *xferData)
 {
-    PriceReq pr;
-    SCM quotes_func;
-    SCM book_scm;
-    SCM scm_window;
-
     g_return_if_fail (xferData);
 
     ENTER(" ");
 
-    quotes_func = scm_c_eval_string ("gnc:book-add-quotes");
-
-    if (!scm_is_procedure (quotes_func))
+    GncQuotes quotes (xferData->book);
+    if (quotes.cmd_result() != 0)
     {
+        if (!quotes.error_msg().empty())
+            PWARN ("%s", quotes.error_msg().c_str());
         LEAVE("quote retrieval failed");
         return;
     }
 
-    book_scm = gnc_book_to_scm (xferData->book);
-    if (scm_is_true (scm_not (book_scm)))
-    {
-        LEAVE("no book");
-        return;
-    }
-
-    scm_window =  SWIG_NewPointerObj(xferData->dialog,
-                                     SWIG_TypeQuery("_p_GtkWindow"), 0);
-
-    if (scm_is_true (scm_not (book_scm)))
-    {
-        LEAVE("no scm window");
-        return;
-    }
-
-    gnc_set_busy_cursor (NULL, TRUE);
-    scm_call_2 (quotes_func, scm_window, book_scm);
-    gnc_unset_busy_cursor (NULL);
+    gnc_set_busy_cursor (nullptr, TRUE);
+    quotes.fetch_all();
+    gnc_unset_busy_cursor (nullptr);
 
     /*the results should be in the price db now, but don't crash if not. */
+    PriceReq pr;
     price_request_from_xferData(&pr, xferData);
     if (lookup_price(&pr, LATEST))
     {
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 276d3e5b1..87ea68a11 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -22,6 +22,7 @@
 #ifndef GNC_QUOTES_HPP
 #define GNC_QUOTES_HPP
 
+#include <memory>
 #include <string>
 #include <vector>
 #include <gnc-commodity.hpp>  // For CommVec alias

commit 1a0be99bc69317aef82bd6db23c437afc02ed044
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Mon Mar 15 18:43:58 2021 +0100

    Build dialog-transfer as C++
    
    Preparation to use GncQuotes instead of price-quotes.scm

diff --git a/gnucash/gnome-utils/CMakeLists.txt b/gnucash/gnome-utils/CMakeLists.txt
index a703c3916..cbd5ad7a7 100644
--- a/gnucash/gnome-utils/CMakeLists.txt
+++ b/gnucash/gnome-utils/CMakeLists.txt
@@ -42,7 +42,7 @@ set (gnome_utils_SOURCES
   dialog-reset-warnings.c
   dialog-tax-table.c
   dialog-totd.c
-  dialog-transfer.c
+  dialog-transfer.cpp
   dialog-userpass.c
   dialog-utils.c
   gnc-account-sel.c
diff --git a/gnucash/gnome-utils/dialog-transfer.c b/gnucash/gnome-utils/dialog-transfer.cpp
similarity index 95%
rename from gnucash/gnome-utils/dialog-transfer.c
rename to gnucash/gnome-utils/dialog-transfer.cpp
index 543d0c608..8de4d94de 100644
--- a/gnucash/gnome-utils/dialog-transfer.c
+++ b/gnucash/gnome-utils/dialog-transfer.cpp
@@ -27,7 +27,9 @@
 #include <gtk/gtk.h>
 #include <gdk/gdkkeysyms.h>
 #include <glib/gi18n.h>
+#include <libguile.h>
 
+extern "C" {
 #include "dialog-transfer.h"
 #include "dialog-utils.h"
 #include "gnc-amount-edit.h"
@@ -43,13 +45,13 @@
 #include "gnc-ui.h"
 #include "Transaction.h"
 #include "Account.h"
-#include <libguile.h>
 #include "swig-runtime.h"
 #include "guile-mappings.h"
 #include "engine-helpers.h"
 #include "gnc-engine-guile.h"
 #include "QuickFill.h"
 #include <gnc-commodity.h>
+}
 
 
 #define DIALOG_TRANSFER_CM_CLASS "dialog-transfer"
@@ -162,6 +164,7 @@ static void gnc_transfer_dialog_set_selected_account (XferDialog *dialog,
                                                       Account *account,
                                                       XferDirection direction);
 
+extern "C"  {
 void gnc_xfer_description_insert_cb(GtkEditable *editable,
                                     const gchar *insert_text,
                                     const gint insert_text_len,
@@ -177,6 +180,7 @@ void price_amount_radio_toggled_cb(GtkToggleButton *togglebutton, gpointer data)
 
 void gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data);
 void gnc_xfer_dialog_close_cb(GtkDialog *dialog, gpointer data);
+}
 
 /** Implementations **********************************************/
 
@@ -337,10 +341,9 @@ gnc_xfer_dialog_update_price (XferDialog *xferData)
 static void
 gnc_xfer_dialog_toggle_cb(GtkToggleButton *button, gpointer data)
 {
-    AccountTreeFilterInfo* info;
     GncTreeViewAccount* treeview = GNC_TREE_VIEW_ACCOUNT (data);
 
-    info = g_object_get_data (G_OBJECT(treeview), "filter-info");
+    auto info = static_cast<AccountTreeFilterInfo*> (g_object_get_data (G_OBJECT(treeview), "filter-info"));
     if (info)
     {
         info->show_inc_exp = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
@@ -355,11 +358,9 @@ gnc_xfer_dialog_key_press_cb (GtkWidget   *widget,
                               GdkEventKey *event,
                               gpointer     unused)
 {
-    GtkWidget *toplevel;
-
     if ((event->keyval == GDK_KEY_Return) || (event->keyval == GDK_KEY_KP_Enter))
     {
-        toplevel = gtk_widget_get_toplevel (widget);
+        auto toplevel = gtk_widget_get_toplevel (widget);
         if (gtk_widget_is_toplevel(toplevel) && GTK_IS_WINDOW(toplevel))
         {
             gtk_window_activate_default(GTK_WINDOW(toplevel));
@@ -375,15 +376,10 @@ gnc_xfer_dialog_set_price_auto (XferDialog *xferData,
                                 const gnc_commodity *from_currency,
                                 const gnc_commodity *to_currency)
 {
-    gnc_numeric from_rate;
-    gnc_numeric to_rate;
-    gnc_numeric price_value;
-
     if (!currency_active)
     {
-        GtkEntry *entry;
         gnc_xfer_dialog_set_price_edit(xferData, gnc_numeric_zero());
-        entry = GTK_ENTRY(gnc_amount_edit_gtk_entry
+        auto entry = GTK_ENTRY(gnc_amount_edit_gtk_entry
                           (GNC_AMOUNT_EDIT(xferData->price_edit)));
         gtk_entry_set_text(entry, "");
 
@@ -399,13 +395,13 @@ gnc_xfer_dialog_set_price_auto (XferDialog *xferData,
         return;
     }
 
-    from_rate = gnc_euro_currency_get_rate (from_currency);
-    to_rate = gnc_euro_currency_get_rate (to_currency);
+    auto from_rate = gnc_euro_currency_get_rate (from_currency);
+    auto to_rate = gnc_euro_currency_get_rate (to_currency);
 
     if (gnc_numeric_zero_p (from_rate) || gnc_numeric_zero_p (to_rate))
         gnc_xfer_dialog_update_price (xferData);
 
-    price_value = gnc_numeric_div (to_rate, from_rate, GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
+    auto price_value = gnc_numeric_div (to_rate, from_rate, GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE);
 
     gnc_amount_edit_set_amount (GNC_AMOUNT_EDIT(xferData->price_edit), price_value);
 
@@ -415,21 +411,17 @@ gnc_xfer_dialog_set_price_auto (XferDialog *xferData,
 static void
 gnc_xfer_dialog_curr_acct_activate(XferDialog *xferData)
 {
-    Account *to_account;
-    Account *from_account;
-    gboolean curr_active;
-
     g_return_if_fail (xferData != NULL);
-    from_account =
+    auto from_account =
         gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_FROM);
 
-    to_account =
+    auto to_account =
         gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_TO);
 
-    curr_active = (xferData->exch_rate ||
-                   ((from_account != NULL) && (to_account != NULL)))
-        && !gnc_commodity_equiv(xferData->from_commodity,
-                                xferData->to_commodity);
+    gboolean curr_active =
+        (xferData->exch_rate ||
+        ((from_account != NULL) && (to_account != NULL))) &&
+        !gnc_commodity_equiv(xferData->from_commodity, xferData->to_commodity);
 
     gtk_widget_set_sensitive(xferData->curr_xfer_table, curr_active);
     gtk_widget_set_sensitive(xferData->price_edit,
@@ -447,12 +439,10 @@ gnc_xfer_dialog_curr_acct_activate(XferDialog *xferData)
 
     if (!curr_active)
     {
-        GtkEntry *entry;
-
         gnc_amount_edit_set_amount(GNC_AMOUNT_EDIT(xferData->to_amount_edit),
                                    gnc_numeric_zero ());
-        entry = GTK_ENTRY(gnc_amount_edit_gtk_entry
-                          (GNC_AMOUNT_EDIT(xferData->to_amount_edit)));
+        auto entry = GTK_ENTRY(gnc_amount_edit_gtk_entry
+                               (GNC_AMOUNT_EDIT(xferData->to_amount_edit)));
         gtk_entry_set_text(entry, "");
     }
 }
@@ -461,9 +451,9 @@ gnc_xfer_dialog_curr_acct_activate(XferDialog *xferData)
 void
 price_amount_radio_toggled_cb(GtkToggleButton *togglebutton, gpointer data)
 {
-    XferDialog *xferData = data;
-    g_return_if_fail (xferData != NULL);
+    g_return_if_fail (data);
 
+    auto xferData = static_cast<XferDialog *> (data);
     gtk_widget_set_sensitive(xferData->price_edit, gtk_toggle_button_get_active
                              (GTK_TOGGLE_BUTTON(xferData->price_radio)));
     gtk_widget_set_sensitive(xferData->to_amount_edit,
@@ -481,23 +471,17 @@ price_amount_radio_toggled_cb(GtkToggleButton *togglebutton, gpointer data)
 static void
 gnc_xfer_dialog_reload_quickfill( XferDialog *xferData )
 {
-    GList *splitlist, *node;
-    Split *split;
-    Transaction *trans;
-    Account *account;
-
-    account = gnc_transfer_dialog_get_selected_account (xferData, xferData->quickfill);
+    auto account = gnc_transfer_dialog_get_selected_account (xferData, xferData->quickfill);
 
     /* get a new QuickFill to use */
     gnc_quickfill_destroy( xferData->qf );
     xferData->qf = gnc_quickfill_new();
 
-    splitlist = xaccAccountGetSplitList( account );
-
-    for ( node = splitlist; node; node = node->next )
+    auto splitlist = xaccAccountGetSplitList( account );
+    for ( GList *node = splitlist; node; node = node->next )
     {
-        split = node->data;
-        trans = xaccSplitGetParent( split );
+        auto split = static_cast<Split *> (node->data);
+        auto trans = xaccSplitGetParent (split);
         gnc_quickfill_insert( xferData->qf,
                               xaccTransGetDescription (trans), QUICKFILL_LIFO);
     }
@@ -508,22 +492,19 @@ static void
 gnc_xfer_dialog_from_tree_selection_changed_cb (GtkTreeSelection *selection,
                                                 gpointer data)
 {
-    XferDialog *xferData = data;
-    GNCPrintAmountInfo print_info;
-    gnc_commodity *commodity;
-    Account *account;
+    auto xferData = static_cast<XferDialog *> (data);
 
-    account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_FROM);
+    auto account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_FROM);
     if (!account)
         return;
 
-    commodity = gnc_account_or_default_currency(account, NULL);
+    auto commodity = gnc_account_or_default_currency(account, NULL);
     gtk_label_set_text(GTK_LABEL(xferData->from_currency_label),
                        gnc_commodity_get_printname(commodity));
 
     xferData->from_commodity = commodity;
 
-    print_info = gnc_account_print_info (account, FALSE);
+    auto print_info = gnc_account_print_info (account, FALSE);
     gnc_amount_edit_set_print_info (GNC_AMOUNT_EDIT (xferData->amount_edit),
                                     print_info);
     gnc_amount_edit_set_fraction (GNC_AMOUNT_EDIT (xferData->amount_edit),
@@ -542,22 +523,19 @@ gnc_xfer_dialog_from_tree_selection_changed_cb (GtkTreeSelection *selection,
 static void
 gnc_xfer_dialog_to_tree_selection_changed_cb (GtkTreeSelection *selection, gpointer data)
 {
-    XferDialog *xferData = data;
-    GNCPrintAmountInfo print_info;
-    gnc_commodity *commodity;
-    Account *account;
+    auto xferData = static_cast<XferDialog *> (data);
 
-    account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_TO);
+    auto account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_TO);
     if (!account)
         return;
 
-    commodity = xaccAccountGetCommodity(account);
+    auto commodity = xaccAccountGetCommodity(account);
     gtk_label_set_text(GTK_LABEL(xferData->to_currency_label),
                        gnc_commodity_get_printname(commodity));
 
     xferData->to_commodity = commodity;
 
-    print_info = gnc_account_print_info (account, FALSE);
+    auto print_info = gnc_account_print_info (account, FALSE);
     gnc_amount_edit_set_print_info (GNC_AMOUNT_EDIT (xferData->to_amount_edit),
                                     print_info);
     gnc_amount_edit_set_fraction (GNC_AMOUNT_EDIT (xferData->to_amount_edit),
@@ -576,10 +554,7 @@ gboolean
 gnc_xfer_dialog_inc_exp_filter_func (Account *account,
                                      gpointer data)
 {
-    AccountTreeFilterInfo* info;
-    GNCAccountType type;
-
-    info = (AccountTreeFilterInfo*)data;
+    auto info = static_cast<AccountTreeFilterInfo *> (data);
 
     if (!info->show_hidden && xaccAccountIsHidden(account))
     {
@@ -591,7 +566,7 @@ gnc_xfer_dialog_inc_exp_filter_func (Account *account,
         return TRUE;
     }
 
-    type = xaccAccountGetType(account);
+    auto type = xaccAccountGetType(account);
     return ((type != ACCT_TYPE_INCOME) && (type != ACCT_TYPE_EXPENSE));
 }
 
@@ -599,18 +574,14 @@ static void
 gnc_xfer_dialog_fill_tree_view(XferDialog *xferData,
                                XferDirection direction)
 {
-    GtkTreeView *tree_view;
     const char *show_inc_exp_message = _("Show the income and expense accounts");
-    GtkWidget *scroll_win;
     GtkWidget *button;
-    GtkTreeSelection *selection;
-    gboolean  use_accounting_labels;
-    AccountTreeFilterInfo *info;
-    GtkBuilder *builder = g_object_get_data (G_OBJECT (xferData->dialog), "builder");
+    GtkWidget *scroll_win;
+    auto builder = static_cast<GtkBuilder *> (g_object_get_data (G_OBJECT (xferData->dialog), "builder"));
 
     g_return_if_fail (xferData != NULL);
-    use_accounting_labels = gnc_prefs_get_bool(GNC_PREFS_GROUP_GENERAL,
-                                               GNC_PREF_ACCOUNTING_LABELS);
+    auto use_accounting_labels = gnc_prefs_get_bool(GNC_PREFS_GROUP_GENERAL,
+                                                    GNC_PREF_ACCOUNTING_LABELS);
 
     /* In "normal" mode (non accounting terms) the account where the
      * money comes from is displayed on the left side and the account
@@ -643,12 +614,13 @@ gnc_xfer_dialog_fill_tree_view(XferDialog *xferData,
     }
 
 
+    AccountTreeFilterInfo *info;
     if (direction == XFER_DIALOG_TO)
         info = to_info;
     else
         info = from_info;
 
-    tree_view = GTK_TREE_VIEW(gnc_tree_view_account_new(FALSE));
+    auto tree_view = GTK_TREE_VIEW(gnc_tree_view_account_new(FALSE));
     gtk_container_add(GTK_CONTAINER(scroll_win), GTK_WIDGET(tree_view));
     info->show_inc_exp = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
     info->show_hidden = FALSE;
@@ -662,7 +634,7 @@ gnc_xfer_dialog_fill_tree_view(XferDialog *xferData,
     g_signal_connect (G_OBJECT (tree_view), "key-press-event",
                       G_CALLBACK (gnc_xfer_dialog_key_press_cb), NULL);
 
-    selection = gtk_tree_view_get_selection (tree_view);
+    auto selection = gtk_tree_view_get_selection (tree_view);
     gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);
 
     gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), FALSE);
@@ -824,8 +796,9 @@ gnc_xfer_dialog_quickfill( XferDialog *xferData )
 static gboolean
 idle_select_region(gpointer data)
 {
-    XferDialog *xferData = data;
-    g_return_val_if_fail(xferData, FALSE);
+    g_return_val_if_fail(data, FALSE);
+
+    auto xferData = static_cast<XferDialog *> (data);
 
     gtk_editable_select_region(GTK_EDITABLE(xferData->description_entry),
                                xferData->desc_start_selection,
@@ -992,8 +965,9 @@ static gboolean
 gnc_xfer_amount_update_cb(GtkWidget *widget, GdkEventFocus *event,
                           gpointer data)
 {
-    XferDialog * xferData = data;
-    g_return_val_if_fail (xferData != NULL, FALSE);
+    g_return_val_if_fail (data, FALSE);
+
+    auto xferData = static_cast<XferDialog *> (data);
 
     gnc_amount_edit_evaluate (GNC_AMOUNT_EDIT (xferData->amount_edit), NULL);
 
@@ -1052,19 +1026,18 @@ static gboolean
 gnc_xfer_price_update_cb(GtkWidget *widget, GdkEventFocus *event,
                          gpointer data)
 {
-    XferDialog *xferData = data;
+    auto xferData = static_cast<XferDialog *> (data);
 
     gnc_xfer_update_to_amount (xferData);
     xferData->price_type = PRICE_TYPE_TRN;
 
-
     return FALSE;
 }
 
 static gboolean
 gnc_xfer_date_changed_cb(GtkWidget *widget, gpointer data)
 {
-    XferDialog *xferData = data;
+    auto xferData = static_cast<XferDialog *> (data);
 
     if (xferData)
         gnc_xfer_dialog_update_price (xferData);
@@ -1076,11 +1049,10 @@ static gboolean
 gnc_xfer_to_amount_update_cb(GtkWidget *widget, GdkEventFocus *event,
                              gpointer data)
 {
-    XferDialog *xferData = data;
-    gnc_numeric price_value;
+    auto xferData = static_cast<XferDialog *> (data);
 
     gnc_amount_edit_evaluate (GNC_AMOUNT_EDIT (xferData->to_amount_edit), NULL);
-    price_value = gnc_xfer_dialog_compute_price_value(xferData);
+    auto price_value = gnc_xfer_dialog_compute_price_value (xferData);
     gnc_amount_edit_set_amount(GNC_AMOUNT_EDIT(xferData->price_edit),
                                price_value);
     xferData->price_source = PRICE_SOURCE_XFER_DLG_VAL;
@@ -1675,14 +1647,9 @@ create_price(XferDialog *xferData, time64 time)
 void
 gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data)
 {
-    XferDialog *xferData = data;
-    Account *to_account;
-    Account *from_account;
-    gnc_numeric amount, to_amount;
-    time64 time;
-    GDate date;
+    g_return_if_fail (data);
+    auto xferData = static_cast<XferDialog *> (data);
 
-    g_return_if_fail (xferData != NULL);
     ENTER(" ");
 
     if (response == GTK_RESPONSE_APPLY)
@@ -1695,7 +1662,7 @@ gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data)
      * Remove date changed handler to prevent it from triggering
      * on a focus-out event while we're already destroying the widget */
     g_signal_handlers_disconnect_by_func (G_OBJECT (xferData->date_entry),
-                                            G_CALLBACK (gnc_xfer_date_changed_cb),
+                                            (gpointer)gnc_xfer_date_changed_cb,
                                             xferData);
 
     if (response != GTK_RESPONSE_OK)
@@ -1705,8 +1672,8 @@ gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data)
         return;
     }
 
-    from_account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_FROM);
-    to_account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_TO);
+    auto from_account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_FROM);
+    auto to_account = gnc_transfer_dialog_get_selected_account (xferData, XFER_DIALOG_TO);
 
     if (xferData->exch_rate == NULL &&
         !check_accounts(xferData, from_account, to_account))
@@ -1719,7 +1686,7 @@ gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data)
         return;
     }
 
-    amount = gnc_amount_edit_get_amount(GNC_AMOUNT_EDIT(xferData->amount_edit));
+    auto amount = gnc_amount_edit_get_amount(GNC_AMOUNT_EDIT(xferData->amount_edit));
 
     if (gnc_numeric_zero_p (amount))
     {
@@ -1728,10 +1695,13 @@ gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data)
         LEAVE("invalid from amount");
         return;
     }
+
+    GDate date;
     g_date_clear (&date, 1);
     gnc_date_edit_get_gdate (GNC_DATE_EDIT (xferData->date_entry), &date);
-    time = gdate_to_time64 (date);
+    auto time = gdate_to_time64 (date);
 
+    auto to_amount = amount;
     if (!gnc_commodity_equiv(xferData->from_commodity, xferData->to_commodity))
     {
         if (!check_edit(xferData))
@@ -1739,22 +1709,18 @@ gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data)
         to_amount = gnc_amount_edit_get_amount
             (GNC_AMOUNT_EDIT(xferData->to_amount_edit));
     }
-    else
-        to_amount = amount;
 
     gnc_suspend_gui_refresh ();
 
     if (xferData->exch_rate)
     {
-        gnc_numeric price_value;
-
         /* If we've got the price-button set, then make sure we update the
          * to-amount before we use it.
          */
         if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(xferData->price_radio)))
             gnc_xfer_update_to_amount(xferData);
 
-        price_value = gnc_xfer_dialog_compute_price_value(xferData);
+        auto price_value = gnc_xfer_dialog_compute_price_value(xferData);
         gnc_amount_edit_set_amount(GNC_AMOUNT_EDIT(xferData->price_edit),
                                    price_value);
         *(xferData->exch_rate) = gnc_numeric_abs(price_value);
@@ -1777,14 +1743,13 @@ gnc_xfer_dialog_response_cb (GtkDialog *dialog, gint response, gpointer data)
 void
 gnc_xfer_dialog_close_cb(GtkDialog *dialog, gpointer data)
 {
-    XferDialog * xferData = data;
-    GtkWidget *entry;
+    auto xferData = static_cast<XferDialog *> (data);
 
     /* Notify transaction callback to unregister here */
     if (xferData->transaction_cb)
         xferData->transaction_cb(NULL, xferData->transaction_user_data);
 
-    entry = gnc_amount_edit_gtk_entry(GNC_AMOUNT_EDIT(xferData->amount_edit));
+    auto entry = gnc_amount_edit_gtk_entry(GNC_AMOUNT_EDIT(xferData->amount_edit));
     g_signal_handlers_disconnect_matched (G_OBJECT (entry), G_SIGNAL_MATCH_DATA,
                                           0, 0, NULL, NULL, xferData);
 
@@ -2070,11 +2035,10 @@ gnc_xfer_dialog_create(GtkWidget *parent, XferDialog *xferData)
 static void
 close_handler (gpointer user_data)
 {
-    XferDialog *xferData = user_data;
-    GtkWidget *dialog;
+    auto xferData = static_cast<XferDialog *> (user_data);
 
     ENTER(" ");
-    dialog = GTK_WIDGET (xferData->dialog);
+    auto dialog = GTK_WIDGET (xferData->dialog);
 
     gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW (dialog));
     gtk_widget_hide (dialog);
@@ -2238,10 +2202,10 @@ void gnc_xfer_dialog_add_user_specified_button( XferDialog *xferData,
 {
     if ( xferData && label && callback )
     {
-        GtkBuilder *builder = g_object_get_data (G_OBJECT (xferData->dialog), "builder");
-        GtkWidget *button   = gtk_button_new_with_label( label );
-        GtkWidget *box      = GTK_WIDGET(gtk_builder_get_object (builder,
-                                                                 "transfermain-vbox" ));
+        auto builder = static_cast<GtkBuilder *> (g_object_get_data (G_OBJECT (xferData->dialog), "builder"));
+        auto button = gtk_button_new_with_label( label );
+        auto box = GTK_WIDGET (gtk_builder_get_object (builder,
+                                                       "transfermain-vbox" ));
         gtk_box_pack_end( GTK_BOX(box), button, FALSE, FALSE, 0 );
         g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (callback), user_data);
         gtk_widget_show( button );
@@ -2292,7 +2256,7 @@ gboolean gnc_xfer_dialog_run_until_done( XferDialog *xferData )
      * that's bad mojo whole gtk_dialog_run is still in control.
      */
     count = g_signal_handlers_disconnect_by_func(dialog,
-                                                 gnc_xfer_dialog_response_cb,
+                                                 (gpointer) gnc_xfer_dialog_response_cb,
                                                  xferData);
     g_assert(count == 1);
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4ab7e5cfd..06676f1ed 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -141,7 +141,7 @@ gnucash/gnome-utils/dialog-query-view.c
 gnucash/gnome-utils/dialog-reset-warnings.c
 gnucash/gnome-utils/dialog-tax-table.c
 gnucash/gnome-utils/dialog-totd.c
-gnucash/gnome-utils/dialog-transfer.c
+gnucash/gnome-utils/dialog-transfer.cpp
 gnucash/gnome-utils/dialog-userpass.c
 gnucash/gnome-utils/dialog-utils.c
 gnucash/gnome-utils/gnc-account-sel.c

commit a00bce168c7082d7857b68e6464950a5efa1aef8
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Mon Mar 15 18:38:24 2021 +0100

    GncQuotes - cache default currency

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 3b310bb58..28cddccd7 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -93,6 +93,7 @@ private:
     int m_cmd_result;
     std::string m_error_msg;
     QofBook *m_book;
+    gnc_commodity *m_dflt_curr;
 };
 
 /* GncQuotes implementation */
@@ -115,6 +116,7 @@ GncQuotesImpl::check (QofBook *book)
     m_error_msg.clear();
     m_cmd_result  = 0;
     m_book = book;
+    m_dflt_curr = gnc_default_currency();
 
     auto perl_executable = bp::search_path("perl");
     auto fq_check = std::string(gnc_path_get_bindir()) + "/gnc-fq-check";
@@ -150,18 +152,17 @@ GncQuotesImpl::fetch (const CommVec& commodities)
 {
     m_comm_vec = commodities;  // Store for later use
 
-    auto dflt_curr = gnc_default_currency();
     bpt::ptree pt, pt_child;
-    pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (dflt_curr));
+    pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (m_dflt_curr));
 
     std::for_each (m_comm_vec.cbegin(), m_comm_vec.cend(),
-        [&pt, &dflt_curr] (auto comm)
+        [this, &pt] (auto comm)
         {
             auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
             auto comm_ns = std::string("currency");
             if (gnc_commodity_is_currency (comm))
             {
-                if (gnc_commodity_equiv(comm, dflt_curr) ||
+                if (gnc_commodity_equiv(comm, m_dflt_curr) ||
                     (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
                     return;
             }
@@ -294,15 +295,13 @@ GncQuotesImpl::parse_quotes (const std::string &quotes_str)
                       "Failed to parse quotes results." + "\n";
     }
 
-    auto book = m_book;
-    auto dflt_curr = gnc_default_currency();
     auto pricedb = gnc_pricedb_get_db (m_book);
     std::for_each(m_comm_vec.begin(), m_comm_vec.end(),
-                  [this, &pt, &dflt_curr, &pricedb] (gnc_commodity *comm)
+                  [this, &pt, &pricedb] (gnc_commodity *comm)
                 {
                     auto comm_ns = gnc_commodity_get_namespace (comm);
                     auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
-                    if (gnc_commodity_equiv(comm, dflt_curr) ||
+                    if (gnc_commodity_equiv(comm, m_dflt_curr) ||
                        (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
                         return;
                     if (pt.find (comm_mnemonic) == pt.not_found())

commit fcbe6cf10cf0ec59d2179a87ce1d884b60a10ecc
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Sun Feb 28 22:36:22 2021 +0100

    Add code to parse json data returned by our F::Q wrapper
    
    This code will convert the json data into GncPrice objects and add them
    to the pricedb, effectively doing what price-quotes.scm does.
    
    A few notable remarks:
    - still requires plenty of cleaning up. This is the first proof of concept
    - like the original scm based code, this parser completely ignores  timezone
      information. As it wasn't used before and nobody complained, it may not
      be that important. Or it can be implemented later.
    - price-quotes.scm would first check if a price already existed in the pricedb
      and try to update that one instead of adding one (only if the old price's
      type is inferior). However that is redundant as gnc_pricedb_add_price does
      the same check. So I have omitted this extra check from GncQuotes.
    - currency quotes can be inverted. I have slightly changed the way to handle
      this. The perl wrapper code will simply set an "inverted" flag in that case,
      but will otherwise not swap currency and commodity as it used to be the case.
      On parsing, the inversion flag will cause the GncNumeric that's parsed from
      the price to be inverted. As it's still a GncNumeric that shouldn't result
      in any loss of precision, while keeping prices in the db always in the default
      currency.

diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index 290c97c87..f57cc62b0 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -54,8 +54,8 @@ static std::string empty_string{};
 /* This static indicates the debugging module that this .o belongs to.  */
 static QofLogModule log_module = GNC_MOD_GUI;
 
-static void
-scm_cleanup_and_exit_with_failure (QofSession *session)
+static int
+cleanup_and_exit_with_failure (QofSession *session)
 {
     if (session)
     {
@@ -71,68 +71,15 @@ scm_cleanup_and_exit_with_failure (QofSession *session)
         qof_session_destroy (session);
     }
     qof_event_resume();
-    gnc_shutdown (1);
+    return 1;
 }
 
+/* scm_boot_guile doesn't expect to return, so call shutdown ourselves here */
 static void
-scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **argv)
+scm_cleanup_and_exit_with_failure (QofSession *session)
 {
-    auto add_quotes_file = static_cast<const std::string*>(data);
-
-    gnc_prefs_init ();
-    qof_event_suspend();
-
-    scm_c_eval_string("(debug-set! stack 200000)");
-
-    auto mod = scm_c_resolve_module("gnucash price-quotes");
-    scm_set_current_module(mod);
-
-    auto add_quotes = scm_c_eval_string("gnc:book-add-quotes");
-    auto session = gnc_get_current_session();
-    if (!session)
-        scm_cleanup_and_exit_with_failure (session);
-
-    qof_session_begin(session, add_quotes_file->c_str(), SESSION_NORMAL_OPEN);
-    if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
-        scm_cleanup_and_exit_with_failure (session);
-
-    qof_session_load(session, NULL);
-    if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
-        scm_cleanup_and_exit_with_failure (session);
-
-    GncQuotes quotes  (qof_session_get_book(session));
-    if (quotes.cmd_result() == 0)
-    {
-        std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
-        auto quote_sources = quotes.sources_as_glist();
-        gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
-        g_list_free_full (quote_sources, g_free);
-    }
-    else
-    {
-        std::cerr << bl::translate ("No quotes retrieved. Finance::Quote isn't "
-        "installed properly.") << "\n";
-        std::cerr << bl::translate ("Error message:") << std::endl;
-        std::cerr << quotes.error_msg() << std::endl;
-    }
-
-    auto scm_book = gnc_book_to_scm(qof_session_get_book(session));
-    auto scm_result = scm_call_2(add_quotes, SCM_BOOL_F, scm_book);
-
-    qof_session_save(session, NULL);
-    if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
-        scm_cleanup_and_exit_with_failure (session);
-
-    qof_session_destroy(session);
-    if (!scm_is_true(scm_result))
-    {
-        PERR ("Failed to add quotes to %s.", add_quotes_file->c_str());
-        scm_cleanup_and_exit_with_failure (session);
-    }
-
-    qof_event_resume();
-    gnc_shutdown(0);
-    return;
+    cleanup_and_exit_with_failure (session);
+    gnc_shutdown (1);
 }
 
 static void
@@ -379,9 +326,48 @@ Gnucash::quotes_info (void)
 int
 Gnucash::add_quotes (const bo_str& uri)
 {
-    if (uri && !uri->empty())
-        scm_boot_guile (0, nullptr, scm_add_quotes, (void *)&(*uri));
+    gnc_prefs_init ();
+    qof_event_suspend();
 
+    auto session = gnc_get_current_session();
+    if (!session)
+        return 1;
+
+    qof_session_begin(session, uri->c_str(), SESSION_NORMAL_OPEN);
+    if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
+        cleanup_and_exit_with_failure (session);
+
+    qof_session_load(session, NULL);
+    if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
+        cleanup_and_exit_with_failure (session);
+
+    GncQuotes quotes (qof_session_get_book(session));
+    if (quotes.cmd_result() == 0)
+    {
+        std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
+        auto quote_sources = quotes.sources_as_glist();
+        gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
+        g_list_free_full (quote_sources, g_free);
+    }
+    else
+    {
+        std::cerr << bl::translate ("No quotes retrieved. Finance::Quote isn't "
+                                    "installed properly.") << "\n";
+        std::cerr << bl::translate ("Error message:") << std::endl;
+        std::cerr << quotes.error_msg() << std::endl;
+    }
+    quotes.fetch_all ();
+
+    qof_session_save(session, NULL);
+    if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
+        cleanup_and_exit_with_failure (session);
+
+    qof_session_destroy(session);
+
+    if (quotes.cmd_result() != 0)
+        std::cerr << bl::format (bl::translate ("Failed to add quotes to {1}.")) % *uri << "\n";
+
+    qof_event_resume();
     return 0;
 }
 
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 08be25b55..3b310bb58 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -27,6 +27,7 @@
 #include <string>
 #include <iostream>
 #include <sstream>
+#include <boost/algorithm/string.hpp>
 #include <boost/filesystem.hpp>
 #include <boost/process.hpp>
 #include <boost/property_tree/ptree.hpp>
@@ -36,6 +37,8 @@
 #include <boost/asio.hpp>
 #include <glib.h>
 #include "gnc-commodity.hpp"
+#include <gnc-datetime.hpp>
+#include <gnc-numeric.hpp>
 #include "gnc-quotes.hpp"
 
 extern "C" {
@@ -48,6 +51,7 @@ extern "C" {
 }
 
 namespace bp = boost::process;
+namespace bfs = boost::filesystem;
 namespace bpt = boost::property_tree;
 namespace bio = boost::iostreams;
 
@@ -78,8 +82,12 @@ private:
     // - one with the contents of stdout
     // - one with the contents of stderr
     // Will also set m_cmd_result
-    CmdOutput run_cmd (std::string cmd_name, StrVec args, StrVec input_vec);
+    template <typename BufferT> CmdOutput run_cmd (const bfs::path &cmd_name, StrVec args, BufferT input);
 
+    void parse_quotes (const std::string &quotes);
+
+
+    CommVec m_comm_vec;
     std::string m_version;
     QuoteSources m_sources;
     int m_cmd_result;
@@ -140,11 +148,13 @@ GncQuotesImpl::sources_as_glist()
 void
 GncQuotesImpl::fetch (const CommVec& commodities)
 {
+    m_comm_vec = commodities;  // Store for later use
+
     auto dflt_curr = gnc_default_currency();
     bpt::ptree pt, pt_child;
     pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (dflt_curr));
 
-    std::for_each (commodities.cbegin(), commodities.cend(),
+    std::for_each (m_comm_vec.cbegin(), m_comm_vec.cend(),
         [&pt, &dflt_curr] (auto comm)
         {
             auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
@@ -152,7 +162,7 @@ GncQuotesImpl::fetch (const CommVec& commodities)
             if (gnc_commodity_is_currency (comm))
             {
                 if (gnc_commodity_equiv(comm, dflt_curr) ||
-                    (!comm_mnemonic  || (strcmp (comm_mnemonic, "XXX") == 0)))
+                    (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
                     return;
             }
             else
@@ -165,7 +175,30 @@ GncQuotesImpl::fetch (const CommVec& commodities)
 
     std::ostringstream result;
     bpt::write_json(result, pt);
-    std::cerr << "GncQuotes fetch_all - resulting json object\n" << result.str() << std::endl;
+    //std::cerr << "GncQuotes fetch_all - resulting json object\n" << result.str() << std::endl;
+
+    auto perl_executable = bp::search_path("perl");
+    auto fq_wrapper = std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper";
+    StrVec args { "-w", fq_wrapper };
+
+    auto cmd_out = run_cmd (perl_executable.string(), args, result.str());
+
+    if (m_cmd_result == 0)
+    {
+        std::string resultstr;
+        for (auto line : cmd_out.first)
+            resultstr.append(std::move(line) + "\n");
+        parse_quotes (resultstr);
+    }
+    else
+        for (auto line : cmd_out.second)
+            m_error_msg.append(std::move(line) + "\n");
+
+    for (auto line : cmd_out.first)
+        std::cerr << "Output line retrieved from wrapper:\n" << line << std::endl;
+
+    for (auto line : cmd_out.second)
+        std::cerr << "Error line retrieved from wrapper:\n" << line << std::endl;
 
 }
 
@@ -186,22 +219,27 @@ format_quotes (const std::vector<gnc_commodity*>)
 }
 
 
-CmdOutput
-GncQuotesImpl::run_cmd (std::string cmd_name, StrVec args, StrVec input_vec)
+template <typename BufferT> CmdOutput
+GncQuotesImpl::run_cmd (const bfs::path &cmd_name, StrVec args, BufferT input)
 {
     StrVec out_vec, err_vec;
 
+    auto av_key = gnc_prefs_get_string ("general.finance-quote", "alphavantage-api-key");
+    if (!av_key)
+        std::cerr << "No AlphaVantage API key set, currency quotes and other AlphaVantage based quotes won't work." << std::endl;
+
     try
     {
         std::future<std::vector<char> > out_buf, err_buf;
         boost::asio::io_service svc;
 
-        auto input_buf = bp::buffer (input_vec);
+        auto input_buf = bp::buffer (input);
         bp::child process (cmd_name, args,
-                            bp::std_out > out_buf,
-                            bp::std_err > err_buf,
-                            bp::std_in < input_buf,
-                            svc);
+                           bp::std_out > out_buf,
+                           bp::std_err > err_buf,
+                           bp::std_in < input_buf,
+                           bp::env["ALPHAVANTAGE_API_KEY"]= (av_key ? av_key : ""),
+                           svc);
         svc.run();
         process.wait();
 
@@ -233,6 +271,168 @@ GncQuotesImpl::run_cmd (std::string cmd_name, StrVec args, StrVec input_vec)
     return CmdOutput (std::move(out_vec), std::move(err_vec));
 }
 
+void
+GncQuotesImpl::parse_quotes (const std::string &quotes_str)
+{
+    bpt::ptree pt;
+    std::istringstream ss {quotes_str};
+
+    try
+    {
+        bpt::read_json (ss, pt);
+    }
+    catch (bpt::json_parser_error &e) {
+        m_cmd_result = -1;
+        m_error_msg = m_error_msg +
+                      "Failed to parse quotes results." + "\n" +
+                      "Error message:" + "\n" +
+                      e.what() + "\n";
+    }
+    catch (...) {
+        m_cmd_result = -1;
+        m_error_msg = m_error_msg +
+                      "Failed to parse quotes results." + "\n";
+    }
+
+    auto book = m_book;
+    auto dflt_curr = gnc_default_currency();
+    auto pricedb = gnc_pricedb_get_db (m_book);
+    std::for_each(m_comm_vec.begin(), m_comm_vec.end(),
+                  [this, &pt, &dflt_curr, &pricedb] (gnc_commodity *comm)
+                {
+                    auto comm_ns = gnc_commodity_get_namespace (comm);
+                    auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
+                    if (gnc_commodity_equiv(comm, dflt_curr) ||
+                       (!comm_mnemonic || (strcmp (comm_mnemonic, "XXX") == 0)))
+                        return;
+                    if (pt.find (comm_mnemonic) == pt.not_found())
+                    {
+                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote didn't return any data.\n";
+                        return;
+                    }
+
+                    std::string key = comm_mnemonic;
+                    boost::optional<bool> success = pt.get_optional<bool> (key + ".success");
+                    std::string price_type = "last";
+                    boost::optional<std::string> price_str = pt.get_optional<std::string> (key + "." + price_type);
+                    if (!price_str)
+                    {
+                        price_type = "nav";
+                        price_str = pt.get_optional<std::string> (key + "." + price_type);
+                    }
+                    if (!price_str)
+                    {
+                        price_type = "price";
+                        price_str = pt.get_optional<std::string> (key + "." + price_type);
+                        /* guile wrapper used "unknown" as price type when "price" was found,
+                         * reproducing here to keep same result for users in the pricedb */
+                        price_type = "unknown";
+                    }
+
+                    boost::optional<bool> inverted_tmp = pt.get_optional<bool> (key + ".inverted");
+                    bool inverted = inverted_tmp ? *inverted_tmp : false;
+                    boost::optional<std::string> date_str = pt.get_optional<std::string> (key + ".date");
+                    boost::optional<std::string> time_str = pt.get_optional<std::string> (key + ".time");
+                    boost::optional<std::string> currency_str = pt.get_optional<std::string> (key + ".currency");
+
+
+                    std::cout << "Commodity: " << comm_mnemonic << "\n";
+                    std::cout << "     Date: " << (date_str ? *date_str : "missing") << "\n";
+                    std::cout << "     Time: " << (time_str ? *time_str : "missing") << "\n";
+                    std::cout << " Currency: " << (currency_str ? *currency_str : "missing") << "\n";
+                    std::cout << "    Price: " << (price_str ? *price_str : "missing") << "\n";
+                    std::cout << " Inverted: " << (inverted ? "yes" : "no") << "\n\n";
+
+                    if (!success || !*success)
+                    {
+                        boost::optional<std::string> errmsg = pt.get_optional<std::string> (key + ".errormsg");
+                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote returned fetch failure.\n";
+                        std::cerr << "Reason: " << (errmsg ? *errmsg : "unknown") << "\n";
+                        return;
+                    }
+
+                    if (!price_str)
+                    {
+                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote didn't return a valid price\n";
+                        return;
+                    }
+
+                    GncNumeric price;
+                    try
+                    {
+                        price = GncNumeric { *price_str };
+                    }
+                    catch (...)
+                    {
+                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - failed to parse returned price '" << *price_str << "'\n";
+                        return;
+                    }
+
+                    if (inverted)
+                        price = price.inv();
+
+                    if (!currency_str)
+                    {
+                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - Finance::Quote didn't return a currency\n";
+                        return;
+                    }
+                    boost::to_upper (*currency_str);
+                    auto commodity_table = gnc_commodity_table_get_table (m_book);
+                    auto currency = gnc_commodity_table_lookup (commodity_table, "ISO4217", currency_str->c_str());
+
+                    if (!currency)
+                    {
+                        std::cerr << "Skipped " << comm_ns << ":" << comm_mnemonic << " - failed to parse returned currency '" << *currency_str << "'\n";
+                        return;
+                    }
+
+                    std::string iso_date_str = GncDate().format ("%Y-%m-%d");
+                    if (date_str)
+                    {
+                    // Returned date is always in MM/DD/YYYY format according to F::Q man page, transform it to simplify conversion to GncDateTime
+                        auto date_tmp = *date_str;
+                        iso_date_str = date_tmp.substr (6, 4) + "-" + date_tmp.substr (0, 2) + "-" + date_tmp.substr (3, 2);
+                    }
+                    else
+                        std::cerr << "Info: no date  was returned for " << comm_ns << ":" << comm_mnemonic << " - will use today\n";
+                    iso_date_str += " " + (time_str ? *time_str : "12:00:00");
+
+                    auto can_convert = true;
+                    try
+                    {
+                        GncDateTime testdt {iso_date_str};
+                    }
+                    catch (...)
+                    {
+                        std::cerr << "Warning: failed to parse quote date and time '" << iso_date_str << "' for " << comm_ns << ":" << comm_mnemonic << " - will use today\n";
+                        return;
+                    }
+
+                    /*  Bit of an odd construct: GncDateTimes can't be copied,
+                        which makes it impossible to first create a temporary GncDateTime
+                        based on whether the string is parsable and then assign that temporary
+                        to our final GncDateTime. The creation has to happen in one go, so
+                        below construct will pass a different constructor argument based on
+                        whether a test conversion worked or not.
+                    */
+                    GncDateTime quotedt {can_convert ? iso_date_str : GncDateTime()};
+
+                    auto gnc_price = gnc_price_create (m_book);
+                    gnc_price_begin_edit (gnc_price);
+                    gnc_price_set_commodity (gnc_price, comm);
+                    gnc_price_set_currency (gnc_price, currency);
+                    gnc_price_set_time64 (gnc_price, static_cast<time64> (quotedt));
+                    gnc_price_set_source (gnc_price, PRICE_SOURCE_FQ);
+                    gnc_price_set_typestr (gnc_price, price_type.c_str());
+                    gnc_price_set_value (gnc_price, price);
+                    gnc_pricedb_add_price (pricedb, gnc_price);
+                    gnc_price_commit_edit (gnc_price);
+                    gnc_price_unref (gnc_price);
+                });
+
+}
+
+
 
 /********************************************************************
  * gnc_quotes_get_quotable_commodities

commit 5c13da0e59901bbcdad97dc9511fb9a1e17bc5bc
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Feb 12 18:52:04 2021 +0100

    Move fetching quotes info to gnc-commands
    
    It needs gnc-prefs which I don't want to add by default to gnucash-cli.

diff --git a/gnucash/gnucash-cli.cpp b/gnucash/gnucash-cli.cpp
index d384675a9..90e6a1b8c 100644
--- a/gnucash/gnucash-cli.cpp
+++ b/gnucash/gnucash-cli.cpp
@@ -131,24 +131,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
     {
         if (*m_quotes_cmd == "info")
         {
-            GncQuotes quotes;
-            if (quotes.cmd_result() == 0)
-            {
-                std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << "\n";
-                std::cout << bl::translate ("Finance::Quote sources: ");
-                for (auto source : quotes.sources())
-                    std::cout << source << " ";
-                std::cout << std::endl;
-                return 0;
-            }
-            else
-            {
-                std::cerr << bl::translate ("Finance::Quote isn't "
-                                            "installed properly.") << "\n";
-                std::cerr << bl::translate ("Error message:") << "\n";
-                std::cerr << quotes.error_msg() << std::endl;
-                return 1;
-            }
+            return Gnucash::quotes_info ();
         }
         else if (*m_quotes_cmd == "get")
         {
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index 3d84c6183..290c97c87 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -352,6 +352,30 @@ scm_report_list ([[maybe_unused]] void *data,
     return;
 }
 
+int
+Gnucash::quotes_info (void)
+{
+    gnc_prefs_init ();
+    GncQuotes quotes;
+    if (quotes.cmd_result() == 0)
+    {
+        std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << "\n";
+        std::cout << bl::translate ("Finance::Quote sources: ");
+        for (auto source : quotes.sources())
+            std::cout << source << " ";
+        std::cout << std::endl;
+        return 0;
+    }
+    else
+    {
+        std::cerr << bl::translate ("Finance::Quote isn't "
+                                    "installed properly.") << "\n";
+        std::cerr << bl::translate ("Error message:") << "\n";
+        std::cerr << quotes.error_msg() << std::endl;
+        return 1;
+    }
+}
+
 int
 Gnucash::add_quotes (const bo_str& uri)
 {
diff --git a/gnucash/gnucash-commands.hpp b/gnucash/gnucash-commands.hpp
index 01511d584..d5958ae5f 100644
--- a/gnucash/gnucash-commands.hpp
+++ b/gnucash/gnucash-commands.hpp
@@ -32,6 +32,7 @@ using bo_str = boost::optional <std::string>;
 
 namespace Gnucash {
 
+    int quotes_info (void);
     int add_quotes (const bo_str& uri);
     int run_report (const bo_str& file_to_load,
                     const bo_str& run_report,

commit 6ecc1ef73f134b0045a7df78245bb2a9544f15fc
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Feb 12 18:18:20 2021 +0100

    GncQuotes - switch to Pimpl idiom
    
    That allows the private implementation to pass a number of variables
    based on various boost libraries. It's better to not have them in
    the public interface to keep compilation times down.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index ac1f892a7..08be25b55 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -55,18 +55,52 @@ namespace bio = boost::iostreams;
 CommVec
 gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
 
-GncQuotes::GncQuotes ()
+class GncQuotesImpl
+{
+public:
+    // Constructor - checks for presence of Finance::Quote and import version and quote sources
+    GncQuotesImpl ();
+    GncQuotesImpl (QofBook *book);
+
+    void fetch_all ();
+    void fetch (const CommVec& commodities);
+
+    const int cmd_result() noexcept { return m_cmd_result; }
+    const std::string& error_msg() noexcept { return m_error_msg; }
+    const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
+    const QuoteSources& sources() noexcept { return m_sources; }
+    GList* sources_as_glist ();
+
+private:
+    // Check if Finance::Quote is properly installed
+    void check (QofBook *book);
+    // Run the command specified. Returns two vectors for further processing by the caller
+    // - one with the contents of stdout
+    // - one with the contents of stderr
+    // Will also set m_cmd_result
+    CmdOutput run_cmd (std::string cmd_name, StrVec args, StrVec input_vec);
+
+    std::string m_version;
+    QuoteSources m_sources;
+    int m_cmd_result;
+    std::string m_error_msg;
+    QofBook *m_book;
+};
+
+/* GncQuotes implementation */
+
+GncQuotesImpl::GncQuotesImpl ()
 {
     check (nullptr);
 }
 
-GncQuotes::GncQuotes (QofBook *book)
+GncQuotesImpl::GncQuotesImpl (QofBook *book)
 {
     check (book);
 }
 
 void
-GncQuotes::check (QofBook *book)
+GncQuotesImpl::check (QofBook *book)
 {
     m_version.clear();
     m_sources.clear();
@@ -94,7 +128,7 @@ GncQuotes::check (QofBook *book)
 }
 
 GList*
-GncQuotes::sources_as_glist()
+GncQuotesImpl::sources_as_glist()
 {
     GList* slist = nullptr;
     std::for_each (m_sources.rbegin(), m_sources.rend(),
@@ -104,7 +138,7 @@ GncQuotes::sources_as_glist()
 
 
 void
-GncQuotes::fetch (const CommVec& commodities)
+GncQuotesImpl::fetch (const CommVec& commodities)
 {
     auto dflt_curr = gnc_default_currency();
     bpt::ptree pt, pt_child;
@@ -137,7 +171,7 @@ GncQuotes::fetch (const CommVec& commodities)
 
 
 void
-GncQuotes::fetch_all ()
+GncQuotesImpl::fetch_all ()
 {
     auto commodities = gnc_quotes_get_quotable_commodities (
         gnc_commodity_table_get_table (m_book));
@@ -153,7 +187,7 @@ format_quotes (const std::vector<gnc_commodity*>)
 
 
 CmdOutput
-GncQuotes::run_cmd (std::string cmd_name, StrVec args, StrVec input_vec)
+GncQuotesImpl::run_cmd (std::string cmd_name, StrVec args, StrVec input_vec)
 {
     StrVec out_vec, err_vec;
 
@@ -282,3 +316,54 @@ gnc_quotes_get_quotable_commodities (const gnc_commodity_table * table)
     //LEAVE ("list head %p", &l);
     return l;
 }
+
+/* Public interface functions */
+// Constructor - checks for presence of Finance::Quote and import version and quote sources
+GncQuotes::GncQuotes ()
+{
+    m_impl = std::make_unique<GncQuotesImpl> ();
+}
+
+GncQuotes::GncQuotes (QofBook *book)
+{
+    m_impl = std::make_unique<GncQuotesImpl> (book);
+}
+
+void
+GncQuotes::fetch_all ()
+{
+    m_impl->fetch_all ();
+}
+
+void GncQuotes::fetch (const CommVec& commodities)
+{
+    m_impl->fetch (commodities);
+}
+
+const int GncQuotes::cmd_result() noexcept
+{
+    return m_impl->cmd_result ();
+}
+
+const std::string& GncQuotes::error_msg() noexcept
+{
+    return m_impl->error_msg ();
+}
+
+const std::string& GncQuotes::version() noexcept
+{
+    return m_impl->version ();
+}
+
+const QuoteSources& GncQuotes::sources() noexcept
+{
+    return m_impl->sources ();
+}
+
+GList* GncQuotes::sources_as_glist ()
+{
+    return m_impl->sources_as_glist ();
+}
+
+GncQuotes::~GncQuotes() = default;
+
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index ab68cb8b7..276d3e5b1 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -37,6 +37,7 @@ using CmdOutput = std::pair <StrVec, StrVec>;
 
 const std::string not_found = std::string ("Not Found");
 
+class GncQuotesImpl;
 
 class GncQuotes
 {
@@ -44,35 +45,19 @@ public:
     // Constructor - checks for presence of Finance::Quote and import version and quote sources
     GncQuotes ();
     GncQuotes (QofBook *book);
-    ~GncQuotes () = default;
+    ~GncQuotes ();
 
     void fetch_all ();
     void fetch (const CommVec& commodities);
 
-    const int cmd_result() noexcept { return m_cmd_result; }
-    const std::string& error_msg() noexcept { return m_error_msg; }
-    const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
-    const QuoteSources& sources() noexcept { return m_sources; }
+    const int cmd_result() noexcept;
+    const std::string& error_msg() noexcept;
+    const std::string& version() noexcept;
+    const QuoteSources& sources() noexcept;
     GList* sources_as_glist ();
 
 private:
-    GncQuotes () = delete;
-    // Check if Finance::Quote is properly installed
-    void check (QofBook *book);
-    // Run the command specified. Returns two vectors for further processing by the caller
-    // - one with the contents of stdout
-    // - one with the contents of stderr
-    // Will also set m_cmd_result
-    template <typename BufferT> CmdOutput run_cmd (std::string cmd_name, StrVec args, BufferT input_vec);
-
-    void parse_quotes (std::string);
-
-
-    std::string m_version;
-    QuoteSources m_sources;
-    int m_cmd_result;
-    std::string m_error_msg;
-    QofBook *m_book;
+    std::unique_ptr<GncQuotesImpl> m_impl;
 };
 
 #endif /* GNC_QUOTES_HPP */

commit 65ae46426b77ae95b45acd55ff73edf6071319a7
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Feb 12 15:12:10 2021 +0100

    GncQuotes - add parameterized construction
    
    For all but the basic check a book is required. Might
    as well be able to pass it directly and store a reference
    to it. That will simplify member function declarations.

diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index bb9d778dd..3d84c6183 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -82,22 +82,6 @@ scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **ar
     gnc_prefs_init ();
     qof_event_suspend();
 
-    GncQuotes quotes;
-    if (quotes.cmd_result() == 0)
-    {
-        std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
-        auto quote_sources = quotes.sources_as_glist();
-        gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
-        g_list_free_full (quote_sources, g_free);
-    }
-    else
-    {
-        std::cerr << bl::translate ("No quotes retrieved. Finance::Quote isn't "
-                                    "installed properly.") << "\n";
-        std::cerr << bl::translate ("Error message:") << std::endl;
-        std::cerr << quotes.error_msg() << std::endl;
-    }
-
     scm_c_eval_string("(debug-set! stack 200000)");
 
     auto mod = scm_c_resolve_module("gnucash price-quotes");
@@ -116,6 +100,22 @@ scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **ar
     if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
         scm_cleanup_and_exit_with_failure (session);
 
+    GncQuotes quotes  (qof_session_get_book(session));
+    if (quotes.cmd_result() == 0)
+    {
+        std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
+        auto quote_sources = quotes.sources_as_glist();
+        gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
+        g_list_free_full (quote_sources, g_free);
+    }
+    else
+    {
+        std::cerr << bl::translate ("No quotes retrieved. Finance::Quote isn't "
+        "installed properly.") << "\n";
+        std::cerr << bl::translate ("Error message:") << std::endl;
+        std::cerr << quotes.error_msg() << std::endl;
+    }
+
     auto scm_book = gnc_book_to_scm(qof_session_get_book(session));
     auto scm_result = scm_call_2(add_quotes, SCM_BOOL_F, scm_book);
 
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index ea2b399b2..ac1f892a7 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -55,18 +55,24 @@ namespace bio = boost::iostreams;
 CommVec
 gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
 
-GncQuotes::GncQuotes()
+GncQuotes::GncQuotes ()
 {
-    check();
+    check (nullptr);
+}
+
+GncQuotes::GncQuotes (QofBook *book)
+{
+    check (book);
 }
 
 void
-GncQuotes::check (void)
+GncQuotes::check (QofBook *book)
 {
     m_version.clear();
     m_sources.clear();
     m_error_msg.clear();
     m_cmd_result  = 0;
+    m_book = book;
 
     auto perl_executable = bp::search_path("perl");
     auto fq_check = std::string(gnc_path_get_bindir()) + "/gnc-fq-check";
@@ -121,7 +127,6 @@ GncQuotes::fetch (const CommVec& commodities)
             auto key = comm_ns + "." + comm_mnemonic;
             pt.put (key, "");
         }
-
     );
 
     std::ostringstream result;
@@ -132,10 +137,10 @@ GncQuotes::fetch (const CommVec& commodities)
 
 
 void
-GncQuotes::fetch_all (QofBook *book)
+GncQuotes::fetch_all ()
 {
     auto commodities = gnc_quotes_get_quotable_commodities (
-        gnc_commodity_table_get_table (book));
+        gnc_commodity_table_get_table (m_book));
 
     fetch (commodities);
 }
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 792a9d12d..ab68cb8b7 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -42,9 +42,11 @@ class GncQuotes
 {
 public:
     // Constructor - checks for presence of Finance::Quote and import version and quote sources
-    GncQuotes();
+    GncQuotes ();
+    GncQuotes (QofBook *book);
+    ~GncQuotes () = default;
 
-    void fetch_all (QofBook *book);
+    void fetch_all ();
     void fetch (const CommVec& commodities);
 
     const int cmd_result() noexcept { return m_cmd_result; }
@@ -54,19 +56,23 @@ public:
     GList* sources_as_glist ();
 
 private:
+    GncQuotes () = delete;
     // Check if Finance::Quote is properly installed
-    void check (void);
+    void check (QofBook *book);
     // Run the command specified. Returns two vectors for further processing by the caller
     // - one with the contents of stdout
     // - one with the contents of stderr
     // Will also set m_cmd_result
-    CmdOutput run_cmd (std::string cmd_name, StrVec args, StrVec input_vec);
+    template <typename BufferT> CmdOutput run_cmd (std::string cmd_name, StrVec args, BufferT input_vec);
+
+    void parse_quotes (std::string);
 
 
     std::string m_version;
     QuoteSources m_sources;
     int m_cmd_result;
     std::string m_error_msg;
+    QofBook *m_book;
 };
 
 #endif /* GNC_QUOTES_HPP */

commit 3685e5de736fde4a5cefdb14ac8f2b7d97ad093b
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Thu Feb 11 16:05:05 2021 +0100

    Factor out the async call to perl
    
    This will allow us to reuse it for several F::Q commands, like
    check, fetch,...

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 0d496aa7c..ea2b399b2 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -68,45 +68,20 @@ GncQuotes::check (void)
     m_error_msg.clear();
     m_cmd_result  = 0;
 
-    auto perl_executable = bp::search_path("perl"); //or get it from somewhere else.
+    auto perl_executable = bp::search_path("perl");
     auto fq_check = std::string(gnc_path_get_bindir()) + "/gnc-fq-check";
+    StrVec args { "-w", fq_check };
 
-    try
-    {
-        std::future<std::vector<char> > output, error;
-        boost::asio::io_service svc;
+    auto cmd_out = run_cmd (perl_executable.string(), args, StrVec());
 
-        bp::child process (perl_executable, "-w", fq_check, bp::std_out > output, bp::std_err > error, svc);
-        svc.run();
-        process.wait();
+    for (auto line : cmd_out.first)
+        if (m_version.empty())
+            std::swap (m_version, line);
+        else
+            m_sources.push_back (std::move(line));
 
-        {
-            auto raw = output.get();
-            std::vector<std::string> data;
-            std::string line;
-            bio::stream_buffer<bio::array_source> sb(raw.data(), raw.size());
-            std::istream is(&sb);
-
-            while (std::getline(is, line) && !line.empty())
-                if (m_version.empty())
-                    std::swap (m_version, line);
-                else
-                    m_sources.push_back (std::move(line));
-
-            raw = error.get();
-            bio::stream_buffer<bio::array_source> eb(raw.data(), raw.size());
-            std::istream es(&eb);
-
-            while (std::getline(es, line) && !line.empty())
-                m_error_msg.append(std::move(line) + "\n");
-        }
-        m_cmd_result = process.exit_code();
-    }
-    catch (std::exception &e)
-    {
-        m_cmd_result = -1;
-        m_error_msg = e.what();
-    };
+    for (auto line : cmd_out.second)
+        m_error_msg.append(std::move(line) + "\n");
 
     if (m_cmd_result == 0)
         std::sort (m_sources.begin(), m_sources.end());
@@ -172,6 +147,54 @@ format_quotes (const std::vector<gnc_commodity*>)
 }
 
 
+CmdOutput
+GncQuotes::run_cmd (std::string cmd_name, StrVec args, StrVec input_vec)
+{
+    StrVec out_vec, err_vec;
+
+    try
+    {
+        std::future<std::vector<char> > out_buf, err_buf;
+        boost::asio::io_service svc;
+
+        auto input_buf = bp::buffer (input_vec);
+        bp::child process (cmd_name, args,
+                            bp::std_out > out_buf,
+                            bp::std_err > err_buf,
+                            bp::std_in < input_buf,
+                            svc);
+        svc.run();
+        process.wait();
+
+        {
+            auto raw = out_buf.get();
+            std::vector<std::string> data;
+            std::string line;
+            bio::stream_buffer<bio::array_source> sb(raw.data(), raw.size());
+            std::istream is(&sb);
+
+            while (std::getline(is, line) && !line.empty())
+                out_vec.push_back (std::move(line));
+
+            raw = err_buf.get();
+            bio::stream_buffer<bio::array_source> eb(raw.data(), raw.size());
+            std::istream es(&eb);
+
+            while (std::getline(es, line) && !line.empty())
+                err_vec.push_back (std::move(line));
+        }
+        m_cmd_result = process.exit_code();
+    }
+    catch (std::exception &e)
+    {
+        m_cmd_result = -1;
+        m_error_msg = e.what();
+    };
+
+    return CmdOutput (std::move(out_vec), std::move(err_vec));
+}
+
+
 /********************************************************************
  * gnc_quotes_get_quotable_commodities
  * list commodities in a given namespace that get price quotes
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 504e62043..792a9d12d 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -31,10 +31,13 @@ extern  "C" {
 #include <qofbook.h>
 }
 
-using QuoteSources = std::vector<std::string>;
+using StrVec = std::vector  <std::string>;
+using QuoteSources = StrVec;
+using CmdOutput = std::pair <StrVec, StrVec>;
 
 const std::string not_found = std::string ("Not Found");
 
+
 class GncQuotes
 {
 public:
@@ -51,8 +54,14 @@ public:
     GList* sources_as_glist ();
 
 private:
-    // Function to check if Finance::Quote is properly installed
+    // Check if Finance::Quote is properly installed
     void check (void);
+    // Run the command specified. Returns two vectors for further processing by the caller
+    // - one with the contents of stdout
+    // - one with the contents of stderr
+    // Will also set m_cmd_result
+    CmdOutput run_cmd (std::string cmd_name, StrVec args, StrVec input_vec);
+
 
     std::string m_version;
     QuoteSources m_sources;

commit 6ce91d7f49691c59320a7e057ea1eaaa70b72188
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Thu Feb 11 15:20:21 2021 +0100

    Drop the single quotes instance code for now
    
    I have been reading on singleton implementations and there appears
    to be a lot of pushback against those.
    We can revisit this if it turns out performance degrades
    significantly by running the F::Q check multiple times.

diff --git a/gnucash/gnucash-cli.cpp b/gnucash/gnucash-cli.cpp
index e22479ffa..d384675a9 100644
--- a/gnucash/gnucash-cli.cpp
+++ b/gnucash/gnucash-cli.cpp
@@ -131,7 +131,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
     {
         if (*m_quotes_cmd == "info")
         {
-            auto quotes = gnc_get_quotes_instance();
+            GncQuotes quotes;
             if (quotes.cmd_result() == 0)
             {
                 std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << "\n";
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index eb1c7c2e2..bb9d778dd 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -82,7 +82,7 @@ scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **ar
     gnc_prefs_init ();
     qof_event_suspend();
 
-    auto quotes = gnc_get_quotes_instance();
+    GncQuotes quotes;
     if (quotes.cmd_result() == 0)
     {
         std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
diff --git a/gnucash/gnucash.cpp b/gnucash/gnucash.cpp
index b90e39a37..544fdeb4c 100644
--- a/gnucash/gnucash.cpp
+++ b/gnucash/gnucash.cpp
@@ -176,7 +176,7 @@ scm_run_gnucash (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **
     /* Install Price Quote Sources */
     auto msg = bl::translate ("Checking Finance::Quote...").str(gnc_get_boost_locale());
 
-    auto quotes = gnc_get_quotes_instance();
+    GncQuotes quotes;
     if (quotes.cmd_result() == 0)
     {
         msg = (bl::format (bl::translate("Found Finance::Quote version {1}.")) % quotes.version()).str(gnc_get_boost_locale());
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 97b4887ad..0d496aa7c 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -51,8 +51,6 @@ namespace bp = boost::process;
 namespace bpt = boost::property_tree;
 namespace bio = boost::iostreams;
 
-static GncQuotes quotes_cached;
-static bool quotes_initialized = false;
 
 CommVec
 gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
@@ -256,19 +254,3 @@ gnc_quotes_get_quotable_commodities (const gnc_commodity_table * table)
     //LEAVE ("list head %p", &l);
     return l;
 }
-
-const GncQuotes& gnc_get_quotes_instance()
-{
-    // The GncQuotes constructor runs check to test if Finance::Quote is properly installed
-    // However due to a race condition the instantiation of the static quotes_cached
-    // may or may not happen before binreloc has run. If binreloc didn't run, this will
-    // try to run gnc-fq-check from the hard-coded install dir. This will fail in all
-    // cases where binreloc is relevant (Windows, macOS or run from builddir).
-    // To catch this, explicitly reinstantiate quotes_cached at first use.
-    if (!quotes_initialized)
-    {
-        quotes_cached = GncQuotes();
-        quotes_initialized = true;
-    }
-    return quotes_cached;
-}
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 8058813a9..504e62043 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -60,6 +60,4 @@ private:
     std::string m_error_msg;
 };
 
-const GncQuotes& gnc_get_quotes_instance (void);
-
 #endif /* GNC_QUOTES_HPP */

commit 616a672d52a61719632b33ea1950fd3eb9e6a360
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Thu Feb 11 15:05:17 2021 +0100

    Rewrite boost::process call to properly capture both stdout and stderr
    
    The previous version of the code could only capture one
    but not both at the same time.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index ee341973e..97b4887ad 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -31,6 +31,9 @@
 #include <boost/process.hpp>
 #include <boost/property_tree/ptree.hpp>
 #include <boost/property_tree/json_parser.hpp>
+#include <boost/iostreams/device/array.hpp>
+#include <boost/iostreams/stream_buffer.hpp>
+#include <boost/asio.hpp>
 #include <glib.h>
 #include "gnc-commodity.hpp"
 #include "gnc-quotes.hpp"
@@ -46,6 +49,7 @@ extern "C" {
 
 namespace bp = boost::process;
 namespace bpt = boost::property_tree;
+namespace bio = boost::iostreams;
 
 static GncQuotes quotes_cached;
 static bool quotes_initialized = false;
@@ -69,24 +73,35 @@ GncQuotes::check (void)
     auto perl_executable = bp::search_path("perl"); //or get it from somewhere else.
     auto fq_check = std::string(gnc_path_get_bindir()) + "/gnc-fq-check";
 
-    bp::ipstream out_stream;
-    bp::ipstream err_stream;
-
     try
     {
-        bp::child process (perl_executable, "-w", fq_check, bp::std_out > out_stream, bp::std_err > err_stream);
-
-        std::string stream_line;
-        while (process.running() && getline (out_stream, stream_line))
-            if (m_version.empty())
-                std::swap (m_version, stream_line);
-            else
-                m_sources.push_back (std::move(stream_line));
-
-        while (process.running() && getline (err_stream, stream_line))
-            m_error_msg.append(stream_line + "\n");
+        std::future<std::vector<char> > output, error;
+        boost::asio::io_service svc;
 
+        bp::child process (perl_executable, "-w", fq_check, bp::std_out > output, bp::std_err > error, svc);
+        svc.run();
         process.wait();
+
+        {
+            auto raw = output.get();
+            std::vector<std::string> data;
+            std::string line;
+            bio::stream_buffer<bio::array_source> sb(raw.data(), raw.size());
+            std::istream is(&sb);
+
+            while (std::getline(is, line) && !line.empty())
+                if (m_version.empty())
+                    std::swap (m_version, line);
+                else
+                    m_sources.push_back (std::move(line));
+
+            raw = error.get();
+            bio::stream_buffer<bio::array_source> eb(raw.data(), raw.size());
+            std::istream es(&eb);
+
+            while (std::getline(es, line) && !line.empty())
+                m_error_msg.append(std::move(line) + "\n");
+        }
         m_cmd_result = process.exit_code();
     }
     catch (std::exception &e)

commit a6771754d5071607fe1f686e5d236bf5e86317a1
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Thu Feb 4 15:10:55 2021 +0100

    GncQuotes - start implementation of fetch/fetch_all

diff --git a/libgnucash/app-utils/CMakeLists.txt b/libgnucash/app-utils/CMakeLists.txt
index 6fcba6f77..3192c5d58 100644
--- a/libgnucash/app-utils/CMakeLists.txt
+++ b/libgnucash/app-utils/CMakeLists.txt
@@ -49,6 +49,7 @@ set(app_utils_ALL_SOURCES ${app_utils_SOURCES} ${app_utils_HEADERS})
 set(app_utils_ALL_LIBRARIES
     gnc-engine
     ${Boost_FILESYSTEM_LIBRARY}
+    ${Boost_PROPERTY_TREE_LIBRARY}
     ${GIO_LDFLAGS}
     ${LIBXML2_LDFLAGS}
     ${LIBXSLT_LDFLAGS}
diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 63c1601fb..ee341973e 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -20,22 +20,32 @@
  * Boston, MA  02110-1301,  USA       gnu at gnu.org                   *
 \ *******************************************************************/
 
+#include <config.h>
+
 #include <algorithm>
 #include <vector>
 #include <string>
 #include <iostream>
+#include <sstream>
 #include <boost/filesystem.hpp>
 #include <boost/process.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/json_parser.hpp>
 #include <glib.h>
+#include "gnc-commodity.hpp"
 #include "gnc-quotes.hpp"
 
 extern "C" {
-    #include "gnc-path.h"
+#include "gnc-commodity.h"
+#include "gnc-path.h"
+#include "gnc-ui-util.h"
 #include <gnc-prefs.h>
 #include <regex.h>
+#include <qofbook.h>
 }
 
 namespace bp = boost::process;
+namespace bpt = boost::property_tree;
 
 static GncQuotes quotes_cached;
 static bool quotes_initialized = false;
@@ -99,6 +109,56 @@ GncQuotes::sources_as_glist()
 }
 
 
+void
+GncQuotes::fetch (const CommVec& commodities)
+{
+    auto dflt_curr = gnc_default_currency();
+    bpt::ptree pt, pt_child;
+    pt.put ("defaultcurrency", gnc_commodity_get_mnemonic (dflt_curr));
+
+    std::for_each (commodities.cbegin(), commodities.cend(),
+        [&pt, &dflt_curr] (auto comm)
+        {
+            auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
+            auto comm_ns = std::string("currency");
+            if (gnc_commodity_is_currency (comm))
+            {
+                if (gnc_commodity_equiv(comm, dflt_curr) ||
+                    (!comm_mnemonic  || (strcmp (comm_mnemonic, "XXX") == 0)))
+                    return;
+            }
+            else
+                comm_ns = gnc_quote_source_get_internal_name (gnc_commodity_get_quote_source (comm));
+
+            auto key = comm_ns + "." + comm_mnemonic;
+            pt.put (key, "");
+        }
+
+    );
+
+    std::ostringstream result;
+    bpt::write_json(result, pt);
+    std::cerr << "GncQuotes fetch_all - resulting json object\n" << result.str() << std::endl;
+
+}
+
+
+void
+GncQuotes::fetch_all (QofBook *book)
+{
+    auto commodities = gnc_quotes_get_quotable_commodities (
+        gnc_commodity_table_get_table (book));
+
+    fetch (commodities);
+}
+
+static const std::vector <std::string>
+format_quotes (const std::vector<gnc_commodity*>)
+{
+    return std::vector <std::string>();
+}
+
+
 /********************************************************************
  * gnc_quotes_get_quotable_commodities
  * list commodities in a given namespace that get price quotes
diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
index 648d335a5..8058813a9 100644
--- a/libgnucash/app-utils/gnc-quotes.hpp
+++ b/libgnucash/app-utils/gnc-quotes.hpp
@@ -24,9 +24,11 @@
 
 #include <string>
 #include <vector>
+#include <gnc-commodity.hpp>  // For CommVec alias
 
 extern  "C" {
 #include <glib.h>
+#include <qofbook.h>
 }
 
 using QuoteSources = std::vector<std::string>;
@@ -36,10 +38,12 @@ const std::string not_found = std::string ("Not Found");
 class GncQuotes
 {
 public:
-    // Constructor - check for presence of Finance::Quote and import version and quote sources
+    // Constructor - checks for presence of Finance::Quote and import version and quote sources
     GncQuotes();
 
-    // Function to check if Finance::Quote is properly installed
+    void fetch_all (QofBook *book);
+    void fetch (const CommVec& commodities);
+
     const int cmd_result() noexcept { return m_cmd_result; }
     const std::string& error_msg() noexcept { return m_error_msg; }
     const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
@@ -47,6 +51,7 @@ public:
     GList* sources_as_glist ();
 
 private:
+    // Function to check if Finance::Quote is properly installed
     void check (void);
 
     std::string m_version;
diff --git a/libgnucash/engine/CMakeLists.txt b/libgnucash/engine/CMakeLists.txt
index 0b494dabb..470fbd34b 100644
--- a/libgnucash/engine/CMakeLists.txt
+++ b/libgnucash/engine/CMakeLists.txt
@@ -53,6 +53,7 @@ set (engine_HEADERS
   gnc-aqbanking-templates.h
   gnc-budget.h
   gnc-commodity.h
+  gnc-commodity.hpp
   gnc-date.h
   gnc-datetime.hpp
   gnc-engine.h
diff --git a/libgnucash/engine/gnc-commodity.hpp b/libgnucash/engine/gnc-commodity.hpp
new file mode 100644
index 000000000..d4590c62c
--- /dev/null
+++ b/libgnucash/engine/gnc-commodity.hpp
@@ -0,0 +1,46 @@
+/**********************************************************************
+ * gnc-commodity.hpp -- API for tradable commodities (incl. currency) *
+ *                                                                    *
+ * 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 Engine
+    @{ */
+/** @addtogroup Commodity Commodities
+
+    @{ */
+/** @file gnc-commodity.hpp
+ *  @brief Commodity handling public routines (C++ api)
+ *  @author Copyright (C) 2021 Geert Janssens
+ */
+
+#ifndef GNC_COMMODITY_HPP
+#define GNC_COMMODITY_HPP
+
+#include <vector>
+
+extern "C" {
+#include <gnc-commodity.h>
+}
+
+using CommVec = std::vector<gnc_commodity*>;
+
+#endif /* GNC_COMMODITY_HPP */
+/** @} */
+/** @} */

commit f3fdc5de12c29d4f91e41c0c548bb3cb105d8be0
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Sat Feb 6 19:34:40 2021 +0100

    Rewrite gnc-fq-helper as finance-quote-wrapper
    
    This rewritten version takes JSON input and spits out JSON.
    Additionally inverted currency quotes will only be flagged.
    The old code also swapped currencies in the result.
    GncQuotes will be written towards these new implementation
    choices.

diff --git a/libgnucash/quotes/CMakeLists.txt b/libgnucash/quotes/CMakeLists.txt
index b33569d39..fcc256f24 100644
--- a/libgnucash/quotes/CMakeLists.txt
+++ b/libgnucash/quotes/CMakeLists.txt
@@ -1,6 +1,6 @@
 
 set(_BIN_FILES "")
-foreach(file gnc-fq-check.in gnc-fq-helper.in gnc-fq-update.in gnc-fq-dump.in)
+foreach(file gnc-fq-check.in gnc-fq-helper.in gnc-fq-update.in gnc-fq-dump.in finance-quote-wrapper.in)
   string(REPLACE ".in" "" _OUTPUT_FILE_NAME ${file})
   set(_ABS_OUTPUT_FILE ${BINDIR_BUILD}/${_OUTPUT_FILE_NAME})
   configure_file( ${file} ${_ABS_OUTPUT_FILE} @ONLY)
@@ -26,4 +26,4 @@ add_custom_target(quotes-bin ALL DEPENDS ${_BIN_FILES})
 install(FILES ${_MAN_FILES} DESTINATION  ${CMAKE_INSTALL_MANDIR}/man1)
 install(PROGRAMS ${_BIN_FILES} DESTINATION ${CMAKE_INSTALL_BINDIR})
 
-set_dist_list(quotes_DIST CMakeLists.txt gnc-fq-check.in gnc-fq-dump.in gnc-fq-helper.in gnc-fq-update.in Quote_example.pl README)
+set_dist_list(quotes_DIST CMakeLists.txt gnc-fq-check.in gnc-fq-dump.in gnc-fq-helper.in gnc-fq-update.in  finance-quote-wrapper.in Quote_example.pl README)
diff --git a/libgnucash/quotes/finance-quote-wrapper.in b/libgnucash/quotes/finance-quote-wrapper.in
new file mode 100755
index 000000000..5089dec74
--- /dev/null
+++ b/libgnucash/quotes/finance-quote-wrapper.in
@@ -0,0 +1,219 @@
+#!@PERL@ -w
+######################################################################
+### finance-quote-wrapper - interface file between gnucash and
+###                         Finanace::Quote. Only intended to be used
+###                         from gnucash code.
+### Based on code taken from gnc-fq-helper.
+### Copyright 2001 Rob Browning <rlb at cs.utexas.edu>
+### Copyright 2021 Geert Janssens
+### 
+### 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
+######################################################################
+
+use strict;
+use English;
+
+=head1 NAME
+
+finance-quote-wrapper  -  internal interface between gnucash and Finance::Quote
+
+=head1 SYNOPSIS
+
+finance-quote-wrapper
+
+=head1 DESCRIPTION
+
+Input: a JSON encoded hash of namespaces and commodities to query prices for.
+Currencies all go under the "currency' namespace, other commodities are
+grouped according to the quotes source they should be queried from
+There should also be a "defaultcurrency" key with the currency to be used as
+base currency for currency quotes.
+
+{
+    "defaultcurrency": "EUR",
+    "currency": {
+        "XAG": "",
+        "HKD": "",
+        "USD": ""
+    },
+    "yahoo_json": {
+        "CSCO": ""
+    }
+}
+
+Output (on standard output):
+
+The retrieved quotes in JSON format for further processing. These are
+the raw values returned by Finance::Quote. The caller is responsible for
+parsing and interpreting the results.
+
+If there are program failures, an error message will be printed on standard error.
+
+Exit status
+
+0 - success
+non-zero - failure
+
+=cut
+
+sub check_modules {
+  my @modules = qw(Finance::Quote JSON::Parse);
+  my @missing;
+
+  foreach my $mod (@modules) {
+    if (eval "require $mod") {
+      $mod->import();
+    }
+    else {
+      push (@missing, $mod);
+    }
+  }
+
+  return unless @missing;
+
+  print STDERR "\n";
+  print STDERR "You need to install the following Perl modules:\n";
+  foreach my $mod (@missing) {
+    print STDERR "  ".$mod."\n";
+  }
+
+  print STDERR "\n";
+  print STDERR "Use your system's package manager to install them,\n";
+  print STDERR "or run 'gnc-fq-update' as root.\n";
+
+  print "missing-lib\n";
+
+  exit 1;
+}
+
+sub sanitize_hash {
+
+    my (%quotehash) = @_;
+    my %newhash;
+
+    my @oldkeys = sort keys %quotehash;
+
+    foreach my $singlekey (@oldkeys) {
+        my ($symbol, $newkey) = split /\x1c/, $singlekey, 2;
+        $newhash{$symbol}{$newkey} = $quotehash{$singlekey};
+    }
+
+    return %newhash;
+}
+
+sub parse_currencies {
+    my($quoter, $currencies, $to_currency) = @_;
+
+    return unless $to_currency;
+
+    my %results;
+    foreach my $from_currency (keys %$currencies) {
+
+        next unless $from_currency;
+
+        my $price = $quoter->currency($from_currency, $to_currency);
+        my $inv_price = undef;
+        my $inverted = 0;
+        # Sometimes price quotes are available in only one direction.
+        unless (defined($price)) {
+            $inv_price = $quoter->currency($to_currency, $from_currency);
+            if (defined($inv_price)) {
+                $price = $inv_price;
+                $inverted = 1;
+            }
+        }
+
+        $results{$from_currency}{"success"} = defined($price);
+        $results{$from_currency}{"inverted"} = $inverted;
+        $results{$from_currency}{"symbol"} = $from_currency;
+        $results{$from_currency}{"currency"} = $to_currency;
+        $results{$from_currency}{"last"} = $price;
+    }
+    return %results;
+}
+
+sub parse_commodities {
+    my($quoter, $quote_method_name, $commodities) = @_;
+
+    my %quote_data = $quoter->fetch($quote_method_name, keys %$commodities);
+    my %normalized_quote_data = sanitize_hash(%quote_data);
+    return %normalized_quote_data;
+}
+
+#---------------------------------------------------------------------------
+# Runtime.
+
+# Check for and load non-standard modules
+check_modules ();
+JSON::Parse->import(qw(valid_json parse_json));
+
+my $json_input = do { local $/; <STDIN> };
+
+if (!valid_json($json_input)) {
+    print STDERR "Could not parse input as valid JSON.\n";
+    print STDERR "Received input:\n$json_input\n";
+    exit 1;
+}
+
+my $requests = parse_json ($json_input);
+
+my $defaultcurrency = $$requests{'defaultcurrency'};
+if (!$defaultcurrency) {
+    $defaultcurrency = "USD";
+    print STDERR "Warning: no default currency was specified, assuming 'USD'\n";
+}
+
+# Create a stockquote object.
+my $quoter = Finance::Quote->new();
+my $prgnam = "gnc-fq-helper";
+
+# Disable default currency conversions.
+$quoter->set_currency();
+
+my $key;
+my $values;
+my %results;
+while (($key, $values) = each %$requests)
+{
+    next if ($key eq "defaultcurrency");
+    if ($key eq "currency")  {
+        my %curr_results = parse_currencies ($quoter, $values, $defaultcurrency, %results);
+        if (%curr_results) {
+            %results = (%results, %curr_results);
+        }
+    }
+    else
+    {
+        my %comm_results = parse_commodities ($quoter, $key, $values, %results);
+        if (%comm_results) {
+            %results = (%results, %comm_results);
+        }
+    }
+}
+
+if (%results) {
+    use JSON;
+    my $jsonval = encode_json \%results;
+    print "$jsonval\n";
+}
+
+STDOUT->flush();
+
+## Local Variables:
+## mode: perl
+## End:

commit 1d94887a0b8aa0bd2b86cb8a8da89fdbcba5159a
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Feb 5 17:48:06 2021 +0100

    Rewrite gnc_commodity_table_get_quotable_commodities as gnc_quotes_get_quotable_commodities
    
    It only makes sense in that context.

diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
index 11bc85993..63c1601fb 100644
--- a/libgnucash/app-utils/gnc-quotes.cpp
+++ b/libgnucash/app-utils/gnc-quotes.cpp
@@ -31,6 +31,8 @@
 
 extern "C" {
     #include "gnc-path.h"
+#include <gnc-prefs.h>
+#include <regex.h>
 }
 
 namespace bp = boost::process;
@@ -38,6 +40,9 @@ namespace bp = boost::process;
 static GncQuotes quotes_cached;
 static bool quotes_initialized = false;
 
+CommVec
+gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
+
 GncQuotes::GncQuotes()
 {
     check();
@@ -94,6 +99,88 @@ GncQuotes::sources_as_glist()
 }
 
 
+/********************************************************************
+ * gnc_quotes_get_quotable_commodities
+ * list commodities in a given namespace that get price quotes
+ ********************************************************************/
+static void
+get_quotables_helper1 (gpointer value, gpointer data)
+{
+    auto l = static_cast<CommVec *> (data);
+    auto comm = static_cast<gnc_commodity *> (value);
+    auto quote_flag = gnc_commodity_get_quote_flag (comm);
+    auto quote_source = gnc_commodity_get_quote_source (comm);
+    auto quote_source_supported = gnc_quote_source_get_supported (quote_source);
+
+    if (!quote_flag ||
+        !quote_source || !quote_source_supported)
+        return;
+    l->push_back (comm);
+}
+
+static gboolean
+get_quotables_helper2 (gnc_commodity *comm, gpointer data)
+{
+    auto l = static_cast<CommVec *> (data);
+    auto quote_flag = gnc_commodity_get_quote_flag (comm);
+    auto quote_source = gnc_commodity_get_quote_source (comm);
+    auto quote_source_supported = gnc_quote_source_get_supported (quote_source);
+
+    if (!quote_flag ||
+        !quote_source || !quote_source_supported)
+        return TRUE;
+    l->push_back (comm);
+    return TRUE;
+}
+
+CommVec
+gnc_quotes_get_quotable_commodities (const gnc_commodity_table * table)
+{
+    gnc_commodity_namespace * ns = NULL;
+    const char *name_space;
+    GList * nslist, * tmp;
+    CommVec l;
+    regex_t pattern;
+    const char *expression = gnc_prefs_get_namespace_regexp ();
+
+    // ENTER("table=%p, expression=%s", table, expression);
+    if (!table)
+        return CommVec ();
+
+    if (expression && *expression)
+    {
+        if (regcomp (&pattern, expression, REG_EXTENDED | REG_ICASE) != 0)
+        {
+            // LEAVE ("Cannot compile regex");
+            return CommVec ();
+        }
+
+        nslist = gnc_commodity_table_get_namespaces (table);
+        for (tmp = nslist; tmp; tmp = tmp->next)
+        {
+            name_space = static_cast<const char *> (tmp->data);
+            if (regexec (&pattern, name_space, 0, NULL, 0) == 0)
+            {
+                // DEBUG ("Running list of %s commodities", name_space);
+                ns = gnc_commodity_table_find_namespace (table, name_space);
+                if (ns)
+                {
+                    auto cm_list = gnc_commodity_namespace_get_commodity_list (ns);
+                    g_list_foreach (cm_list, &get_quotables_helper1, (gpointer) &l);
+                }
+            }
+        }
+        g_list_free (nslist);
+        regfree (&pattern);
+    }
+    else
+    {
+        gnc_commodity_table_foreach_commodity (table, get_quotables_helper2,
+                                               (gpointer) &l);
+    }
+    //LEAVE ("list head %p", &l);
+    return l;
+}
 
 const GncQuotes& gnc_get_quotes_instance()
 {

commit 466db526b2589961958a99485bfa76644a04d2df
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Jan 29 15:01:35 2021 +0100

    Move source files for GncQuotes to app-utils
    
    It will depend on functions in that library. This can probably be
    fixed by cleaning up app-utils, but that's not the topic of
    this feature.

diff --git a/gnucash/CMakeLists.txt b/gnucash/CMakeLists.txt
index 064612857..8e6e339d1 100644
--- a/gnucash/CMakeLists.txt
+++ b/gnucash/CMakeLists.txt
@@ -63,7 +63,7 @@ target_compile_definitions(gnucash PRIVATE -DG_LOG_DOMAIN=\"gnc.bin\")
 
 target_link_libraries (gnucash
    gnc-ledger-core gnc-gnome gnc-gnome-utils gnc-app-utils
-   gnc-engine gnc-module gnc-core-utils gnc-quotes gnucash-guile
+   gnc-engine gnc-module gnc-core-utils gnucash-guile
    gnc-qif-import gnc-csv-import gnc-csv-export gnc-log-replay
    gnc-bi-import gnc-customer-import gnc-report
    PkgConfig::GTK3 ${GUILE_LDFLAGS} ${GLIB2_LDFLAGS}
@@ -98,7 +98,7 @@ target_compile_definitions(gnucash-cli PRIVATE -DG_LOG_DOMAIN=\"gnc.bin\")
 
 target_link_libraries (gnucash-cli
    gnc-gnome-utils gnc-app-utils
-   gnc-engine gnc-core-utils gnc-quotes gnucash-guile gnc-report
+   gnc-engine gnc-core-utils gnucash-guile gnc-report
    ${GUILE_LDFLAGS} ${GLIB2_LDFLAGS}
    ${Boost_LIBRARIES}
 )
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index 824993492..eb1c7c2e2 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -88,7 +88,7 @@ scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **ar
         std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
         auto quote_sources = quotes.sources_as_glist();
         gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
-        g_list_free_full (quote_sources);
+        g_list_free_full (quote_sources, g_free);
     }
     else
     {
diff --git a/libgnucash/app-utils/CMakeLists.txt b/libgnucash/app-utils/CMakeLists.txt
index f1401e11d..6fcba6f77 100644
--- a/libgnucash/app-utils/CMakeLists.txt
+++ b/libgnucash/app-utils/CMakeLists.txt
@@ -17,6 +17,7 @@ set (app_utils_HEADERS
   gnc-gsettings.h
   gnc-help-utils.h
   gnc-prefs-utils.h
+  gnc-quotes.hpp
   gnc-state.h
   gnc-ui-util.h
   gnc-ui-balances.h
@@ -31,6 +32,7 @@ set (app_utils_SOURCES
   gnc-euro.c
   gnc-gsettings.cpp
   gnc-prefs-utils.c
+  gnc-quotes.cpp
   gnc-state.c
   gnc-ui-util.c
   gnc-ui-balances.c
@@ -46,7 +48,7 @@ endif()
 set(app_utils_ALL_SOURCES ${app_utils_SOURCES} ${app_utils_HEADERS})
 set(app_utils_ALL_LIBRARIES
     gnc-engine
-    ${GLIB_LDFLAGS}
+    ${Boost_FILESYSTEM_LIBRARY}
     ${GIO_LDFLAGS}
     ${LIBXML2_LDFLAGS}
     ${LIBXSLT_LDFLAGS}
diff --git a/libgnucash/quotes/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp
similarity index 100%
rename from libgnucash/quotes/gnc-quotes.cpp
rename to libgnucash/app-utils/gnc-quotes.cpp
diff --git a/libgnucash/quotes/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp
similarity index 100%
rename from libgnucash/quotes/gnc-quotes.hpp
rename to libgnucash/app-utils/gnc-quotes.hpp
diff --git a/libgnucash/quotes/CMakeLists.txt b/libgnucash/quotes/CMakeLists.txt
index d855246c6..b33569d39 100644
--- a/libgnucash/quotes/CMakeLists.txt
+++ b/libgnucash/quotes/CMakeLists.txt
@@ -1,54 +1,3 @@
-### libgnc-quotes
-set (quotes_SOURCES
-    gnc-quotes.cpp
-)
-
-# Add dependency on config.h
-set_source_files_properties (${quotes_SOURCES} PROPERTIES OBJECT_DEPENDS ${CONFIG_H})
-
-set(quotes_noinst_HEADERS
-    gnc-quotes.hpp
-)
-
-add_library(gnc-quotes ${quotes_SOURCES} ${quotes_noinst_HEADERS})
-
-add_dependencies(gnc-quotes gnc-vcs-info)
-
-target_include_directories(gnc-quotes
-    PUBLIC
-        ${CMAKE_CURRENT_BINARY_DIR}
-        ${CMAKE_CURRENT_SOURCE_DIR}
-        ${GLIB2_INCLUDE_DIRS}
-    PRIVATE
-        ${GTK_MAC_INCLUDE_DIRS}
-)
-
-target_link_libraries(gnc-quotes
-    PRIVATE
-        gnc-core-utils
-        ${Boost_LIBRARIES}
-        ${GLIB2_LDFLAGS}
-        ${GOBJECT_LDFLAGS}
-        ${GTK_MAC_LDFLAGS}
-        "$<$<BOOL:${MAC_INTEGRATION}>:${OSX_EXTRA_LIBRARIES}>"
-)
-
-target_compile_definitions(gnc-quotes
-    PRIVATE
-        G_LOG_DOMAIN=\"gnc.quotes\"
-        ${GTK_MAC_CFLAGS_OTHER}
-)
-
-target_compile_options(gnc-quotes
-    PRIVATE
-        $<$<BOOL:${MAC_INTEGRATION}>:${OSX_EXTRA_COMPILE_FLAGS}>
-)
-
-install(TARGETS gnc-quotes
-    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
-    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
-    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
-)
 
 set(_BIN_FILES "")
 foreach(file gnc-fq-check.in gnc-fq-helper.in gnc-fq-update.in gnc-fq-dump.in)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index af1caca03..4ab7e5cfd 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -516,6 +516,7 @@ libgnucash/app-utils/gnc-exp-parser.c
 libgnucash/app-utils/gnc-gsettings.cpp
 libgnucash/app-utils/gnc-help-utils.c
 libgnucash/app-utils/gnc-prefs-utils.c
+libgnucash/app-utils/gnc-quotes.cpp
 libgnucash/app-utils/gnc-state.c
 libgnucash/app-utils/gnc-sx-instance-model.c
 libgnucash/app-utils/gnc-ui-balances.c
@@ -672,7 +673,6 @@ libgnucash/engine/Transaction.c
 libgnucash/engine/TransLog.c
 libgnucash/gnc-module/example/gncmod-example.c
 libgnucash/gnc-module/gnc-module.c
-libgnucash/quotes/gnc-quotes.cpp
 libgnucash/tax/de_DE/tax.scm
 libgnucash/tax/de_DE/txf-help.scm
 libgnucash/tax/de_DE/txf.scm

commit 8c4bd86c7b44f257047ca94d6e246ac2c6b1b21e
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Jan 29 19:55:42 2021 +0100

    Fix memory leak as suggested by Christopher Lam

diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index ab69dd2e7..824993492 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -88,7 +88,7 @@ scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **ar
         std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
         auto quote_sources = quotes.sources_as_glist();
         gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
-        g_list_free (quote_sources);
+        g_list_free_full (quote_sources);
     }
     else
     {

commit 9d62755b4a20e61990387fd001fddea60368be9a
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Jan 29 14:45:21 2021 +0100

    Make GncQuotes::check() a private function, returning nothing
    
    At the same time do an explicit reinstantiation of quotes_cached at first use
    to work around what seems to be a race condition between static instantiation
    and binreloc.

diff --git a/libgnucash/quotes/gnc-quotes.cpp b/libgnucash/quotes/gnc-quotes.cpp
index 5c30ea18e..11bc85993 100644
--- a/libgnucash/quotes/gnc-quotes.cpp
+++ b/libgnucash/quotes/gnc-quotes.cpp
@@ -36,8 +36,14 @@ extern "C" {
 namespace bp = boost::process;
 
 static GncQuotes quotes_cached;
+static bool quotes_initialized = false;
 
-bool
+GncQuotes::GncQuotes()
+{
+    check();
+}
+
+void
 GncQuotes::check (void)
 {
     m_version.clear();
@@ -74,12 +80,8 @@ GncQuotes::check (void)
         m_error_msg = e.what();
     };
 
-    auto success = (m_cmd_result == 0);
-
-    if (success)
+    if (m_cmd_result == 0)
         std::sort (m_sources.begin(), m_sources.end());
-
-    return success;
 }
 
 GList*
@@ -95,5 +97,16 @@ GncQuotes::sources_as_glist()
 
 const GncQuotes& gnc_get_quotes_instance()
 {
+    // The GncQuotes constructor runs check to test if Finance::Quote is properly installed
+    // However due to a race condition the instantiation of the static quotes_cached
+    // may or may not happen before binreloc has run. If binreloc didn't run, this will
+    // try to run gnc-fq-check from the hard-coded install dir. This will fail in all
+    // cases where binreloc is relevant (Windows, macOS or run from builddir).
+    // To catch this, explicitly reinstantiate quotes_cached at first use.
+    if (!quotes_initialized)
+    {
+        quotes_cached = GncQuotes();
+        quotes_initialized = true;
+    }
     return quotes_cached;
 }
diff --git a/libgnucash/quotes/gnc-quotes.hpp b/libgnucash/quotes/gnc-quotes.hpp
index 949e9d297..648d335a5 100644
--- a/libgnucash/quotes/gnc-quotes.hpp
+++ b/libgnucash/quotes/gnc-quotes.hpp
@@ -36,10 +36,8 @@ const std::string not_found = std::string ("Not Found");
 class GncQuotes
 {
 public:
-    bool check (void);
-
     // Constructor - check for presence of Finance::Quote and import version and quote sources
-    GncQuotes()  { check(); }
+    GncQuotes();
 
     // Function to check if Finance::Quote is properly installed
     const int cmd_result() noexcept { return m_cmd_result; }
@@ -47,7 +45,10 @@ public:
     const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
     const QuoteSources& sources() noexcept { return m_sources; }
     GList* sources_as_glist ();
+
 private:
+    void check (void);
+
     std::string m_version;
     QuoteSources m_sources;
     int m_cmd_result;

commit 32df095d4f73e1306516b445474af3b3c97adcc0
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Jan 29 12:13:46 2021 +0100

    Catch potential boost::process exceptions
    
    Could happen if the perl executable isn't found and perhaps also
    if there's a stream exception.

diff --git a/libgnucash/quotes/gnc-quotes.cpp b/libgnucash/quotes/gnc-quotes.cpp
index c37596038..5c30ea18e 100644
--- a/libgnucash/quotes/gnc-quotes.cpp
+++ b/libgnucash/quotes/gnc-quotes.cpp
@@ -50,20 +50,29 @@ GncQuotes::check (void)
 
     bp::ipstream out_stream;
     bp::ipstream err_stream;
-    bp::child process (perl_executable, "-w", fq_check, bp::std_out > out_stream, bp::std_err > err_stream);
 
-    std::string stream_line;
-    while (process.running() && getline (out_stream, stream_line))
-        if (m_version.empty())
-            std::swap (m_version, stream_line);
-        else
-            m_sources.push_back (std::move(stream_line));
-
-    while (process.running() && getline (err_stream, stream_line))
-        m_error_msg.append(stream_line + "\n");
-
-    process.wait();
-    m_cmd_result = process.exit_code();
+    try
+    {
+        bp::child process (perl_executable, "-w", fq_check, bp::std_out > out_stream, bp::std_err > err_stream);
+
+        std::string stream_line;
+        while (process.running() && getline (out_stream, stream_line))
+            if (m_version.empty())
+                std::swap (m_version, stream_line);
+            else
+                m_sources.push_back (std::move(stream_line));
+
+        while (process.running() && getline (err_stream, stream_line))
+            m_error_msg.append(stream_line + "\n");
+
+        process.wait();
+        m_cmd_result = process.exit_code();
+    }
+    catch (std::exception &e)
+    {
+        m_cmd_result = -1;
+        m_error_msg = e.what();
+    };
 
     auto success = (m_cmd_result == 0);
 

commit d79306f7db5a0c1bed4402abfe6579029d559365
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Jan 29 12:03:10 2021 +0100

    Protect boost process output read loop from deadlock
    
    As per the boost::process tutorials

diff --git a/libgnucash/quotes/gnc-quotes.cpp b/libgnucash/quotes/gnc-quotes.cpp
index 2868c2fc4..c37596038 100644
--- a/libgnucash/quotes/gnc-quotes.cpp
+++ b/libgnucash/quotes/gnc-quotes.cpp
@@ -53,12 +53,13 @@ GncQuotes::check (void)
     bp::child process (perl_executable, "-w", fq_check, bp::std_out > out_stream, bp::std_err > err_stream);
 
     std::string stream_line;
-    while (getline (out_stream, stream_line))
+    while (process.running() && getline (out_stream, stream_line))
         if (m_version.empty())
             std::swap (m_version, stream_line);
         else
             m_sources.push_back (std::move(stream_line));
-    while (getline (err_stream, stream_line))
+
+    while (process.running() && getline (err_stream, stream_line))
         m_error_msg.append(stream_line + "\n");
 
     process.wait();

commit f658ff409fa455273db91461ad6798fe339f5cef
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Jan 29 11:58:19 2021 +0100

    Tweak line endings in output streams
    
    Flush only at the very end.

diff --git a/gnucash/gnucash-cli.cpp b/gnucash/gnucash-cli.cpp
index 13dab902f..e22479ffa 100644
--- a/gnucash/gnucash-cli.cpp
+++ b/gnucash/gnucash-cli.cpp
@@ -134,7 +134,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
             auto quotes = gnc_get_quotes_instance();
             if (quotes.cmd_result() == 0)
             {
-                std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
+                std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << "\n";
                 std::cout << bl::translate ("Finance::Quote sources: ");
                 for (auto source : quotes.sources())
                     std::cout << source << " ";
@@ -145,7 +145,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
             {
                 std::cerr << bl::translate ("Finance::Quote isn't "
                                             "installed properly.") << "\n";
-                std::cerr << bl::translate ("Error message:") << std::endl;
+                std::cerr << bl::translate ("Error message:") << "\n";
                 std::cerr << quotes.error_msg() << std::endl;
                 return 1;
             }
@@ -156,7 +156,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
             if (!m_file_to_load || m_file_to_load->empty())
             {
                 std::cerr << bl::translate("Missing data file parameter") << "\n\n"
-                << *m_opt_desc_display.get();
+                << *m_opt_desc_display.get() << std::endl;
                 return 1;
             }
             else
@@ -165,7 +165,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
         else
         {
             std::cerr << bl::format (bl::translate("Unknown quotes command '{1}'")) % *m_quotes_cmd << "\n\n"
-                      << *m_opt_desc_display.get();
+                      << *m_opt_desc_display.get() << std::endl;
             return 1;
         }
     }
@@ -177,7 +177,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
             if (!m_file_to_load || m_file_to_load->empty())
             {
                 std::cerr << bl::translate("Missing data file parameter") << "\n\n"
-                          << *m_opt_desc_display.get();
+                          << *m_opt_desc_display.get() << std::endl;
                 return 1;
             }
             else
@@ -201,7 +201,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
             if (!m_report_name || m_report_name->empty())
             {
                 std::cerr << bl::translate("Missing --name parameter") << "\n\n"
-                          << *m_opt_desc_display.get();
+                          << *m_opt_desc_display.get() << std::endl;
                 return 1;
             }
             else
@@ -209,13 +209,13 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
         else
         {
             std::cerr << bl::format (bl::translate("Unknown report command '{1}'")) % *m_report_cmd << "\n\n"
-                      << *m_opt_desc_display.get();
+                      << *m_opt_desc_display.get() << std::endl;
             return 1;
         }
     }
 
     std::cerr << bl::translate("Missing command or option") << "\n\n"
-              << *m_opt_desc_display.get();
+              << *m_opt_desc_display.get() << std::endl;
 
     return 1;
 }
diff --git a/gnucash/gnucash-core-app.cpp b/gnucash/gnucash-core-app.cpp
index 60b05a115..91e28e00d 100644
--- a/gnucash/gnucash-core-app.cpp
+++ b/gnucash/gnucash-core-app.cpp
@@ -244,7 +244,7 @@ Gnucash::CoreApp::parse_command_line (int argc, char **argv)
     catch (std::exception &e)
     {
         std::cerr << e.what() << "\n\n";
-        std::cerr << *m_opt_desc_display.get() << "\n";
+        std::cerr << *m_opt_desc_display.get() << std::endl;
 
         exit(1);
     }
@@ -275,13 +275,13 @@ Gnucash::CoreApp::parse_command_line (int argc, char **argv)
         else
             std::cout << rel_fmt % gnc_version () << "\n";
 
-        std::cout << bl::translate ("Build ID") << ": " << gnc_build_id () << "\n";
+        std::cout << bl::translate ("Build ID") << ": " << gnc_build_id () << std::endl;
         exit(0);
     }
 
     if (m_show_help)
     {
-        std::cout << *m_opt_desc_display.get() << "\n";
+        std::cout << *m_opt_desc_display.get() << std::endl;
         exit(0);
     }
 

commit 8b772384cd30cac87f1f2bb942e9ce42ad229571
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Fri Jan 29 11:57:28 2021 +0100

    Various performance fixes based on feedback
    - const correctnes
    - avoid unnecessary copying
    - avoid need to reverse GList

diff --git a/gnucash/gnucash-cli.cpp b/gnucash/gnucash-cli.cpp
index c161e78c1..13dab902f 100644
--- a/gnucash/gnucash-cli.cpp
+++ b/gnucash/gnucash-cli.cpp
@@ -132,7 +132,7 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
         if (*m_quotes_cmd == "info")
         {
             auto quotes = gnc_get_quotes_instance();
-            if (quotes.check())
+            if (quotes.cmd_result() == 0)
             {
                 std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
                 std::cout << bl::translate ("Finance::Quote sources: ");
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index 2edf8e6ca..ab69dd2e7 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -83,7 +83,7 @@ scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **ar
     qof_event_suspend();
 
     auto quotes = gnc_get_quotes_instance();
-    if (quotes.check())
+    if (quotes.cmd_result() == 0)
     {
         std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
         auto quote_sources = quotes.sources_as_glist();
diff --git a/gnucash/gnucash.cpp b/gnucash/gnucash.cpp
index bcf3feb7a..b90e39a37 100644
--- a/gnucash/gnucash.cpp
+++ b/gnucash/gnucash.cpp
@@ -177,7 +177,7 @@ scm_run_gnucash (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **
     auto msg = bl::translate ("Checking Finance::Quote...").str(gnc_get_boost_locale());
 
     auto quotes = gnc_get_quotes_instance();
-    if (quotes.check())
+    if (quotes.cmd_result() == 0)
     {
         msg = (bl::format (bl::translate("Found Finance::Quote version {1}.")) % quotes.version()).str(gnc_get_boost_locale());
         auto quote_sources = quotes.sources_as_glist();
diff --git a/libgnucash/quotes/gnc-quotes.cpp b/libgnucash/quotes/gnc-quotes.cpp
index 20abcc362..2868c2fc4 100644
--- a/libgnucash/quotes/gnc-quotes.cpp
+++ b/libgnucash/quotes/gnc-quotes.cpp
@@ -55,9 +55,9 @@ GncQuotes::check (void)
     std::string stream_line;
     while (getline (out_stream, stream_line))
         if (m_version.empty())
-            m_version = stream_line;
+            std::swap (m_version, stream_line);
         else
-            m_sources.push_back (stream_line);
+            m_sources.push_back (std::move(stream_line));
     while (getline (err_stream, stream_line))
         m_error_msg.append(stream_line + "\n");
 
@@ -76,15 +76,14 @@ GList*
 GncQuotes::sources_as_glist()
 {
     GList* slist = nullptr;
-    for (auto source : m_sources)
-        slist  = g_list_append (slist, g_strdup(source.c_str()));
+    std::for_each (m_sources.rbegin(), m_sources.rend(),
+                    [&slist](const std::string& source) { slist  = g_list_prepend (slist, g_strdup(source.c_str())); });
     return slist;
 }
 
 
 
-GncQuotes&
-gnc_get_quotes_instance (void)
+const GncQuotes& gnc_get_quotes_instance()
 {
     return quotes_cached;
 }
diff --git a/libgnucash/quotes/gnc-quotes.hpp b/libgnucash/quotes/gnc-quotes.hpp
index 86838a3c9..949e9d297 100644
--- a/libgnucash/quotes/gnc-quotes.hpp
+++ b/libgnucash/quotes/gnc-quotes.hpp
@@ -29,6 +29,10 @@ extern  "C" {
 #include <glib.h>
 }
 
+using QuoteSources = std::vector<std::string>;
+
+const std::string not_found = std::string ("Not Found");
+
 class GncQuotes
 {
 public:
@@ -38,18 +42,18 @@ public:
     GncQuotes()  { check(); }
 
     // Function to check if Finance::Quote is properly installed
-    int cmd_result() { return m_cmd_result; }
-    std::string error_msg() { return m_error_msg; }
-    std::string version() { return m_version.empty() ? "Not Found" : m_version; }
-    std::vector <std::string> sources() { return m_sources; }
+    const int cmd_result() noexcept { return m_cmd_result; }
+    const std::string& error_msg() noexcept { return m_error_msg; }
+    const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
+    const QuoteSources& sources() noexcept { return m_sources; }
     GList* sources_as_glist ();
 private:
     std::string m_version;
-    std::vector <std::string> m_sources;
+    QuoteSources m_sources;
     int m_cmd_result;
     std::string m_error_msg;
 };
 
-GncQuotes& gnc_get_quotes_instance (void);
+const GncQuotes& gnc_get_quotes_instance (void);
 
 #endif /* GNC_QUOTES_HPP */

commit 2f7ed7f25d0a67f972487e215c4fc0845f4b721b
Author: Geert Janssens <geert at kobaltwit.be>
Date:   Thu Jan 28 17:34:19 2021 +0100

    Initial version of libgnc-quotes
    
    This library intends to wrap Finance::Quote the way price-quotes.scm currently does.
    In this first commit the library replaces price-quotes.scm's library to install
    quote sources. In addition it exposes a new command line parameter  in gnucash-cli
    to show which version of Finance::Quote is installed on the system (if any) and
    which quotes sources that verions exposes.

diff --git a/gnucash/CMakeLists.txt b/gnucash/CMakeLists.txt
index 8e6e339d1..064612857 100644
--- a/gnucash/CMakeLists.txt
+++ b/gnucash/CMakeLists.txt
@@ -63,7 +63,7 @@ target_compile_definitions(gnucash PRIVATE -DG_LOG_DOMAIN=\"gnc.bin\")
 
 target_link_libraries (gnucash
    gnc-ledger-core gnc-gnome gnc-gnome-utils gnc-app-utils
-   gnc-engine gnc-module gnc-core-utils gnucash-guile
+   gnc-engine gnc-module gnc-core-utils gnc-quotes gnucash-guile
    gnc-qif-import gnc-csv-import gnc-csv-export gnc-log-replay
    gnc-bi-import gnc-customer-import gnc-report
    PkgConfig::GTK3 ${GUILE_LDFLAGS} ${GLIB2_LDFLAGS}
@@ -98,7 +98,7 @@ target_compile_definitions(gnucash-cli PRIVATE -DG_LOG_DOMAIN=\"gnc.bin\")
 
 target_link_libraries (gnucash-cli
    gnc-gnome-utils gnc-app-utils
-   gnc-engine gnc-core-utils gnucash-guile gnc-report
+   gnc-engine gnc-core-utils gnc-quotes gnucash-guile gnc-report
    ${GUILE_LDFLAGS} ${GLIB2_LDFLAGS}
    ${Boost_LIBRARIES}
 )
diff --git a/gnucash/gnucash-cli.cpp b/gnucash/gnucash-cli.cpp
index a679b5af6..c161e78c1 100644
--- a/gnucash/gnucash-cli.cpp
+++ b/gnucash/gnucash-cli.cpp
@@ -42,6 +42,7 @@ extern "C" {
 #include <boost/nowide/args.hpp>
 #endif
 #include <iostream>
+#include <gnc-quotes.hpp>
 
 namespace bl = boost::locale;
 
@@ -94,7 +95,8 @@ Gnucash::GnucashCli::configure_program_options (void)
     bpo::options_description quotes_options(_("Price Quotes Retrieval Options"));
     quotes_options.add_options()
     ("quotes,Q", bpo::value (&m_quotes_cmd),
-     _("Execute price quote related commands. Currently only one command is supported.\n\n"
+     _("Execute price quote related commands. The following commands are supported.\n\n"
+       "  info: \tShow Finance::Quote version and exposed quote sources.\n"
        "  get: \tFetch current quotes for all foreign currencies and stocks in the given GnuCash datafile.\n"))
     ("namespace", bpo::value (&m_namespace),
      _("Regular expression determining which namespace commodities will be retrieved for"));
@@ -127,21 +129,45 @@ Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **a
 
     if (m_quotes_cmd)
     {
-        if (*m_quotes_cmd != "get")
+        if (*m_quotes_cmd == "info")
         {
-            std::cerr << bl::format (bl::translate("Unknown quotes command '{1}'")) % *m_quotes_cmd << "\n\n"
-            << *m_opt_desc_display.get();
-            return 1;
+            auto quotes = gnc_get_quotes_instance();
+            if (quotes.check())
+            {
+                std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
+                std::cout << bl::translate ("Finance::Quote sources: ");
+                for (auto source : quotes.sources())
+                    std::cout << source << " ";
+                std::cout << std::endl;
+                return 0;
+            }
+            else
+            {
+                std::cerr << bl::translate ("Finance::Quote isn't "
+                                            "installed properly.") << "\n";
+                std::cerr << bl::translate ("Error message:") << std::endl;
+                std::cerr << quotes.error_msg() << std::endl;
+                return 1;
+            }
         }
+        else if (*m_quotes_cmd == "get")
+        {
 
-        if (!m_file_to_load || m_file_to_load->empty())
+            if (!m_file_to_load || m_file_to_load->empty())
+            {
+                std::cerr << bl::translate("Missing data file parameter") << "\n\n"
+                << *m_opt_desc_display.get();
+                return 1;
+            }
+            else
+                return Gnucash::add_quotes (m_file_to_load);
+        }
+        else
         {
-            std::cerr << bl::translate("Missing data file parameter") << "\n\n"
+            std::cerr << bl::format (bl::translate("Unknown quotes command '{1}'")) % *m_quotes_cmd << "\n\n"
                       << *m_opt_desc_display.get();
             return 1;
         }
-        else
-            return Gnucash::add_quotes (m_file_to_load);
     }
 
     if (m_report_cmd)
diff --git a/gnucash/gnucash-commands.cpp b/gnucash/gnucash-commands.cpp
index 646098784..2edf8e6ca 100644
--- a/gnucash/gnucash-commands.cpp
+++ b/gnucash/gnucash-commands.cpp
@@ -45,6 +45,7 @@ extern "C" {
 #include <fstream>
 #include <iostream>
 #include <gnc-report.h>
+#include <gnc-quotes.hpp>
 
 namespace bl = boost::locale;
 
@@ -78,22 +79,30 @@ scm_add_quotes(void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **ar
 {
     auto add_quotes_file = static_cast<const std::string*>(data);
 
-    scm_c_eval_string("(debug-set! stack 200000)");
-
-    auto mod = scm_c_resolve_module("gnucash price-quotes");
-    scm_set_current_module(mod);
-
     gnc_prefs_init ();
     qof_event_suspend();
-    scm_c_eval_string("(gnc:price-quotes-install-sources)");
 
-    if (!gnc_quote_source_fq_installed())
+    auto quotes = gnc_get_quotes_instance();
+    if (quotes.check())
+    {
+        std::cout << bl::format (bl::translate ("Found Finance::Quote version {1}.")) % quotes.version() << std::endl;
+        auto quote_sources = quotes.sources_as_glist();
+        gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
+        g_list_free (quote_sources);
+    }
+    else
     {
         std::cerr << bl::translate ("No quotes retrieved. Finance::Quote isn't "
                                     "installed properly.") << "\n";
-        scm_cleanup_and_exit_with_failure (nullptr);
+        std::cerr << bl::translate ("Error message:") << std::endl;
+        std::cerr << quotes.error_msg() << std::endl;
     }
 
+    scm_c_eval_string("(debug-set! stack 200000)");
+
+    auto mod = scm_c_resolve_module("gnucash price-quotes");
+    scm_set_current_module(mod);
+
     auto add_quotes = scm_c_eval_string("gnc:book-add-quotes");
     auto session = gnc_get_current_session();
     if (!session)
diff --git a/gnucash/gnucash.cpp b/gnucash/gnucash.cpp
index abd4ac160..bcf3feb7a 100644
--- a/gnucash/gnucash.cpp
+++ b/gnucash/gnucash.cpp
@@ -69,6 +69,7 @@ extern "C" {
 #include <iostream>
 #include <gnc-report.h>
 #include <gnc-locale-utils.hpp>
+#include <gnc-quotes.hpp>
 
 namespace bl = boost::locale;
 
@@ -174,9 +175,24 @@ scm_run_gnucash (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **
 
     /* Install Price Quote Sources */
     auto msg = bl::translate ("Checking Finance::Quote...").str(gnc_get_boost_locale());
+
+    auto quotes = gnc_get_quotes_instance();
+    if (quotes.check())
+    {
+        msg = (bl::format (bl::translate("Found Finance::Quote version {1}.")) % quotes.version()).str(gnc_get_boost_locale());
+        auto quote_sources = quotes.sources_as_glist();
+        gnc_quote_source_set_fq_installed (quotes.version().c_str(), quote_sources);
+        g_list_free (quote_sources);
+        scm_c_use_module("gnucash price-quotes");
+    }
+    else
+    {
+        msg = bl::translate("Unable to load Finance::Quote.").str(gnc_get_boost_locale());
+        PINFO ("Attempt to load Finance::Quote returned this error message:\n");
+        PINFO ("%s", quotes.error_msg().c_str());
+    }
+
     gnc_update_splash_screen (msg.c_str(), GNC_SPLASH_PERCENTAGE_UNKNOWN);
-    scm_c_use_module("gnucash price-quotes");
-    scm_c_eval_string("(gnc:price-quotes-install-sources)");
 
     gnc_hook_run(HOOK_STARTUP, NULL);
 
diff --git a/gnucash/price-quotes.scm b/gnucash/price-quotes.scm
index 8e3ff255f..511c62981 100644
--- a/gnucash/price-quotes.scm
+++ b/gnucash/price-quotes.scm
@@ -23,7 +23,6 @@
 (define-module (gnucash price-quotes))
 
 (export gnc:book-add-quotes) ;; called from gnome/dialog-price-edit-db.c
-(export gnc:price-quotes-install-sources)
 
 (use-modules (gnucash engine))
 (use-modules (gnucash utilities))
@@ -33,35 +32,6 @@
 (use-modules (srfi srfi-11)
              (srfi srfi-1))
 
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(define gnc:*finance-quote-check*
-  (string-append (gnc-path-get-bindir) "/gnc-fq-check"))
-
-(define (gnc:fq-check-sources)
-  (let ((program #f))
-
-    (define (start-program)
-      (set! program
-        (gnc-spawn-process-async
-         (list "perl" "-w" gnc:*finance-quote-check*) #t)))
-
-    (define (get-sources)
-      (when program
-        (catch #t
-          (lambda ()
-            (let ((results (read (fdes->inport (gnc-process-get-fd program 1)))))
-              (gnc:debug "gnc:fq-check-sources results: " results)
-              results))
-          (lambda (key . args) key))))
-
-    (define (kill-program)
-      (when program
-        (gnc-detach-process program #t)
-        (set! program #f)))
-
-    (dynamic-wind start-program get-sources kill-program)))
-
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;
 ;; Finance::Quote based instantaneous quotes -- used by the
@@ -532,13 +502,3 @@ Run 'gnc-fq-update' as root to install them.")))
 
         (when keep-going?
           (book-add-prices! book (filter (negate string?) prices)))))))
-
-(define (gnc:price-quotes-install-sources)
-  (let ((sources (gnc:fq-check-sources)))
-    (cond
-     ((list? sources)
-      ;; Translators: ~A is the version string
-      (format #t (G_ "Found Finance::Quote version ~A.") (car sources))
-      (newline)
-      (gnc:msg "Found Finance::Quote version " (car sources))
-      (gnc-quote-source-set-fq-installed (car sources) (cdr sources))))))
diff --git a/libgnucash/quotes/CMakeLists.txt b/libgnucash/quotes/CMakeLists.txt
index b33569d39..d855246c6 100644
--- a/libgnucash/quotes/CMakeLists.txt
+++ b/libgnucash/quotes/CMakeLists.txt
@@ -1,3 +1,54 @@
+### libgnc-quotes
+set (quotes_SOURCES
+    gnc-quotes.cpp
+)
+
+# Add dependency on config.h
+set_source_files_properties (${quotes_SOURCES} PROPERTIES OBJECT_DEPENDS ${CONFIG_H})
+
+set(quotes_noinst_HEADERS
+    gnc-quotes.hpp
+)
+
+add_library(gnc-quotes ${quotes_SOURCES} ${quotes_noinst_HEADERS})
+
+add_dependencies(gnc-quotes gnc-vcs-info)
+
+target_include_directories(gnc-quotes
+    PUBLIC
+        ${CMAKE_CURRENT_BINARY_DIR}
+        ${CMAKE_CURRENT_SOURCE_DIR}
+        ${GLIB2_INCLUDE_DIRS}
+    PRIVATE
+        ${GTK_MAC_INCLUDE_DIRS}
+)
+
+target_link_libraries(gnc-quotes
+    PRIVATE
+        gnc-core-utils
+        ${Boost_LIBRARIES}
+        ${GLIB2_LDFLAGS}
+        ${GOBJECT_LDFLAGS}
+        ${GTK_MAC_LDFLAGS}
+        "$<$<BOOL:${MAC_INTEGRATION}>:${OSX_EXTRA_LIBRARIES}>"
+)
+
+target_compile_definitions(gnc-quotes
+    PRIVATE
+        G_LOG_DOMAIN=\"gnc.quotes\"
+        ${GTK_MAC_CFLAGS_OTHER}
+)
+
+target_compile_options(gnc-quotes
+    PRIVATE
+        $<$<BOOL:${MAC_INTEGRATION}>:${OSX_EXTRA_COMPILE_FLAGS}>
+)
+
+install(TARGETS gnc-quotes
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+)
 
 set(_BIN_FILES "")
 foreach(file gnc-fq-check.in gnc-fq-helper.in gnc-fq-update.in gnc-fq-dump.in)
diff --git a/libgnucash/quotes/gnc-fq-check.in b/libgnucash/quotes/gnc-fq-check.in
index 6b76829df..8eb768458 100755
--- a/libgnucash/quotes/gnc-fq-check.in
+++ b/libgnucash/quotes/gnc-fq-check.in
@@ -91,12 +91,12 @@ check_modules ();
 my $quoter = Finance::Quote->new();
 my $prgnam = "gnc-fq-check";
 
+print "$Finance::Quote::VERSION\n";
 my @qsources;
 my @sources = $quoter->sources();
 foreach my $source (@sources) {
-  push(@qsources, "\"$source\"");
+  print "$source\n";
 }
-printf "(\"%s\" %s)\n", $Finance::Quote::VERSION, join(" ", sort(@qsources));
 
 ## Local Variables:
 ## mode: perl
diff --git a/libgnucash/quotes/gnc-quotes.cpp b/libgnucash/quotes/gnc-quotes.cpp
new file mode 100644
index 000000000..20abcc362
--- /dev/null
+++ b/libgnucash/quotes/gnc-quotes.cpp
@@ -0,0 +1,90 @@
+/********************************************************************\
+ * gnc-quotes.hpp -- proxy for Finance::Quote                       *
+ * Copyright (C) 2021 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   *
+ * 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                   *
+\ *******************************************************************/
+
+#include <algorithm>
+#include <vector>
+#include <string>
+#include <iostream>
+#include <boost/filesystem.hpp>
+#include <boost/process.hpp>
+#include <glib.h>
+#include "gnc-quotes.hpp"
+
+extern "C" {
+    #include "gnc-path.h"
+}
+
+namespace bp = boost::process;
+
+static GncQuotes quotes_cached;
+
+bool
+GncQuotes::check (void)
+{
+    m_version.clear();
+    m_sources.clear();
+    m_error_msg.clear();
+    m_cmd_result  = 0;
+
+    auto perl_executable = bp::search_path("perl"); //or get it from somewhere else.
+    auto fq_check = std::string(gnc_path_get_bindir()) + "/gnc-fq-check";
+
+    bp::ipstream out_stream;
+    bp::ipstream err_stream;
+    bp::child process (perl_executable, "-w", fq_check, bp::std_out > out_stream, bp::std_err > err_stream);
+
+    std::string stream_line;
+    while (getline (out_stream, stream_line))
+        if (m_version.empty())
+            m_version = stream_line;
+        else
+            m_sources.push_back (stream_line);
+    while (getline (err_stream, stream_line))
+        m_error_msg.append(stream_line + "\n");
+
+    process.wait();
+    m_cmd_result = process.exit_code();
+
+    auto success = (m_cmd_result == 0);
+
+    if (success)
+        std::sort (m_sources.begin(), m_sources.end());
+
+    return success;
+}
+
+GList*
+GncQuotes::sources_as_glist()
+{
+    GList* slist = nullptr;
+    for (auto source : m_sources)
+        slist  = g_list_append (slist, g_strdup(source.c_str()));
+    return slist;
+}
+
+
+
+GncQuotes&
+gnc_get_quotes_instance (void)
+{
+    return quotes_cached;
+}
diff --git a/libgnucash/quotes/gnc-quotes.hpp b/libgnucash/quotes/gnc-quotes.hpp
new file mode 100644
index 000000000..86838a3c9
--- /dev/null
+++ b/libgnucash/quotes/gnc-quotes.hpp
@@ -0,0 +1,55 @@
+/********************************************************************\
+ * gnc-quotes.hpp -- proxy for Finance::Quote                       *
+ * Copyright (C) 2021 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   *
+ * 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                   *
+\********************************************************************/
+#ifndef GNC_QUOTES_HPP
+#define GNC_QUOTES_HPP
+
+#include <string>
+#include <vector>
+
+extern  "C" {
+#include <glib.h>
+}
+
+class GncQuotes
+{
+public:
+    bool check (void);
+
+    // Constructor - check for presence of Finance::Quote and import version and quote sources
+    GncQuotes()  { check(); }
+
+    // Function to check if Finance::Quote is properly installed
+    int cmd_result() { return m_cmd_result; }
+    std::string error_msg() { return m_error_msg; }
+    std::string version() { return m_version.empty() ? "Not Found" : m_version; }
+    std::vector <std::string> sources() { return m_sources; }
+    GList* sources_as_glist ();
+private:
+    std::string m_version;
+    std::vector <std::string> m_sources;
+    int m_cmd_result;
+    std::string m_error_msg;
+};
+
+GncQuotes& gnc_get_quotes_instance (void);
+
+#endif /* GNC_QUOTES_HPP */
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 04fcede2e..af1caca03 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -672,6 +672,7 @@ libgnucash/engine/Transaction.c
 libgnucash/engine/TransLog.c
 libgnucash/gnc-module/example/gncmod-example.c
 libgnucash/gnc-module/gnc-module.c
+libgnucash/quotes/gnc-quotes.cpp
 libgnucash/tax/de_DE/tax.scm
 libgnucash/tax/de_DE/txf-help.scm
 libgnucash/tax/de_DE/txf.scm

commit b0ae402c23dedc3914aa4a2a31214b48e9780679
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Sep 17 15:47:05 2022 -0700

    Limit instantiation of GncInt128 constructors to integral values.
    
    Instead of using static_assert. This prevents the compiler from even
    trying and avoids weird compilation errors when testing types for
    instantiating other templates.

diff --git a/libgnucash/engine/gnc-int128.hpp b/libgnucash/engine/gnc-int128.hpp
index 67c88d64a..db463fad8 100644
--- a/libgnucash/engine/gnc-int128.hpp
+++ b/libgnucash/engine/gnc-int128.hpp
@@ -90,38 +90,25 @@ enum // Values for m_flags
  */
 /** Default constructor. Makes 0. */
     GncInt128();
-    template <typename T>
+    template <typename T,
+              std::enable_if_t<std::is_integral<T>::value, bool> = true>
     GncInt128(T lower) : GncInt128(INT64_C(0), static_cast<int64_t>(lower))
-    {
-        static_assert (std::is_integral<T>(),
-                       "GncInt128 can be constructed only with "
-                       "integral arguments.");
-    }
+    {}
     GncInt128(uint64_t lower) : GncInt128 {UINT64_C(0), lower} {}
 /** Double-integer constructor template.
  */
-    template <typename T, typename U>
+    template <typename T, typename U,
+              std::enable_if_t<(std::is_integral<T>::value &&
+              std::is_integral<U>::value), bool> = true>
     GncInt128(T upper, U lower, unsigned char flags = '\0') :
         GncInt128 {static_cast<int64_t>(upper),
-                   static_cast<int64_t>(lower), flags}
-    {
-        static_assert (std::is_integral<T>(),
-                       "GncInt128 can be constructed only with "
-                       "integral arguments.");
-        static_assert (std::is_integral<U>(),
-                       "GncInt128 can be constructed only with "
-                       "integral arguments.");
-    }
+                   static_cast<int64_t>(lower), flags} {}
 
     GncInt128 (int64_t upper, int64_t lower, unsigned char flags = '\0');
-    template <typename T>
+    template <typename T,
+              std::enable_if_t<std::is_integral<T>::value, bool> = true>
     GncInt128(T upper, uint64_t lower) :
-        GncInt128 {static_cast<int64_t>(upper), lower}
-        {
-            static_assert (std::is_integral<T>(),
-                           "GncInt128 can be constructed only with "
-                           "integral arguments.");
-        }
+        GncInt128 {static_cast<int64_t>(upper), lower} {}
 
     GncInt128 (int64_t upper, uint64_t lower, unsigned char flags = '\0');
     GncInt128 (uint64_t upper, uint64_t lower, unsigned char flags = '\0');

commit 3c75d212abc1e95b8647c191be7acb1088f9ad3f
Author: John Ralls <jralls at ceridwen.us>
Date:   Wed Sep 14 17:39:21 2022 -0700

    Fix build on Apple Silicon or maybe Apple-clang-14.0
    
    The compiler complains that there's no matching
    gnc_register_number_range_option for GncOptionDB*, which without this
    commit is true because the explicit templates are for GncOptionDBPtr&.
    Note that the original template definition is for GncOptionDB* and
    that the header-defined inlines that take GncOptionDBPtr& call the
    GncOptionDB* version.

diff --git a/libgnucash/engine/gnc-optiondb.cpp b/libgnucash/engine/gnc-optiondb.cpp
index 0a2d82caf..3d1decc6c 100644
--- a/libgnucash/engine/gnc-optiondb.cpp
+++ b/libgnucash/engine/gnc-optiondb.cpp
@@ -1312,11 +1312,11 @@ gnc_option_db_lookup_qofinstance_value(GncOptionDB* odb, const char* section,
 }
 
 // Force creation of templates
-template void gnc_register_number_range_option(GncOptionDBPtr& db,
+template void gnc_register_number_range_option(GncOptionDB* db,
                                       const char* section, const char* name,
                                       const char* key, const char* doc_string,
                                       int value, int min, int max, int step);
-template void gnc_register_number_range_option(GncOptionDBPtr& db,
+template void gnc_register_number_range_option(GncOptionDB* db,
                                       const char* section, const char* name,
                                       const char* key, const char* doc_string,
                                       double value, double min,



Summary of changes:
 CMakeLists.txt                                     |    8 +
 bindings/app-utils.i                               |   16 -
 bindings/core-utils.i                              |    5 -
 bindings/engine.i                                  |   40 +-
 bindings/guile/glib-guile.c                        |  136 ---
 bindings/guile/glib-guile.h                        |   36 -
 bindings/guile/gnc-engine-guile.cpp                |    1 -
 bindings/guile/gnc-helpers.c                       |   41 -
 bindings/guile/gnc-helpers.h                       |   12 -
 bindings/guile/utilities.scm                       |    8 +-
 bindings/python/example_scripts/priceDB_test.py    |    2 +-
 .../example_scripts/price_database_example.py      |    2 +-
 common/config.h.cmake.in                           |    3 +
 gnucash/CMakeLists.txt                             |    7 +-
 gnucash/gnome-utils/CMakeLists.txt                 |    2 +-
 .../{dialog-transfer.c => dialog-transfer.cpp}     |  223 ++---
 gnucash/gnome/CMakeLists.txt                       |    4 +-
 ...og-price-edit-db.c => dialog-price-edit-db.cpp} |  280 +++---
 gnucash/gnucash-cli.cpp                            |   67 +-
 gnucash/gnucash-commands.cpp                       |  136 ++-
 gnucash/gnucash-commands.hpp                       |    6 +
 gnucash/gnucash-core-app.cpp                       |    6 +-
 gnucash/gnucash.cpp                                |   23 +-
 gnucash/price-quotes.scm                           |  544 ----------
 libgnucash/app-utils/CMakeLists.txt                |    6 +-
 libgnucash/app-utils/gnc-quotes.cpp                | 1059 ++++++++++++++++++++
 libgnucash/app-utils/gnc-quotes.hpp                |  146 +++
 libgnucash/app-utils/test/CMakeLists.txt           |   19 +
 libgnucash/app-utils/test/gtest-gnc-quotes.cpp     |  411 ++++++++
 libgnucash/core-utils/gnc-glib-utils.c             |   40 -
 libgnucash/core-utils/gnc-glib-utils.h             |   21 -
 libgnucash/engine/CMakeLists.txt                   |    1 +
 .../engine/gnc-commodity.hpp                       |   69 +-
 libgnucash/engine/gnc-pricedb.c                    |    1 -
 libgnucash/quotes/CMakeLists.txt                   |    6 +-
 libgnucash/quotes/Quote_example.pl                 |   90 --
 libgnucash/quotes/README                           |   20 +-
 libgnucash/quotes/finance-quote-wrapper.in         |  286 ++++++
 libgnucash/quotes/gnc-fq-check.in                  |  103 --
 libgnucash/quotes/gnc-fq-dump.in                   |  242 -----
 libgnucash/quotes/gnc-fq-helper.in                 |  435 --------
 libgnucash/quotes/gnc-fq-update.in                 |    1 -
 po/POTFILES.in                                     |    6 +-
 43 files changed, 2373 insertions(+), 2197 deletions(-)
 rename gnucash/gnome-utils/{dialog-transfer.c => dialog-transfer.cpp} (94%)
 rename gnucash/gnome/{dialog-price-edit-db.c => dialog-price-edit-db.cpp} (77%)
 delete mode 100644 gnucash/price-quotes.scm
 create mode 100644 libgnucash/app-utils/gnc-quotes.cpp
 create mode 100644 libgnucash/app-utils/gnc-quotes.hpp
 create mode 100644 libgnucash/app-utils/test/gtest-gnc-quotes.cpp
 copy gnucash/gnome-utils/dialog-reset-warnings.h => libgnucash/engine/gnc-commodity.hpp (65%)
 delete mode 100755 libgnucash/quotes/Quote_example.pl
 create mode 100755 libgnucash/quotes/finance-quote-wrapper.in
 delete mode 100755 libgnucash/quotes/gnc-fq-check.in
 delete mode 100755 libgnucash/quotes/gnc-fq-dump.in
 delete mode 100755 libgnucash/quotes/gnc-fq-helper.in



More information about the gnucash-changes mailing list