mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
Merge b437380a0f
into 5ce3a9dd1d
This commit is contained in:
commit
8d3c0b1bd7
@ -262,6 +262,41 @@ QofBook * qof_session_get_book (QofSession *session);
|
||||
// TODO: Unroll/remove
|
||||
const char *qof_session_get_url (QofSession *session);
|
||||
|
||||
/* note: copied from qofsession.h -- maintain manually until
|
||||
qofsession can be %included properly */
|
||||
typedef enum
|
||||
{
|
||||
SESSION_NORMAL_OPEN = 0,
|
||||
SESSION_NEW_STORE = 2,
|
||||
SESSION_NEW_OVERWRITE = 3,
|
||||
SESSION_READ_ONLY = 4,
|
||||
SESSION_BREAK_LOCK = 5
|
||||
} SessionOpenMode;
|
||||
|
||||
%inline {
|
||||
static void qof_session_save_quiet ()
|
||||
{
|
||||
QofSession *session = gnc_get_current_session();
|
||||
qof_session_save (session, [](const char* message, double percent){});
|
||||
}
|
||||
|
||||
static bool qof_session_load_quiet (const char *filename, SessionOpenMode mode)
|
||||
{
|
||||
gnc_clear_current_session();
|
||||
QofSession *session = gnc_get_current_session();
|
||||
qof_session_begin (session, filename, mode);
|
||||
auto io_error = qof_session_get_error (session);
|
||||
if (io_error != ERR_BACKEND_NO_ERR)
|
||||
{
|
||||
PWARN ("Loading error: %s", qof_backend_get_error_string (io_error));
|
||||
return false;
|
||||
}
|
||||
qof_session_load (session, [](const char* message, double percent){});
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
%ignore qof_print_date_time_buff;
|
||||
%ignore gnc_tm_free;
|
||||
%newobject qof_print_date;
|
||||
@ -419,6 +454,12 @@ void qof_book_set_string_option(QofBook* book, const char* opt_name, const char*
|
||||
SET_ENUM("HOOK-REPORT");
|
||||
SET_ENUM("HOOK-SAVE-OPTIONS");
|
||||
|
||||
SET_ENUM("SESSION-NORMAL-OPEN");
|
||||
SET_ENUM("SESSION-NEW-STORE");
|
||||
SET_ENUM("SESSION-NEW-OVERWRITE");
|
||||
SET_ENUM("SESSION-READ-ONLY");
|
||||
SET_ENUM("SESSION-BREAK-LOCK");
|
||||
|
||||
//SET_ENUM("GNC-ID-ACCOUNT");
|
||||
SET_ENUM("QOF-ID-BOOK-SCM");
|
||||
//SET_ENUM("GNC-ID-BUDGET");
|
||||
|
62
doc/examples/book-to-hledger.scm
Normal file
62
doc/examples/book-to-hledger.scm
Normal file
@ -0,0 +1,62 @@
|
||||
;; this file is meant to be run via the gnucash-cli interface: --script simple-book-add-txn.scm
|
||||
;;
|
||||
;; gnucash-cli book.gnucash --script simple-book-add-txn.scm
|
||||
;;
|
||||
|
||||
(use-modules (gnucash core-utils))
|
||||
(use-modules (gnucash engine))
|
||||
(use-modules (gnucash app-utils))
|
||||
(use-modules (gnucash report))
|
||||
(use-modules (ice-9 match))
|
||||
|
||||
(define iso-date (qof-date-format-get-string QOF-DATE-FORMAT-ISO))
|
||||
(define book (gnc-get-current-book))
|
||||
(define root (gnc-get-current-root-account))
|
||||
(define query (qof-query-create-for-splits))
|
||||
(qof-query-set-book query (gnc-get-current-book))
|
||||
(xaccQueryAddAccountMatch query (gnc-account-get-descendants root) QOF-GUID-MATCH-ANY QOF-QUERY-AND)
|
||||
(qof-query-set-sort-order
|
||||
query
|
||||
(list SPLIT-TRANS TRANS-DATE-POSTED)
|
||||
'()
|
||||
(list QUERY-DEFAULT-SORT))
|
||||
|
||||
(define (dump-transaction trans)
|
||||
(format #t "~a ~a\n"
|
||||
(gnc-print-time64 (xaccTransGetDate trans) iso-date)
|
||||
(xaccTransGetDescription trans))
|
||||
(define (split->account s)
|
||||
(gnc-account-get-full-name (xaccSplitGetAccount s)))
|
||||
(define (split->amount s)
|
||||
(format #f "~a ~a"
|
||||
(exact->inexact (xaccSplitGetAmount s))
|
||||
(gnc-commodity-get-mnemonic (xaccAccountGetCommodity (xaccSplitGetAccount s)))))
|
||||
(define max-width
|
||||
(let lp ((splits (xaccTransGetSplitList trans)) (maximum 0))
|
||||
(match splits
|
||||
(() (+ maximum 2))
|
||||
((s . rest)
|
||||
(lp rest (max maximum (+ (string-length (split->account s))
|
||||
(string-length (split->amount s)))))))))
|
||||
(for-each
|
||||
(lambda (s)
|
||||
(define txn (xaccSplitGetParent s))
|
||||
(define acc-name (split->account s))
|
||||
(define amt-str (split->amount s))
|
||||
(format #t " ~a~a~a\n"
|
||||
acc-name
|
||||
(make-string (- max-width (string-length acc-name) (string-length amt-str)) #\space)
|
||||
amt-str))
|
||||
(xaccTransGetSplitList trans)))
|
||||
|
||||
(define split-has-no-account? (compose null? xaccSplitGetAccount))
|
||||
|
||||
(let lp ((splits (xaccQueryGetSplitsUniqueTrans query)))
|
||||
(newline)
|
||||
(match splits
|
||||
(() #f)
|
||||
(((? split-has-no-account?) . rest) (lp rest))
|
||||
((split . rest) (dump-transaction (xaccSplitGetParent split)) (lp rest))))
|
||||
|
||||
(qof-query-destroy query)
|
||||
(gnc-clear-current-session)
|
139
doc/examples/simple-book-add-txn.scm
Normal file
139
doc/examples/simple-book-add-txn.scm
Normal file
@ -0,0 +1,139 @@
|
||||
;; this file is meant to be run via the gnucash-cli interface: --script simple-book-add-txn.scm
|
||||
;;
|
||||
;; gnucash-cli book.gnucash --script simple-book-add-txn.scm
|
||||
;;
|
||||
;; the book will be considered "valid" if it has a basic hierarchy such as the following
|
||||
;; Assets
|
||||
;; |-Current
|
||||
;; | |- Bank
|
||||
;; Expenses
|
||||
;; |-Govt
|
||||
;; | |- Taxes
|
||||
;; |-Personal
|
||||
;; |-Medical
|
||||
|
||||
(use-modules (gnucash core-utils))
|
||||
(use-modules (gnucash engine))
|
||||
(use-modules (gnucash app-utils))
|
||||
(use-modules (gnucash report))
|
||||
(use-modules (ice-9 rdelim))
|
||||
(use-modules (ice-9 match))
|
||||
|
||||
(define (get-line prompt)
|
||||
(format #t "\x1b[1;33m~a:\x1b[m " prompt)
|
||||
(let ((rv (read-line)))
|
||||
(if (eof-object? rv) "" rv)))
|
||||
|
||||
(define (get-amount prompt)
|
||||
(let ((amount (gnc-numeric-from-string (get-line prompt))))
|
||||
(if (number? amount)
|
||||
amount
|
||||
(get-amount prompt))))
|
||||
|
||||
(define (get-item-from-list lst elt->string prompt)
|
||||
(define (get-amount-line) (get-amount prompt))
|
||||
(let lp ((idx 1) (lst lst))
|
||||
(unless (null? lst)
|
||||
(format #t "~a. ~a\n" idx (elt->string (car lst)))
|
||||
(lp (1+ idx) (cdr lst))))
|
||||
(let lp ((idx (get-amount-line)))
|
||||
(cond
|
||||
((and (integer? idx) (positive? idx))
|
||||
(let lp1 ((idx (1- idx)) (lst lst))
|
||||
(cond
|
||||
((null? lst) (lp (get-amount-line)))
|
||||
((zero? idx) (car lst))
|
||||
(else (lp1 (1- idx) (cdr lst))))))
|
||||
(else (lp (get-amount-line))))))
|
||||
|
||||
(define (get-account prompt parent)
|
||||
(define descendants (gnc-account-get-descendants-sorted parent))
|
||||
(get-item-from-list descendants gnc-account-get-full-name "Select account by index"))
|
||||
|
||||
(define (get-binary-response prompt)
|
||||
(match (get-line prompt)
|
||||
((or "Y" "y") #t)
|
||||
((or "N" "n") #f)
|
||||
(else (get-binary-response prompt))))
|
||||
|
||||
(define (add-to-transaction book txn account amount memo)
|
||||
(let ((split (xaccMallocSplit book)))
|
||||
(xaccSplitSetAccount split account)
|
||||
(xaccSplitSetAmount split amount)
|
||||
(xaccSplitSetValue split amount)
|
||||
(xaccSplitSetMemo split memo)
|
||||
(xaccSplitSetParent split txn)))
|
||||
|
||||
(define (quit-program exitlevel)
|
||||
(gnc-clear-current-session)
|
||||
(exit exitlevel))
|
||||
|
||||
(define (get-new-uri session)
|
||||
(define filepath (get-line "please input correct path, or leave blank to abort"))
|
||||
(gnc-clear-current-session)
|
||||
(cond
|
||||
((string-null? filepath) (quit-program 1))
|
||||
((qof-session-load-quiet filepath SESSION-NORMAL-OPEN) #f) ;success
|
||||
(else (get-new-uri session))))
|
||||
|
||||
(define session (gnc-get-current-session))
|
||||
(define root (gnc-get-current-root-account))
|
||||
|
||||
(let check-book-loop ()
|
||||
(cond
|
||||
((or (null? (gnc-account-lookup-by-full-name root "Assets:Current:Bank"))
|
||||
(null? (gnc-account-lookup-by-full-name root "Expenses"))
|
||||
(null? (gnc-account-lookup-by-full-name root "Expenses:Govt:Taxes")))
|
||||
(display "\n\n\nWARNING: It doesn't seem the correct book is loaded.\n")
|
||||
(get-new-uri session)
|
||||
(check-book-loop))))
|
||||
|
||||
(define book (gnc-get-current-book))
|
||||
(define acc-BANK (gnc-account-lookup-by-full-name root "Assets:Current:Bank"))
|
||||
(define acc-EXP (gnc-account-lookup-by-full-name root "Expenses"))
|
||||
(define acc-EXP-TAX (gnc-account-lookup-by-full-name root "Expenses:Govt:Taxes"))
|
||||
(define acc-EXP-LEAF (get-account "Expense leaf account" acc-EXP))
|
||||
|
||||
(define (accounts-action action-fn)
|
||||
(action-fn acc-BANK)
|
||||
(action-fn acc-EXP-LEAF)
|
||||
(action-fn acc-EXP-TAX))
|
||||
|
||||
(define description (get-line "Description"))
|
||||
|
||||
(let lp ()
|
||||
(define txn (xaccMallocTransaction book))
|
||||
(define net-amount (get-amount "Amount, without tax"))
|
||||
(define tax-amount (* net-amount 1/10))
|
||||
(define total-amount (+ tax-amount net-amount))
|
||||
|
||||
(xaccTransBeginEdit txn)
|
||||
(xaccTransSetCurrency txn (xaccAccountGetCommodity acc-BANK))
|
||||
(xaccTransSetDatePostedSecsNormalized txn (current-time))
|
||||
(xaccTransSetDescription txn description)
|
||||
(add-to-transaction book txn acc-BANK (- total-amount) "from bank")
|
||||
(add-to-transaction book txn acc-EXP-LEAF net-amount "expense net")
|
||||
(add-to-transaction book txn acc-EXP-TAX tax-amount "tax paid")
|
||||
(newline)
|
||||
(gnc:dump-transaction txn)
|
||||
|
||||
(cond
|
||||
((not (xaccTransIsBalanced txn))
|
||||
(display "WARNING: transaction is not balanced. Try again.\n")
|
||||
(xaccTransRollbackEdit txn)
|
||||
(xaccTransDestroy txn)
|
||||
(lp))
|
||||
((get-binary-response "Please confirm transaction [YN]")
|
||||
(accounts-action xaccAccountBeginEdit)
|
||||
(xaccTransCommitEdit txn)
|
||||
(accounts-action xaccAccountCommitEdit))
|
||||
(else
|
||||
(xaccTransRollbackEdit txn)
|
||||
(xaccTransDestroy txn))))
|
||||
|
||||
;; (gnc:dump-book)
|
||||
(when (qof-book-session-not-saved book)
|
||||
(display "Saving book...\n")
|
||||
(qof-session-save-quiet))
|
||||
|
||||
(quit-program 0)
|
@ -111,6 +111,7 @@ target_link_libraries (gnucash
|
||||
gnc-bi-import gnc-customer-import gnc-report
|
||||
PkgConfig::GTK3 ${GUILE_LDFLAGS} PkgConfig::GLIB2
|
||||
${Boost_LIBRARIES}
|
||||
${Python3_LIBRARIES}
|
||||
)
|
||||
|
||||
set(gnucash_cli_SOURCES
|
||||
@ -137,15 +138,20 @@ endif()
|
||||
|
||||
add_dependencies (gnucash-cli gnucash)
|
||||
|
||||
target_compile_definitions(gnucash-cli PRIVATE -DG_LOG_DOMAIN=\"gnc.bin\")
|
||||
target_compile_definitions(gnucash-cli PRIVATE
|
||||
-DG_LOG_DOMAIN=\"gnc.bin\"
|
||||
$<$<BOOL:${WITH_PYTHON}>:HAVE_PYTHON_H>)
|
||||
|
||||
target_link_libraries (gnucash-cli
|
||||
gnc-app-utils
|
||||
gnc-engine gnc-core-utils gnucash-guile gnc-report
|
||||
${GUILE_LDFLAGS} PkgConfig::GLIB2
|
||||
${Boost_LIBRARIES}
|
||||
${Python3_LIBRARIES}
|
||||
)
|
||||
|
||||
target_include_directories (gnucash-cli PRIVATE ${Python3_INCLUDE_DIRS})
|
||||
|
||||
if (BUILDING_FROM_VCS)
|
||||
target_compile_definitions(gnucash PRIVATE -DGNC_VCS=\"git\")
|
||||
target_compile_definitions(gnucash-cli PRIVATE -DGNC_VCS=\"git\")
|
||||
|
@ -62,6 +62,12 @@ namespace Gnucash {
|
||||
boost::optional <std::string> m_namespace;
|
||||
bool m_verbose = false;
|
||||
|
||||
boost::optional <std::string> m_script;
|
||||
std::vector<std::string> m_script_args;
|
||||
std::string m_language;
|
||||
bool m_interactive;
|
||||
bool m_open_readwrite;
|
||||
|
||||
boost::optional <std::string> m_report_cmd;
|
||||
boost::optional <std::string> m_report_name;
|
||||
boost::optional <std::string> m_export_type;
|
||||
@ -107,6 +113,17 @@ Gnucash::GnucashCli::configure_program_options (void)
|
||||
m_opt_desc_display->add (quotes_options);
|
||||
m_opt_desc_all.add (quotes_options);
|
||||
|
||||
bpo::options_description cli_options(_("Scripting and/or Interactive Session Options"));
|
||||
cli_options.add_options()
|
||||
("script,S", bpo::value (&m_script), _("Script to run"))
|
||||
("script-args", bpo::value (&m_script_args), _("Script arguments"))
|
||||
("interactive,I", bpo::bool_switch (&m_interactive), _("Interactive session"))
|
||||
("language,L", bpo::value (&m_language)->default_value("guile"), _("Specify language for script or interactive session; guile (default) or python"))
|
||||
("readwrite,W", bpo::bool_switch (&m_open_readwrite), _("Open datafile read-write for script and/or interactive session"));
|
||||
m_pos_opt_desc.add("script-args", -1);
|
||||
m_opt_desc_display->add (cli_options);
|
||||
m_opt_desc_all.add (cli_options);
|
||||
|
||||
bpo::options_description report_options(_("Report Generation Options"));
|
||||
report_options.add_options()
|
||||
("report,R", bpo::value (&m_report_cmd),
|
||||
@ -127,10 +144,32 @@ may be specified to describe some saved options.\n"
|
||||
}
|
||||
|
||||
int
|
||||
Gnucash::GnucashCli::start ([[maybe_unused]] int argc, [[maybe_unused]] char **argv)
|
||||
Gnucash::GnucashCli::start (int argc, char **argv)
|
||||
{
|
||||
Gnucash::CoreApp::start();
|
||||
|
||||
if (m_interactive || m_script)
|
||||
{
|
||||
std::vector<const char*> newArgv =
|
||||
{ argc ? argv[0] : "", m_file_to_load ? m_file_to_load->c_str() : ""};
|
||||
std::transform (m_script_args.begin(), m_script_args.end(), std::back_inserter(newArgv),
|
||||
[](const std::string& s) { return s.c_str(); });
|
||||
// note the vector<const char*> is valid as long as script_args's strings are not damaged!
|
||||
|
||||
std::cout << "\n\nScript args:";
|
||||
for (const auto& arg : newArgv)
|
||||
std::cout << ' ' << arg;
|
||||
std::cout << '\n';
|
||||
std::cout << "File to load: " << (m_file_to_load ? *m_file_to_load : "(null)") << std::endl;
|
||||
std::cout << "Language: " << m_language << std::endl;
|
||||
std::cout << "Script: " << (m_script ? *m_script : "(null)") << std::endl;
|
||||
std::cout << "Readwrite: " << (m_open_readwrite ? 'Y' : 'N') << std::endl;
|
||||
std::cout << "Interactive: " << (m_interactive ? 'Y' : 'N') << "\n\n" << std::endl;
|
||||
|
||||
return Gnucash::run_scripting (newArgv, m_file_to_load, m_language, m_script,
|
||||
m_open_readwrite, m_interactive);
|
||||
}
|
||||
|
||||
if (!m_quotes_cmd.empty())
|
||||
{
|
||||
if (m_quotes_cmd.front() == "info")
|
||||
|
@ -31,6 +31,8 @@
|
||||
|
||||
#include "gnucash-commands.hpp"
|
||||
#include "gnucash-core-app.hpp"
|
||||
#include "gnc-ui-util.h"
|
||||
#include "gnc-path.h"
|
||||
|
||||
#include <gnc-filepath-utils.h>
|
||||
#include <gnc-engine-guile.h>
|
||||
@ -40,12 +42,17 @@
|
||||
#include <qoflog.h>
|
||||
|
||||
#include <boost/locale.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <gnc-report.h>
|
||||
#include <gnc-quotes.hpp>
|
||||
|
||||
#ifdef HAVE_PYTHON_H
|
||||
#include <Python.h>
|
||||
#endif
|
||||
|
||||
namespace bl = boost::locale;
|
||||
|
||||
static std::string empty_string{};
|
||||
@ -53,6 +60,38 @@ static std::string empty_string{};
|
||||
/* This static indicates the debugging module that this .o belongs to. */
|
||||
static QofLogModule log_module = GNC_MOD_GUI;
|
||||
|
||||
#ifdef _WIN32
|
||||
struct ConsoleStruct
|
||||
{
|
||||
public:
|
||||
bool has_ansi () { return m_has_ansi; };
|
||||
private:
|
||||
HANDLE m_stdoutHandle = INVALID_HANDLE_VALUE;
|
||||
DWORD m_outModeInit = 0;
|
||||
bool m_has_ansi = false;
|
||||
ConsoleStruct () : m_stdoutHandle {GetStdHandle(STD_OUTPUT_HANDLE)}
|
||||
{
|
||||
if (m_stdoutHandle != INVALID_HANDLE_VALUE && GetConsoleMode(m_stdoutHandle, &m_outModeInit))
|
||||
{
|
||||
SetConsoleMode (m_stdoutHandle, m_outModeInit | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
m_has_ansi = true;
|
||||
}
|
||||
}
|
||||
~ConsoleStruct ()
|
||||
{
|
||||
if (m_stdoutHandle == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
printf ("\x1b[0m");
|
||||
SetConsoleMode (m_stdoutHandle, m_outModeInit);
|
||||
}
|
||||
} console_ansi;
|
||||
#else
|
||||
struct ConsoleStruct
|
||||
{
|
||||
bool has_ansi () { return true; };
|
||||
} console_ansi;
|
||||
#endif
|
||||
|
||||
static int
|
||||
cleanup_and_exit_with_failure (QofSession *session)
|
||||
{
|
||||
@ -99,6 +138,64 @@ report_session_percentage (const char *message, double percent)
|
||||
return;
|
||||
}
|
||||
|
||||
static std::string
|
||||
get_line (const char* prompt)
|
||||
{
|
||||
std::string rv;
|
||||
std::cout << (console_ansi.has_ansi() ? "\x1b[1;33m" : "") << prompt
|
||||
<< (console_ansi.has_ansi() ? "\x1b[m" : "");
|
||||
if (!std::getline (std::cin, rv))
|
||||
gnc_shutdown_cli (1);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static std::string
|
||||
get_choice (const char* prompt, const std::vector<std::string> choices)
|
||||
{
|
||||
std::string response;
|
||||
do
|
||||
{
|
||||
response = get_line (prompt);
|
||||
}
|
||||
while (std::none_of (choices.begin(), choices.end(),
|
||||
[&response](auto& choice) { return choice == response; } ));
|
||||
return response;
|
||||
}
|
||||
|
||||
struct scripting_args
|
||||
{
|
||||
const boost::optional<std::string>& script;
|
||||
bool interactive;
|
||||
};
|
||||
|
||||
// when guile or python script/interactive session succeed
|
||||
static void
|
||||
cleanup_and_exit_with_save ()
|
||||
{
|
||||
if (gnc_current_session_exist())
|
||||
{
|
||||
auto session = gnc_get_current_session();
|
||||
auto book = gnc_get_current_book();
|
||||
auto is_yes = [] (const char* prompt)
|
||||
{
|
||||
auto s = get_choice (prompt, {"Y","y","N","n"});
|
||||
return s == "Y" || s == "y";
|
||||
};
|
||||
|
||||
std::cerr << _("Warning: session was not cleared.") << std::endl;
|
||||
|
||||
if (!qof_book_session_not_saved (book))
|
||||
std::cerr << _("Please don't forget to clear session before shutdown.") << std::endl;
|
||||
else if (qof_book_is_readonly (book))
|
||||
std::cerr << _("Book is readonly. Unsaved changes will be lost.") << std::endl;
|
||||
else if (is_yes (_("There are unsaved changes. Save before clearing [YN]?")))
|
||||
qof_session_save (session, report_session_percentage);
|
||||
|
||||
gnc_clear_current_session ();
|
||||
}
|
||||
gnc_shutdown_cli (0);
|
||||
}
|
||||
|
||||
/* Don't try to use std::string& for the members of the following struct, it
|
||||
* results in the values getting corrupted as it passes through initializing
|
||||
* Scheme when compiled with Clang.
|
||||
@ -124,6 +221,48 @@ write_report_file (const char *html, const char* file)
|
||||
// ofs destructor will close the file
|
||||
}
|
||||
|
||||
static QofSession*
|
||||
load_file (const std::string& file_to_load, bool open_readwrite)
|
||||
{
|
||||
PINFO ("Loading %s %s", file_to_load.c_str(), open_readwrite ? "(r/w)" : "(readonly)");
|
||||
auto session = gnc_get_current_session();
|
||||
if (!session)
|
||||
gnc_shutdown_cli (1);
|
||||
|
||||
auto mode = open_readwrite ? SESSION_NORMAL_OPEN : SESSION_READ_ONLY;
|
||||
while (true)
|
||||
{
|
||||
qof_session_begin (session, file_to_load.c_str(), mode);
|
||||
auto io_err = qof_session_get_error (session);
|
||||
switch (io_err)
|
||||
{
|
||||
case ERR_BACKEND_NO_ERR:
|
||||
qof_session_load (session, report_session_percentage);
|
||||
return session;
|
||||
case ERR_BACKEND_LOCKED:
|
||||
{
|
||||
// Translators: [R] [U] and [A] are responses and must not be translated.
|
||||
auto response = get_choice (_("File Locked. Open [R]eadonly, [U]nlock or [A]bort?"),
|
||||
{"R","r","U","u","A","a"});
|
||||
if (response == "R" || response == "r")
|
||||
mode = SESSION_READ_ONLY;
|
||||
else if (response == "U" || response == "u")
|
||||
mode = SESSION_BREAK_LOCK;
|
||||
else if (response == "A" || response == "a")
|
||||
gnc_shutdown_cli (1);
|
||||
break;
|
||||
}
|
||||
case ERR_BACKEND_READONLY:
|
||||
std::cerr << _("File is readonly. Cannot open read-write") << std::endl;
|
||||
mode = SESSION_READ_ONLY;
|
||||
break;
|
||||
default:
|
||||
std::cerr << _("Unknown error. Abort.") << std::endl;
|
||||
scm_cleanup_and_exit_with_failure (session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
scm_run_report (void *data,
|
||||
[[maybe_unused]] int argc, [[maybe_unused]] char **argv)
|
||||
@ -160,17 +299,7 @@ scm_run_report (void *data,
|
||||
|
||||
PINFO ("Loading datafile %s...\n", datafile);
|
||||
|
||||
auto session = gnc_get_current_session ();
|
||||
if (!session)
|
||||
scm_cleanup_and_exit_with_failure (session);
|
||||
|
||||
qof_session_begin (session, datafile, SESSION_READ_ONLY);
|
||||
if (qof_session_get_error (session) != ERR_BACKEND_NO_ERR)
|
||||
scm_cleanup_and_exit_with_failure (session);
|
||||
|
||||
qof_session_load (session, report_session_percentage);
|
||||
if (qof_session_get_error (session) != ERR_BACKEND_NO_ERR)
|
||||
scm_cleanup_and_exit_with_failure (session);
|
||||
auto session = load_file (args->file_to_load, false);
|
||||
|
||||
if (!args->export_type.empty())
|
||||
{
|
||||
@ -273,14 +402,7 @@ scm_report_show (void *data,
|
||||
{
|
||||
auto datafile = args->file_to_load.c_str();
|
||||
PINFO ("Loading datafile %s...\n", datafile);
|
||||
|
||||
auto session = gnc_get_current_session ();
|
||||
if (session)
|
||||
{
|
||||
qof_session_begin (session, datafile, SESSION_READ_ONLY);
|
||||
if (qof_session_get_error (session) == ERR_BACKEND_NO_ERR)
|
||||
qof_session_load (session, report_session_percentage);
|
||||
}
|
||||
[[maybe_unused]] auto session = load_file (args->file_to_load, false);
|
||||
}
|
||||
|
||||
scm_call_2 (scm_c_eval_string ("gnc:cmdline-report-show"),
|
||||
@ -345,17 +467,7 @@ Gnucash::add_quotes (const bo_str& 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)
|
||||
return cleanup_and_exit_with_failure (session);
|
||||
|
||||
qof_session_load(session, NULL);
|
||||
if (qof_session_get_error(session) != ERR_BACKEND_NO_ERR)
|
||||
return cleanup_and_exit_with_failure (session);
|
||||
auto session = load_file (*uri, true);
|
||||
|
||||
try
|
||||
{
|
||||
@ -433,3 +545,170 @@ Gnucash::report_list (void)
|
||||
scm_boot_guile (0, nullptr, scm_report_list, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// scripting code follows:
|
||||
|
||||
static void
|
||||
run_guile_cli (void *data, [[maybe_unused]] int argc, [[maybe_unused]] char **argv)
|
||||
{
|
||||
auto args = static_cast<scripting_args*>(data);
|
||||
if (args->script)
|
||||
{
|
||||
PINFO ("Running script from %s... ", args->script->c_str());
|
||||
scm_c_primitive_load (args->script->c_str());
|
||||
}
|
||||
if (args->interactive)
|
||||
{
|
||||
std::cout << _("Welcome to Gnucash Interactive Guile Session") << std::endl;
|
||||
std::vector<const char*> modules =
|
||||
{ "gnucash core-utils", "gnucash engine", "gnucash app-utils", "gnucash report",
|
||||
"system repl repl", "ice-9 readline" };
|
||||
auto show_and_load = [](const auto& mod)
|
||||
{
|
||||
std::cout << bl::format ("(use-modules ({1}))") % mod << std::endl;
|
||||
scm_c_use_module (mod);
|
||||
};
|
||||
std::for_each (modules.begin(), modules.end(), show_and_load);
|
||||
scm_c_eval_string ("(activate-readline)");
|
||||
scm_c_eval_string ("(start-repl)");
|
||||
}
|
||||
cleanup_and_exit_with_save ();
|
||||
}
|
||||
|
||||
|
||||
#ifdef HAVE_PYTHON_H
|
||||
static void
|
||||
python_cleanup_and_exit (PyConfig& config, PyStatus& status)
|
||||
{
|
||||
if (qof_book_session_not_saved (gnc_get_current_book()))
|
||||
std::cerr << _("Book is readonly. Unsaved changes will be lost.") << std::endl;
|
||||
gnc_clear_current_session ();
|
||||
|
||||
PyConfig_Clear(&config);
|
||||
if (status.err_msg && *status.err_msg)
|
||||
std::cerr << bl::format (_("Python Config failed with error {1}")) % status.err_msg
|
||||
<< std::endl;
|
||||
gnc_shutdown_cli (status.exitcode);
|
||||
}
|
||||
|
||||
static void
|
||||
run_python_cli (int argc, char **argv, scripting_args* args)
|
||||
{
|
||||
PyConfig config;
|
||||
PyConfig_InitPythonConfig(&config);
|
||||
|
||||
PyStatus status = PyConfig_SetBytesArgv(&config, argc, argv);
|
||||
if (PyStatus_Exception(status))
|
||||
python_cleanup_and_exit (config, status);
|
||||
|
||||
status = Py_InitializeFromConfig(&config);
|
||||
if (PyStatus_Exception(status))
|
||||
python_cleanup_and_exit (config, status);
|
||||
|
||||
PyConfig_Clear(&config);
|
||||
|
||||
if (args->script)
|
||||
{
|
||||
auto script_filename = args->script->c_str();
|
||||
PINFO ("Running python script %s...", script_filename);
|
||||
auto fp = fopen (script_filename, "rb");
|
||||
if (!fp)
|
||||
{
|
||||
std::cerr << bl::format (_("Unable to load Python script {1}")) % script_filename
|
||||
<< std::endl;
|
||||
python_cleanup_and_exit (config, status);
|
||||
}
|
||||
else if (PyRun_SimpleFileEx (fp, script_filename, 1) != 0)
|
||||
{
|
||||
std::cerr << bl::format (_("Python script {1} execution failed.")) % script_filename
|
||||
<< std::endl;
|
||||
python_cleanup_and_exit (config, status);
|
||||
}
|
||||
}
|
||||
if (args->interactive)
|
||||
{
|
||||
std::cout << _("Welcome to Gnucash Interactive Python Session") << std::endl;
|
||||
PyRun_InteractiveLoop (stdin, "foo");
|
||||
}
|
||||
Py_Finalize();
|
||||
cleanup_and_exit_with_save ();
|
||||
}
|
||||
#endif
|
||||
|
||||
static const std::vector<const char*> valid_schemes = { "file", "mysql", "postgres" };
|
||||
|
||||
static bool
|
||||
is_valid_uri (const bo_str& uri)
|
||||
{
|
||||
if (!uri)
|
||||
return false;
|
||||
|
||||
if (boost::filesystem::is_regular_file (*uri))
|
||||
return true;
|
||||
|
||||
auto scheme = g_uri_parse_scheme (uri->c_str());
|
||||
|
||||
if (!scheme)
|
||||
return false;
|
||||
|
||||
auto rv = std::any_of (valid_schemes.begin(), valid_schemes.end(),
|
||||
[&scheme](const char* str) { return !g_strcmp0(str, scheme); });
|
||||
|
||||
g_free (scheme);
|
||||
return rv;
|
||||
}
|
||||
|
||||
int
|
||||
Gnucash::run_scripting (std::vector<const char*> newArgv,
|
||||
const bo_str& file_to_load,
|
||||
std::string& language,
|
||||
const bo_str& script,
|
||||
bool open_readwrite,
|
||||
bool interactive)
|
||||
{
|
||||
std::vector<std::string> errors;
|
||||
static const std::vector<std::string> languages = { "guile", "python" };
|
||||
|
||||
if (open_readwrite && !file_to_load)
|
||||
errors.push_back (_ ("--readwrite: missing datafile!"));
|
||||
|
||||
if (script && (!boost::filesystem::is_regular_file (*script)))
|
||||
errors.push_back ((bl::format (_("--script: {1} is not a file")) % *script).str());
|
||||
|
||||
if (std::none_of (languages.begin(), languages.end(),
|
||||
[&language](auto& lang){ return language == lang; }))
|
||||
errors.push_back (_ ("--language: must be 'python' or 'guile'"));
|
||||
#ifndef HAVE_PYTHON_H
|
||||
else if (language == "python")
|
||||
errors.push_back (_("--language: python wasn't compiled in this build"));
|
||||
#endif
|
||||
|
||||
if (!errors.empty())
|
||||
{
|
||||
std::cerr << _("Errors parsing arguments:") << std::endl;
|
||||
auto to_console = [](const auto& str){ std::cerr << str << std::endl; };
|
||||
std::for_each (errors.begin(), errors.end(), to_console);
|
||||
gnc_shutdown_cli (1);
|
||||
}
|
||||
|
||||
gnc_prefs_init ();
|
||||
gnc_ui_util_init();
|
||||
if (is_valid_uri (file_to_load))
|
||||
[[maybe_unused]] auto session = load_file (*file_to_load, open_readwrite);
|
||||
|
||||
scripting_args args { script, interactive };
|
||||
if (language == "guile")
|
||||
{
|
||||
scm_boot_guile (newArgv.size(), (char**)newArgv.data(), run_guile_cli, &args);
|
||||
return 0; // never reached...
|
||||
}
|
||||
#ifdef HAVE_PYTHON_H
|
||||
else if (language == "python")
|
||||
{
|
||||
run_python_cli (newArgv.size(), (char**)newArgv.data(), &args);
|
||||
return 0; // never reached...
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0; // never reached
|
||||
}
|
||||
|
@ -46,5 +46,11 @@ namespace Gnucash {
|
||||
int report_list (void);
|
||||
int report_show (const bo_str& file_to_load,
|
||||
const bo_str& run_report);
|
||||
int run_scripting (std::vector<const char*> newArgv,
|
||||
const bo_str& m_file_to_load,
|
||||
std::string& m_language,
|
||||
const bo_str& m_script,
|
||||
bool m_open_readwrite,
|
||||
bool m_interactive);
|
||||
}
|
||||
#endif
|
||||
|
@ -294,7 +294,7 @@ Gnucash::CoreApp::add_common_program_options (void)
|
||||
("input-file", bpo::value (&m_file_to_load),
|
||||
_("[datafile]"));
|
||||
|
||||
m_pos_opt_desc.add("input-file", -1);
|
||||
m_pos_opt_desc.add("input-file", 1);
|
||||
|
||||
m_opt_desc_all.add (common_options);
|
||||
m_opt_desc_all.add (hidden_options);
|
||||
|
@ -31,6 +31,7 @@
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "qof-backend.hpp"
|
||||
|
||||
@ -140,6 +141,68 @@ QofBackend::release_backends()
|
||||
}
|
||||
}
|
||||
/***********************************************************************/
|
||||
|
||||
static const std::unordered_map<QofBackendError,const char*> qof_backend_error_map =
|
||||
{
|
||||
{ ERR_BACKEND_NO_ERR, nullptr },
|
||||
{ ERR_BACKEND_NO_HANDLER, "no backend handler found for this access method (ENOSYS)" },
|
||||
{ ERR_BACKEND_NO_BACKEND, "Backend * pointer was unexpectedly null" },
|
||||
{ ERR_BACKEND_BAD_URL, "Can't parse url" },
|
||||
{ ERR_BACKEND_NO_SUCH_DB, "the named database doesn't exist" },
|
||||
{ ERR_BACKEND_CANT_CONNECT,"bad dbname/login/passwd or network failure" },
|
||||
{ ERR_BACKEND_CONN_LOST, "Lost connection to server" },
|
||||
{ ERR_BACKEND_LOCKED, "in use by another user (ETXTBSY)" },
|
||||
{ ERR_BACKEND_STORE_EXISTS,"File exists, data would be destroyed" },
|
||||
{ ERR_BACKEND_READONLY, "cannot write to file/directory" },
|
||||
{ ERR_BACKEND_TOO_NEW, "file/db version newer than what we can read" },
|
||||
{ ERR_BACKEND_DATA_CORRUPT,"data in db is corrupt" },
|
||||
{ ERR_BACKEND_SERVER_ERR, "error in response from server" },
|
||||
{ ERR_BACKEND_ALLOC, "internal memory allocation failure" },
|
||||
{ ERR_BACKEND_PERM, "user login successful, but no permissions to access the desired object" },
|
||||
{ ERR_BACKEND_MODIFIED, "commit of object update failed because another user has modified the object" },
|
||||
{ ERR_BACKEND_MOD_DESTROY, "commit of object update failed because another user has deleted the object" },
|
||||
{ ERR_BACKEND_MISC, "undetermined error" },
|
||||
{ ERR_QOF_OVERFLOW, "EOVERFLOW - generated by strtol or strtoll. When converting XML strings into numbers, an overflow has been detected. The XML file contains invalid data in a field that is meant to hold a signed long integer or signed long long integer." },
|
||||
|
||||
/* fileio errors */
|
||||
{ ERR_FILEIO_FILE_BAD_READ, "read failed or file prematurely truncated" },
|
||||
{ ERR_FILEIO_FILE_EMPTY, "file exists, is readable, but is empty" },
|
||||
{ ERR_FILEIO_FILE_LOCKERR, "mangled locks (unspecified error)" },
|
||||
{ ERR_FILEIO_FILE_NOT_FOUND,"not found / no such file" },
|
||||
{ ERR_FILEIO_FILE_TOO_OLD, "file version so old we can't read it" },
|
||||
{ ERR_FILEIO_UNKNOWN_FILE_TYPE,"didn't recognize the file type" },
|
||||
{ ERR_FILEIO_PARSE_ERROR, "couldn't parse the data in the file" },
|
||||
{ ERR_FILEIO_BACKUP_ERROR, "couldn't make a backup of the file" },
|
||||
{ ERR_FILEIO_WRITE_ERROR, "couldn't write to the file" },
|
||||
{ ERR_FILEIO_READ_ERROR, "Could not open the file for reading." },
|
||||
{ ERR_FILEIO_NO_ENCODING, "file does not specify encoding" },
|
||||
{ ERR_FILEIO_FILE_EACCES, "No read access permission for the given file" },
|
||||
{ ERR_FILEIO_RESERVED_WRITE,"User attempt to write to a directory reserved for internal use by GnuCash" },
|
||||
{ ERR_FILEIO_FILE_UPGRADE, "file will be upgraded and not be able to be read by prior versions - warn user" },
|
||||
|
||||
/* network errors */
|
||||
{ ERR_NETIO_SHORT_READ, "not enough bytes received" },
|
||||
{ ERR_NETIO_WRONG_CONTENT_TYPE,"wrong kind of server, wrong data served" },
|
||||
{ ERR_NETIO_NOT_GNCXML, "whatever it is, we can't parse it." },
|
||||
|
||||
/* database errors */
|
||||
{ ERR_SQL_MISSING_DATA, "database doesn't contain expected data" },
|
||||
{ ERR_SQL_DB_TOO_OLD, "database is old and needs upgrading" },
|
||||
{ ERR_SQL_DB_TOO_NEW, "database is newer, we can't write to it" },
|
||||
{ ERR_SQL_DB_BUSY, "database is busy, cannot upgrade version" },
|
||||
{ ERR_SQL_BAD_DBI, "LibDBI has numeric errors" },
|
||||
{ ERR_SQL_DBI_UNTESTABLE, "could not complete test for LibDBI bug" },
|
||||
|
||||
/* RPC errors */
|
||||
{ ERR_RPC_HOST_UNK, "Host unknown" },
|
||||
{ ERR_RPC_CANT_BIND, "can't bind to address" },
|
||||
{ ERR_RPC_CANT_ACCEPT, "can't accept connection" },
|
||||
{ ERR_RPC_NO_CONNECTION, "no connection to server" },
|
||||
{ ERR_RPC_BAD_VERSION, "RPC Version Mismatch" },
|
||||
{ ERR_RPC_FAILED, "Operation failed" },
|
||||
{ ERR_RPC_NOT_ADDED, "object not added" }
|
||||
};
|
||||
|
||||
QofBackendError
|
||||
qof_backend_get_error (QofBackend* qof_be)
|
||||
{
|
||||
@ -147,6 +210,18 @@ qof_backend_get_error (QofBackend* qof_be)
|
||||
return ((QofBackend*)qof_be)->get_error();
|
||||
}
|
||||
|
||||
const char*
|
||||
qof_backend_get_error_string (QofBackendError backend_error)
|
||||
{
|
||||
auto str_iter = qof_backend_error_map.find (backend_error);
|
||||
if (str_iter == qof_backend_error_map.end())
|
||||
{
|
||||
PERR ("cannot error string for error %d", backend_error);
|
||||
return nullptr;
|
||||
}
|
||||
return str_iter->second;
|
||||
}
|
||||
|
||||
void
|
||||
qof_backend_set_error (QofBackend* qof_be, QofBackendError err)
|
||||
{
|
||||
|
@ -131,6 +131,10 @@ typedef struct QofBackend QofBackend;
|
||||
/* The following functions are used in C files. */
|
||||
/** Get the last backend error. */
|
||||
QofBackendError qof_backend_get_error (QofBackend*);
|
||||
|
||||
/** Get the last backend error string. */
|
||||
const char* qof_backend_get_error_string (QofBackendError);
|
||||
|
||||
/** Set the error on the specified QofBackend. */
|
||||
void qof_backend_set_error (QofBackend*, QofBackendError);
|
||||
|
||||
|
@ -15,3 +15,7 @@ libgnucash/engine/iso-4217-currencies.c
|
||||
|
||||
# This file containing @PROJECT_NAME@ shouldn't be translated.
|
||||
gnucash/gschemas/org.gnucash.GnuCash.deprecated.gschema.xml.in
|
||||
|
||||
# These files are example scripts for gnucash-cli
|
||||
doc/examples/book-to-hledger.scm
|
||||
doc/examples/simple-book-add-txn.scm
|
||||
|
Loading…
Reference in New Issue
Block a user