diff --git a/gnucash/import-export/csv-imp/CMakeLists.txt b/gnucash/import-export/csv-imp/CMakeLists.txt index 91de99e1dc..93bdfa6c3b 100644 --- a/gnucash/import-export/csv-imp/CMakeLists.txt +++ b/gnucash/import-export/csv-imp/CMakeLists.txt @@ -10,15 +10,20 @@ SET(csv_import_remote_SOURCES SET(csv_import_SOURCES gncmod-csv-import.c assistant-csv-account-import.c + assistant-csv-price-import.cpp assistant-csv-trans-import.cpp gnc-plugin-csv-import.c csv-account-import.c gnc-csv-account-map.c gnc-csv-gnumeric-popup.c gnc-csv-tokenizer.cpp - gnc-csv-trans-settings.cpp + gnc-csv-import-settings.cpp + gnc-csv-price-import-settings.cpp + gnc-csv-trans-import-settings.cpp gnc-dummy-tokenizer.cpp gnc-fw-tokenizer.cpp + gnc-price-import.cpp + gnc-price-props.cpp gnc-tokenizer.cpp gnc-trans-props.cpp gnc-tx-import.cpp @@ -35,15 +40,20 @@ SET(csv_import_remote_HEADERS SET(csv_import_noinst_HEADERS assistant-csv-account-import.h + assistant-csv-price-import.h assistant-csv-trans-import.h gnc-plugin-csv-import.h csv-account-import.h gnc-csv-account-map.h gnc-csv-gnumeric-popup.h gnc-csv-tokenizer.hpp - gnc-csv-trans-settings.hpp + gnc-csv-import-settings.hpp + gnc-csv-price-import-settings.hpp + gnc-csv-trans-import-settings.hpp gnc-dummy-tokenizer.hpp gnc-fw-tokenizer.hpp + gnc-price-import.hpp + gnc-price-props.hpp gnc-tokenizer.hpp gnc-trans-props.hpp gnc-tx-import.hpp @@ -84,7 +94,7 @@ INSTALL(TARGETS gncmod-csv-import # No headers to install SET(csv_import_GLADE assistant-csv-account-import.glade - assistant-csv-trans-import.glade) + assistant-csv-price-import.glade assistant-csv-trans-import.glade) INSTALL(FILES ${csv_import_GLADE} DESTINATION ${CMAKE_INSTALL_DATADIR}/gnucash/gtkbuilder) diff --git a/gnucash/import-export/csv-imp/Makefile.am b/gnucash/import-export/csv-imp/Makefile.am index 06ef43f207..b4ab800b87 100644 --- a/gnucash/import-export/csv-imp/Makefile.am +++ b/gnucash/import-export/csv-imp/Makefile.am @@ -5,6 +5,7 @@ pkglib_LTLIBRARIES=libgncmod-csv-import.la libgncmod_csv_import_la_SOURCES = \ gncmod-csv-import.c \ assistant-csv-account-import.c \ + assistant-csv-price-import.cpp \ assistant-csv-trans-import.cpp \ gnc-plugin-csv-import.c \ csv-account-import.c \ @@ -13,13 +14,18 @@ libgncmod_csv_import_la_SOURCES = \ gnc-csv-gnumeric-popup.c \ gnc-dummy-tokenizer.cpp \ gnc-fw-tokenizer.cpp \ + gnc-price-import.cpp \ + gnc-price-props.cpp \ gnc-tokenizer.cpp \ gnc-tx-import.cpp \ gnc-trans-props.cpp \ - gnc-csv-trans-settings.cpp + gnc-csv-import-settings.cpp \ + gnc-csv-price-import-settings.cpp \ + gnc-csv-trans-import-settings.cpp noinst_HEADERS = \ assistant-csv-account-import.h \ + assistant-csv-price-import.h \ assistant-csv-trans-import.h \ gnc-plugin-csv-import.h \ csv-account-import.h \ @@ -28,10 +34,14 @@ noinst_HEADERS = \ gnc-csv-gnumeric-popup.h \ gnc-dummy-tokenizer.hpp \ gnc-fw-tokenizer.hpp \ + gnc-price-import.hpp \ + gnc-price-props.hpp \ gnc-tokenizer.hpp \ gnc-tx-import.hpp \ gnc-trans-props.hpp \ - gnc-csv-trans-settings.hpp + gnc-csv-import-settings.hpp \ + gnc-csv-price-import-settings.hpp \ + gnc-csv-trans-import-settings.hpp libgncmod_csv_import_la_LDFLAGS = -avoid-version @@ -75,6 +85,7 @@ ui_DATA = \ gtkbuilderdir = ${GNC_GTKBUILDER_DIR} gtkbuilder_DATA = \ assistant-csv-account-import.glade \ + assistant-csv-price-import.glade \ assistant-csv-trans-import.glade EXTRA_DIST = $(ui_DATA) $(gtkbuilder_DATA) CMakeLists.txt diff --git a/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp b/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp new file mode 100644 index 0000000000..091123302a --- /dev/null +++ b/gnucash/import-export/csv-imp/assistant-csv-price-import.cpp @@ -0,0 +1,1835 @@ +/*******************************************************************\ + * assistant-csv-price-import.c -- An assistant for importing * + * Prices from a file. * + * * + * Copyright (C) 2017 Robert Fewell * + * * + * 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@gnu.org * +\********************************************************************/ +/** @file assistant-csv-price-import.cpp + @brief CSV Import Assistant + @author Copyright (c) 2016 Geert Janssens + @author Copyright (c) 2017 Robert Fewell +*/ + +#include + +extern "C" +{ +#include "config.h" + +#include +#include +#include + +#include "gnc-ui.h" +#include "gnc-uri-utils.h" +#include "gnc-ui-util.h" +#include "dialog-utils.h" + +#include "gnc-component-manager.h" + +#include "gnc-state.h" + +#include "assistant-csv-price-import.h" + +#include "gnc-csv-gnumeric-popup.h" +#include "go-charmap-sel.h" +} + +#include "gnc-csv-price-import-settings.hpp" +#include "gnc-price-import.hpp" +#include "gnc-fw-tokenizer.hpp" +#include "gnc-csv-tokenizer.hpp" + +#define MIN_COL_WIDTH 70 +#define GNC_PREFS_GROUP "dialogs.import.csv" +#define ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS "assistant-csv-price-import" + +/* This static indicates the debugging module that this .o belongs to. */ +static QofLogModule log_module = GNC_MOD_ASSISTANT; + +class CsvImpPriceAssist +{ +public: + CsvImpPriceAssist (); + + void assist_prepare_cb (GtkWidget *page); + void assist_file_page_prepare (); + void assist_preview_page_prepare (); + void assist_confirm_page_prepare (); + void assist_summary_page_prepare (); + void assist_finish (); + void assist_compmgr_close (); + + void file_confirm_cb (); + + void preview_settings_delete (); + void preview_settings_save (); + void preview_settings_name (GtkEntry* entry); + void preview_settings_load (); + void preview_update_skipped_rows (); + void preview_over_write (bool over); + void preview_update_separators (GtkWidget* widget); + void preview_update_file_format (); + void preview_update_encoding (const char* encoding); + void preview_update_date_format (); + void preview_update_currency_format (); + void preview_update_currency (); + void preview_update_commodity (); + void preview_reparse_col_type (GncPricePropType type); + void preview_update_col_type (GtkComboBox* cbox); + void preview_update_fw_columns (GtkTreeView* treeview, GdkEventButton* event); + + void preview_populate_settings_combo(); + void preview_handle_save_del_sensitivity (GtkComboBox* combo); + void preview_split_column (int col, int offset); + void preview_refresh_table (); + void preview_refresh (); + void preview_validate_settings (); + + friend gboolean + fixed_context_menu_handler_price (GnumericPopupMenuElement const *element, + gpointer userdata); +private: + /* helper functions to manage the context menu for fixed with columns */ + uint32_t get_new_col_rel_pos (GtkTreeViewColumn *tcol, int dx); + void fixed_context_menu (GdkEventButton *event, int col, int dx); + /* helper function to calculate row colors for the preview table (to visualize status) */ + void preview_row_fill_state_cells (GtkListStore *store, GtkTreeIter *iter, + std::string& err_msg, bool skip); + /* helper function to create preview header cell combo boxes listing available column types */ + GtkWidget* preview_cbox_factory (GtkTreeModel* model, uint32_t colnum); + /* helper function to set rendering parameters for preview data columns */ + void preview_style_column (uint32_t col_num, GtkTreeModel* model); + + GtkAssistant *csv_imp_asst; + + GtkWidget *file_page; /**< Assistant file page widget */ + GtkWidget *file_chooser; /**< The widget for the file chooser */ + std::string m_file_name; /**< The import file name */ + + GtkWidget *preview_page; /**< Assistant preview page widget */ + GtkComboBox *settings_combo; /**< The Settings Combo */ + GtkWidget *save_button; /**< The Save Settings button */ + GtkWidget *del_button; /**< The Delete Settings button */ + + GtkWidget *combo_hbox; /**< The Settings Combo hbox */ + GtkSpinButton *start_row_spin; /**< The widget for the start row spinner */ + GtkSpinButton *end_row_spin; /**< The widget for the end row spinner */ + GtkWidget *skip_alt_rows_button; /**< The widget for Skip alternate rows from start row */ + GtkWidget *skip_errors_button; /**< The widget for Skip error rows*/ + GtkWidget *csv_button; /**< The widget for the CSV button */ + GtkWidget *fixed_button; /**< The widget for the Fixed Width button */ + GtkWidget *over_write_cbutton; /**< The widget for Price Over Write */ + GtkWidget *commodity_selector; /**< The widget for commodity combo box */ + GtkWidget *currency_selector; /**< The widget for currency combo box */ + GOCharmapSel *encselector; /**< The widget for selecting the encoding */ + GtkWidget *separator_table; /**< Container for the separator checkboxes */ + GtkCheckButton *sep_button[SEP_NUM_OF_TYPES]; /**< Checkbuttons for common separators */ + GtkWidget *fw_instructions_hbox; /**< Container for fixed-width instructions */ + GtkCheckButton *custom_cbutton; /**< The checkbutton for a custom separator */ + GtkEntry *custom_entry; /**< The entry for custom separators */ + GtkComboBoxText *date_format_combo; /**< The Combo Text widget for selecting the date format */ + GtkComboBoxText *currency_format_combo; /**< The Combo Text widget for selecting the currency format */ + GtkTreeView *treeview; /**< The treeview containing the data */ + GtkLabel *instructions_label; /**< The instructions label */ + GtkImage *instructions_image; /**< The instructions image */ + bool encoding_selected_called; /**< Before encoding_selected is first called, this is false. + * error lines, instead of all the file data. */ + int fixed_context_col; /**< The number of the column the user has clicked */ + int fixed_context_offset; /**< The offset (in characters) in the column + * the user has clicked */ + + GtkWidget *confirm_page; /**< Assistant confirm page widget */ + + GtkWidget *summary_page; /**< Assistant summary page widget */ + GtkWidget *summary_label; /**< The summary text */ + + std::unique_ptr price_imp; /**< The actual data we are previewing */ +}; + + +/******************************************************* + * Assistant call back functions + *******************************************************/ + +extern "C" +{ +void csv_price_imp_assist_prepare_cb (GtkAssistant *assistant, GtkWidget *page, CsvImpPriceAssist* info); +void csv_price_imp_assist_destroy_cb (GtkWidget *object, CsvImpPriceAssist* info); +void csv_price_imp_assist_cancel_cb (GtkAssistant *gtkassistant, CsvImpPriceAssist* info); +void csv_price_imp_assist_close_cb (GtkAssistant *gtkassistant, CsvImpPriceAssist* info); +void csv_price_imp_assist_finish_cb (GtkAssistant *gtkassistant, CsvImpPriceAssist* info); +void csv_price_imp_file_confirm_cb (GtkWidget *button, CsvImpPriceAssist *info); +void csv_price_imp_preview_del_settings_cb (GtkWidget *button, CsvImpPriceAssist *info); +void csv_price_imp_preview_save_settings_cb (GtkWidget *button, CsvImpPriceAssist *info); +void csv_price_imp_preview_settings_sel_changed_cb (GtkComboBox *combo, CsvImpPriceAssist *info); +void csv_price_imp_preview_settings_text_inserted_cb (GtkEditable *entry, gchar *new_text, + gint new_text_length, gint *position, CsvImpPriceAssist *info); +void csv_price_imp_preview_settings_text_changed_cb (GtkEntry *entry, CsvImpPriceAssist *info); +void csv_price_imp_preview_srow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info); +void csv_price_imp_preview_erow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info); +void csv_price_imp_preview_skiprows_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info); +void csv_price_imp_preview_skiperrors_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info); +void csv_price_imp_preview_overwrite_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info); +void csv_price_imp_preview_sep_button_cb (GtkWidget* widget, CsvImpPriceAssist* info); +void csv_price_imp_preview_sep_fixed_sel_cb (GtkToggleButton* csv_button, CsvImpPriceAssist* info); +void csv_price_imp_preview_acct_sel_cb (GtkWidget* widget, CsvImpPriceAssist* info); +void csv_price_imp_preview_enc_sel_cb (GOCharmapSel* selector, const char* encoding, + CsvImpPriceAssist* info); +} + +void +csv_price_imp_assist_prepare_cb (GtkAssistant *assistant, GtkWidget *page, + CsvImpPriceAssist* info) +{ + info->assist_prepare_cb(page); +} + +void +csv_price_imp_assist_destroy_cb (GtkWidget *object, CsvImpPriceAssist* info) +{ + gnc_unregister_gui_component_by_data (ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS, info); + delete info; +} + +void +csv_price_imp_assist_cancel_cb (GtkAssistant *assistant, CsvImpPriceAssist* info) +{ + gnc_close_gui_component_by_data (ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS, info); +} + +void +csv_price_imp_assist_close_cb (GtkAssistant *assistant, CsvImpPriceAssist* info) +{ + gnc_close_gui_component_by_data (ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS, info); +} + +void +csv_price_imp_assist_finish_cb (GtkAssistant *assistant, CsvImpPriceAssist* info) +{ + info->assist_finish (); +} + +void csv_price_imp_file_confirm_cb (GtkWidget *button, CsvImpPriceAssist *info) +{ + info->file_confirm_cb(); +} + +void csv_price_imp_preview_del_settings_cb (GtkWidget *button, CsvImpPriceAssist *info) +{ + info->preview_settings_delete(); +} + +void csv_price_imp_preview_save_settings_cb (GtkWidget *button, CsvImpPriceAssist *info) +{ + info->preview_settings_save(); +} + +void csv_price_imp_preview_settings_sel_changed_cb (GtkComboBox *combo, CsvImpPriceAssist *info) +{ + info->preview_settings_load(); +} + +void +csv_price_imp_preview_settings_text_inserted_cb (GtkEditable *entry, gchar *new_text, + gint new_text_length, gint *position, CsvImpPriceAssist *info) +{ + if (!new_text) + return; + + /* Prevent entering [], which are invalid characters in key files */ + auto base_txt = std::string (new_text); + auto mod_txt = base_txt; + std::replace (mod_txt.begin(), mod_txt.end(), '[', '('); + std::replace (mod_txt.begin(), mod_txt.end(), ']', ')'); + if (base_txt == mod_txt) + return; + g_signal_handlers_block_by_func (entry, (gpointer) csv_price_imp_preview_settings_text_inserted_cb, info); + gtk_editable_insert_text (entry, mod_txt.c_str(), mod_txt.size() , position); + g_signal_handlers_unblock_by_func (entry, (gpointer) csv_price_imp_preview_settings_text_inserted_cb, info); + + g_signal_stop_emission_by_name (entry, "insert_text"); +} + +void +csv_price_imp_preview_settings_text_changed_cb (GtkEntry *entry, CsvImpPriceAssist *info) +{ + info->preview_settings_name(entry); +} + +void csv_price_imp_preview_srow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info) +{ + info->preview_update_skipped_rows(); +} + +void csv_price_imp_preview_erow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info) +{ + info->preview_update_skipped_rows(); +} + +void csv_price_imp_preview_skiprows_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info) +{ + info->preview_update_skipped_rows(); +} + +void csv_price_imp_preview_skiperrors_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info) +{ + info->preview_update_skipped_rows(); +} + +void csv_price_imp_preview_overwrite_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info) +{ + info->preview_over_write (gtk_toggle_button_get_active (checkbox)); +} + +void csv_price_imp_preview_sep_button_cb (GtkWidget* widget, CsvImpPriceAssist* info) +{ + info->preview_update_separators(widget); +} + +void csv_price_imp_preview_sep_fixed_sel_cb (GtkToggleButton* csv_button, CsvImpPriceAssist* info) +{ + info->preview_update_file_format(); +} + +void csv_price_imp_preview_enc_sel_cb (GOCharmapSel* selector, const char* encoding, + CsvImpPriceAssist* info) +{ + info->preview_update_encoding(encoding); +} + +static void csv_price_imp_preview_date_fmt_sel_cb (GtkComboBox* format_selector, CsvImpPriceAssist* info) +{ + info->preview_update_date_format(); +} + +static void csv_price_imp_preview_currency_fmt_sel_cb (GtkComboBox* format_selector, CsvImpPriceAssist* info) +{ + info->preview_update_currency_format(); +} + +static void csv_price_imp_preview_currency_sel_cb (GtkComboBox* currency_selector, CsvImpPriceAssist* info) +{ + info->preview_update_currency(); +} + +static void csv_price_imp_preview_commodity_sel_cb (GtkComboBox* commodity_selector, CsvImpPriceAssist* info) +{ + info->preview_update_commodity(); +} + +void csv_price_imp_preview_col_type_changed_cb (GtkComboBox* cbox, CsvImpPriceAssist* info) +{ + info->preview_update_col_type (cbox); +} + +gboolean +csv_price_imp_preview_treeview_clicked_cb (GtkTreeView* treeview, GdkEventButton* event, + CsvImpPriceAssist* info) +{ + info->preview_update_fw_columns(treeview, event); + return false; +} + +static +gnc_commodity *get_commodity_from_combo (GtkComboBox *combo) +{ + GtkTreeModel *model, *sort_model; + GtkTreeIter iter, siter; + gchar *string; + gnc_commodity *comm; + + if (!gtk_combo_box_get_active_iter (combo, &siter)) + return nullptr; + + sort_model = gtk_combo_box_get_model (combo); + model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT(sort_model)); + + gtk_tree_model_sort_convert_iter_to_child_iter (GTK_TREE_MODEL_SORT(sort_model), + &iter, &siter); + + gtk_tree_model_get (GTK_TREE_MODEL (model), &iter, 0, &string, 2, &comm, -1); + + PINFO("Commodity string is %s", string); + + g_free (string); + return comm; +} + +static void +set_commodity_for_combo (GtkComboBox *combo, gnc_commodity *comm) +{ + GtkTreeModel *model, *sort_model; + GtkTreeIter iter, siter; + gnc_commodity *model_comm; + gboolean valid; + + sort_model = gtk_combo_box_get_model (combo); + model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT(sort_model)); + valid = gtk_tree_model_get_iter_first (model, &iter); + + while (valid) + { + gtk_tree_model_get (model, &iter, 2, &model_comm, -1); + if (model_comm == comm) + { + if (gtk_tree_model_sort_convert_child_iter_to_iter (GTK_TREE_MODEL_SORT(sort_model), &siter, &iter)) + { + gtk_combo_box_set_active_iter (combo, &siter); + return; + } + } + /* Make iter point to the next row in the list store */ + valid = gtk_tree_model_iter_next (model, &iter); + } + // Not found, set it to first iter + valid = gtk_tree_model_get_iter_first (model, &iter); + if (gtk_tree_model_sort_convert_child_iter_to_iter (GTK_TREE_MODEL_SORT(sort_model), &siter, &iter)) + gtk_combo_box_set_active_iter (combo, &siter); +} + +static +GtkTreeModel *get_model (bool all_commodity) +{ + GtkTreeModel *store, *model; + const gnc_commodity_table *commodity_table = gnc_get_current_commodities (); + gnc_commodity *tmp_commodity = nullptr; + char *tmp_namespace = nullptr; + GList *commodity_list = nullptr; + GList *namespace_list = gnc_commodity_table_get_namespaces (commodity_table); + GtkTreeIter iter; + + store = GTK_TREE_MODEL(gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER)); + model = gtk_tree_model_sort_new_with_model (store); + // set sort to sort on second string, first string will be shown + gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (model), 1, GTK_SORT_ASCENDING); + + gtk_list_store_append (GTK_LIST_STORE(store), &iter); + gtk_list_store_set (GTK_LIST_STORE(store), &iter, 0, " ", 1, " ", 2, nullptr, -1); + + namespace_list = g_list_first (namespace_list); + while (namespace_list != nullptr) + { + tmp_namespace = (char*)namespace_list->data; + DEBUG("Looking at namespace %s", tmp_namespace); + + /* Hide the template entry */ + if (g_utf8_collate (tmp_namespace, "template" ) != 0) + { + if ((g_utf8_collate (tmp_namespace, GNC_COMMODITY_NS_CURRENCY ) == 0) || (all_commodity == true)) + { + commodity_list = gnc_commodity_table_get_commodities (commodity_table, tmp_namespace); + commodity_list = g_list_first (commodity_list); + while (commodity_list != nullptr) + { + const gchar *name_str; + gchar *sort_str; + tmp_commodity = (gnc_commodity*)commodity_list->data; + DEBUG("Looking at commodity %s", gnc_commodity_get_fullname (tmp_commodity)); + + name_str = gnc_commodity_get_printname (tmp_commodity); + + sort_str = g_strconcat (tmp_namespace, "::", gnc_commodity_get_mnemonic (tmp_commodity), nullptr); + DEBUG("Name string is %s, Sort string is %s", name_str, sort_str); + + gtk_list_store_append (GTK_LIST_STORE(store), &iter); + gtk_list_store_set (GTK_LIST_STORE(store), &iter, 0, name_str, 1, sort_str, 2, tmp_commodity, -1); + + g_free (sort_str); + commodity_list = g_list_next (commodity_list); + } + } + } + namespace_list = g_list_next (namespace_list); + } + g_list_free (commodity_list); + g_list_free (namespace_list); + + return model; +} + + +/******************************************************* + * Assistant Constructor + *******************************************************/ +CsvImpPriceAssist::CsvImpPriceAssist () +{ + auto builder = gtk_builder_new(); + gnc_builder_add_from_file (builder , "assistant-csv-price-import.glade", "start_row_adj"); + gnc_builder_add_from_file (builder , "assistant-csv-price-import.glade", "end_row_adj"); + gnc_builder_add_from_file (builder , "assistant-csv-price-import.glade", "liststore1"); + gnc_builder_add_from_file (builder , "assistant-csv-price-import.glade", "liststore2"); + gnc_builder_add_from_file (builder , "assistant-csv-price-import.glade", "CSV Price Assistant"); + csv_imp_asst = GTK_ASSISTANT(gtk_builder_get_object (builder, "CSV Price Assistant")); + + /* Enable buttons on all page. */ + gtk_assistant_set_page_complete (csv_imp_asst, + GTK_WIDGET(gtk_builder_get_object (builder, "start_page")), + true); + gtk_assistant_set_page_complete (csv_imp_asst, + GTK_WIDGET(gtk_builder_get_object (builder, "file_page")), + false); + gtk_assistant_set_page_complete (csv_imp_asst, + GTK_WIDGET(gtk_builder_get_object (builder, "preview_page")), + false); + gtk_assistant_set_page_complete (csv_imp_asst, + GTK_WIDGET(gtk_builder_get_object (builder, "confirm_page")), + true); + gtk_assistant_set_page_complete (csv_imp_asst, + GTK_WIDGET(gtk_builder_get_object (builder, "summary_page")), + true); + + /* File chooser Page */ + file_page = GTK_WIDGET(gtk_builder_get_object (builder, "file_page")); + file_chooser = gtk_file_chooser_widget_new (GTK_FILE_CHOOSER_ACTION_OPEN); + g_signal_connect (G_OBJECT(file_chooser), "file-activated", + G_CALLBACK(csv_price_imp_file_confirm_cb), this); + auto button = gtk_button_new_with_label (_("OK")); + gtk_widget_set_size_request (button, 100, -1); + gtk_widget_show (button); + auto h_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous (GTK_BOX (h_box), TRUE); + gtk_widget_set_hexpand (GTK_WIDGET(h_box), TRUE); + gtk_box_pack_start (GTK_BOX(h_box), button, FALSE, FALSE, 0); + gtk_file_chooser_set_extra_widget (GTK_FILE_CHOOSER(file_chooser), h_box); + g_signal_connect (G_OBJECT(button), "clicked", + G_CALLBACK(csv_price_imp_file_confirm_cb), this); + + auto box = GTK_WIDGET(gtk_builder_get_object (builder, "file_page")); + gtk_box_pack_start (GTK_BOX(box), file_chooser, TRUE, TRUE, 6); + gtk_widget_show (file_chooser); + + /* Preview Settings Page */ + { + preview_page = GTK_WIDGET(gtk_builder_get_object (builder, "preview_page")); + + // Add Settings combo + auto settings_store = gtk_list_store_new (2, G_TYPE_POINTER, G_TYPE_STRING); + settings_combo = GTK_COMBO_BOX(gtk_combo_box_new_with_model_and_entry (GTK_TREE_MODEL(settings_store))); + gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX(settings_combo), SET_NAME); + gtk_combo_box_set_active (GTK_COMBO_BOX(settings_combo), 0); + + combo_hbox = GTK_WIDGET(gtk_builder_get_object (builder, "combo_hbox")); + gtk_box_pack_start (GTK_BOX(combo_hbox), GTK_WIDGET(settings_combo), true, true, 6); + gtk_widget_show (GTK_WIDGET(settings_combo)); + + g_signal_connect (G_OBJECT(settings_combo), "changed", + G_CALLBACK(csv_price_imp_preview_settings_sel_changed_cb), this); + + // Additionally connect to the changed signal of the embedded GtkEntry + auto emb_entry = gtk_bin_get_child (GTK_BIN (settings_combo)); + g_signal_connect (G_OBJECT(emb_entry), "changed", + G_CALLBACK(csv_price_imp_preview_settings_text_changed_cb), this); + g_signal_connect (G_OBJECT(emb_entry), "insert-text", + G_CALLBACK(csv_price_imp_preview_settings_text_inserted_cb), this); + + // Add Save Settings button + save_button = GTK_WIDGET(gtk_builder_get_object (builder, "save_settings")); + + // Add Delete Settings button + del_button = GTK_WIDGET(gtk_builder_get_object (builder, "delete_settings")); + + /* The table containing the separator configuration widgets */ + start_row_spin = GTK_SPIN_BUTTON(gtk_builder_get_object (builder, "start_row")); + end_row_spin = GTK_SPIN_BUTTON(gtk_builder_get_object (builder, "end_row")); + skip_alt_rows_button = GTK_WIDGET(gtk_builder_get_object (builder, "skip_rows")); + skip_errors_button = GTK_WIDGET(gtk_builder_get_object (builder, "skip_errors_button")); + over_write_cbutton = GTK_WIDGET(gtk_builder_get_object (builder, "over_write_button")); + separator_table = GTK_WIDGET(gtk_builder_get_object (builder, "separator_table")); + fw_instructions_hbox = GTK_WIDGET(gtk_builder_get_object (builder, "fw_instructions_hbox")); + + /* Load the separator buttons from the glade builder file into the + * sep_buttons array. */ + const char* sep_button_names[] = { + "space_cbutton", + "tab_cbutton", + "comma_cbutton", + "colon_cbutton", + "semicolon_cbutton", + "hyphen_cbutton" + }; + for (int i = 0; i < SEP_NUM_OF_TYPES; i++) + sep_button[i] + = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, sep_button_names[i])); + + /* Load and connect the custom separator checkbutton in the same way + * as the other separator buttons. */ + custom_cbutton + = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_cbutton")); + + /* Load the entry for the custom separator entry. Connect it to the + * sep_button_clicked event handler as well. */ + custom_entry = (GtkEntry*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_entry")); + + /* Create the encoding selector widget and add it to the assistant */ + encselector = GO_CHARMAP_SEL(go_charmap_sel_new(GO_CHARMAP_SEL_TO_UTF8)); + /* Connect the selector to the encoding_selected event handler. */ + g_signal_connect (G_OBJECT(encselector), "charmap_changed", + G_CALLBACK(csv_price_imp_preview_enc_sel_cb), this); + + auto encoding_container = GTK_CONTAINER(gtk_builder_get_object (builder, "encoding_container")); + gtk_container_add (encoding_container, GTK_WIDGET(encselector)); + gtk_widget_show_all (GTK_WIDGET(encoding_container)); + + /* Add commodity selection widget */ + commodity_selector = GTK_WIDGET(gtk_builder_get_object (builder, "commodity_cbox")); + gtk_combo_box_set_model (GTK_COMBO_BOX(commodity_selector), get_model (true)); + g_signal_connect(G_OBJECT(commodity_selector), "changed", + G_CALLBACK(csv_price_imp_preview_commodity_sel_cb), this); + + /* Add currency selection widget */ + currency_selector = GTK_WIDGET(gtk_builder_get_object (builder, "currency_cbox")); + gtk_combo_box_set_model (GTK_COMBO_BOX(currency_selector), get_model (false)); + g_signal_connect(G_OBJECT(currency_selector), "changed", + G_CALLBACK(csv_price_imp_preview_currency_sel_cb), this); + + /* The instructions label and image */ + instructions_label = GTK_LABEL(gtk_builder_get_object (builder, "instructions_label")); + instructions_image = GTK_IMAGE(gtk_builder_get_object (builder, "instructions_image")); + + /* Add in the date format combo box and hook it up to an event handler. */ + date_format_combo = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + for (auto& date_fmt : GncDate::c_formats) + gtk_combo_box_text_append_text (date_format_combo, _(date_fmt.m_fmt.c_str())); + gtk_combo_box_set_active (GTK_COMBO_BOX(date_format_combo), 0); + g_signal_connect (G_OBJECT(date_format_combo), "changed", + G_CALLBACK(csv_price_imp_preview_date_fmt_sel_cb), this); + + /* Add it to the assistant. */ + auto date_format_container = GTK_CONTAINER(gtk_builder_get_object (builder, "date_format_container")); + gtk_container_add (date_format_container, GTK_WIDGET(date_format_combo)); + gtk_widget_show_all (GTK_WIDGET(date_format_container)); + + /* Add in the currency format combo box and hook it up to an event handler. */ + currency_format_combo = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + for (int i = 0; i < num_currency_formats_price; i++) + { + gtk_combo_box_text_append_text (currency_format_combo, _(currency_format_user_price[i])); + } + /* Default will the locale */ + gtk_combo_box_set_active (GTK_COMBO_BOX(currency_format_combo), 0); + g_signal_connect (G_OBJECT(currency_format_combo), "changed", + G_CALLBACK(csv_price_imp_preview_currency_fmt_sel_cb), this); + + /* Add it to the assistant. */ + auto currency_format_container = GTK_CONTAINER(gtk_builder_get_object (builder, "currency_format_container")); + gtk_container_add (currency_format_container, GTK_WIDGET(currency_format_combo)); + gtk_widget_show_all (GTK_WIDGET(currency_format_container)); + + /* Connect the CSV/Fixed-Width radio button event handler. */ + csv_button = GTK_WIDGET(gtk_builder_get_object (builder, "csv_button")); + fixed_button = GTK_WIDGET(gtk_builder_get_object (builder, "fixed_button")); + + /* Load the data treeview and connect it to its resizing event handler. */ + treeview = (GtkTreeView*)GTK_WIDGET(gtk_builder_get_object (builder, "treeview")); + gtk_tree_view_set_headers_clickable (treeview, true); + + /* This is true only after encoding_selected is called, so we must + * set it initially to false. */ + encoding_selected_called = false; + } + + /* Confirm Page */ + confirm_page = GTK_WIDGET(gtk_builder_get_object (builder, "confirm_page")); + + /* Summary Page */ + summary_page = GTK_WIDGET(gtk_builder_get_object (builder, "summary_page")); + summary_label = GTK_WIDGET(gtk_builder_get_object (builder, "summary_label")); + + gnc_restore_window_size (GNC_PREFS_GROUP, GTK_WINDOW(csv_imp_asst)); + + gtk_builder_connect_signals (builder, this); + g_object_unref (G_OBJECT(builder)); + + gtk_widget_show_all (GTK_WIDGET(csv_imp_asst)); + gnc_window_adjust_for_screen (GTK_WINDOW(csv_imp_asst)); +} + +/************************************************** + * Code related to the file chooser page + **************************************************/ + +/* csv_price_imp_file_confirm_cb + * + * call back for ok button in file chooser widget + */ +void +CsvImpPriceAssist::file_confirm_cb () +{ + auto file_name = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER(file_chooser)); + if (!file_name) + return; + + auto filepath = gnc_uri_get_path (file_name); + auto starting_dir = g_path_get_dirname (filepath); + + m_file_name = file_name; + gnc_set_default_directory (GNC_PREFS_GROUP, starting_dir); + + DEBUG("file_name selected is %s", m_file_name.c_str()); + DEBUG("starting directory is %s", starting_dir); + + g_free (filepath); + g_free (file_name); + g_free (starting_dir); + + /* Load the file into parse_data. */ + price_imp = std::unique_ptr(new GncPriceImport); + /* Assume data is CSV. User can later override to Fixed Width if needed */ + try + { + price_imp->file_format (GncImpFileFormat::CSV); + price_imp->load_file (m_file_name); + price_imp->tokenize (true); + } + catch (std::ifstream::failure& e) + { + /* File loading failed ... */ + gnc_error_dialog (GTK_WINDOW(csv_imp_asst), "%s", e.what()); + return; + } + catch (std::range_error &e) + { + /* Parsing failed ... */ + gnc_error_dialog (GTK_WINDOW(csv_imp_asst), "%s", e.what()); + return; + } + /* Get settings store and populate */ + preview_populate_settings_combo(); + gtk_combo_box_set_active (settings_combo, 0); + + // set over_write to false as default + price_imp->over_write (false); + + auto num = gtk_assistant_get_current_page (csv_imp_asst); + gtk_assistant_set_current_page (csv_imp_asst, num + 1); +} + + +/************************************************** + * Code related to the preview page + **************************************************/ + +/* Set the available presets in the settings combo box + */ +void CsvImpPriceAssist::preview_populate_settings_combo() +{ + // Clear the list store + auto model = gtk_combo_box_get_model (settings_combo); + gtk_list_store_clear (GTK_LIST_STORE(model)); + + // Append the default entry + auto presets = get_import_presets_price (); + for (auto preset : presets) + { + GtkTreeIter iter; + gtk_list_store_append (GTK_LIST_STORE(model), &iter); + /* FIXME we store the raw pointer to the preset, while it's + * managed by a shared pointer. This is dangerous because + * when the shared pointer goes out of scope, our pointer will dangle. + * For now this is safe, because the shared pointers in this case are + * long-lived, but this may need refactoring. + */ + gtk_list_store_set (GTK_LIST_STORE(model), &iter, SET_GROUP, preset.get(), SET_NAME, preset->m_name.c_str(), -1); + } +} + +/* Enable or disable the save and delete settings buttons + * depending on what is selected and entered as settings name + */ +void CsvImpPriceAssist::preview_handle_save_del_sensitivity (GtkComboBox* combo) +{ + GtkTreeIter iter; + auto can_delete = false; + auto can_save = false; + auto entry = gtk_bin_get_child (GTK_BIN(combo)); + auto entry_text = gtk_entry_get_text (GTK_ENTRY(entry)); + /* Handle sensitivity of the delete and save button */ + if (gtk_combo_box_get_active_iter (combo, &iter)) + { + CsvPriceImpSettings *preset; + GtkTreeModel *model = gtk_combo_box_get_model (combo); + gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); + + if (preset && !preset_is_reserved_name (preset->m_name)) + { + /* Current preset is not read_only, so buttons can be enabled */ + can_delete = true; + can_save = true; + } + } + else if (entry_text && (strlen (entry_text) > 0) && + !preset_is_reserved_name (std::string(entry_text))) + can_save = true; + + gtk_widget_set_sensitive (save_button, can_save); + gtk_widget_set_sensitive (del_button, can_delete); +} + +void +CsvImpPriceAssist::preview_settings_name (GtkEntry* entry) +{ + auto text = gtk_entry_get_text (entry); + if (text) + price_imp->settings_name(text); + + auto box = gtk_widget_get_parent (GTK_WIDGET(entry)); + auto combo = gtk_widget_get_parent (GTK_WIDGET(box)); + + preview_handle_save_del_sensitivity (GTK_COMBO_BOX(combo)); +} + +/* Use selected preset to configure the import. Triggered when + * a preset is selected in the settings combo. + */ +void +CsvImpPriceAssist::preview_settings_load () +{ + // Get the Active Selection + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter (settings_combo, &iter)) + return; + + CsvPriceImpSettings *preset = nullptr; + auto model = gtk_combo_box_get_model (settings_combo); + gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); + + if (!preset) + return; + + price_imp->settings (*preset); + if (preset->m_load_error) + gnc_error_dialog (GTK_WINDOW(csv_imp_asst), + "%s", _("There were problems reading some saved settings, continuing to load.\n" + "Please review and save again.")); + + preview_refresh (); + preview_handle_save_del_sensitivity (settings_combo); +} + +/* Callback to delete a settings entry + */ +void +CsvImpPriceAssist::preview_settings_delete () +{ + // Get the Active Selection + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter (settings_combo, &iter)) + return; + + CsvPriceImpSettings *preset = nullptr; + auto model = gtk_combo_box_get_model (settings_combo); + gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); + + auto response = gnc_ok_cancel_dialog (GTK_WINDOW(csv_imp_asst), + GTK_RESPONSE_CANCEL, + "%s", _("Delete the Import Settings.")); + if (response == GTK_RESPONSE_OK) + { + preset->remove(); + preview_populate_settings_combo(); + gtk_combo_box_set_active (settings_combo, 0); // Default + preview_refresh (); // Reset the widgets + } +} + +/* Callback to save the current settings to the gnucash state file. + */ +void +CsvImpPriceAssist::preview_settings_save () +{ + auto title = _("Save the Import Settings."); + auto new_name = price_imp->settings_name(); + + /* Check if the entry text matches an already existing preset */ + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter (settings_combo, &iter)) + { + + auto model = gtk_combo_box_get_model (settings_combo); + bool valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) + { + // Walk through the list, reading each row + CsvPriceImpSettings *preset; + gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); + + if (preset && (preset->m_name == std::string(new_name))) + { + auto response = gnc_ok_cancel_dialog (GTK_WINDOW(csv_imp_asst), + GTK_RESPONSE_OK, + "%s", _("Setting name already exists, over write?")); + if (response != GTK_RESPONSE_OK) + return; + + break; + } + valid = gtk_tree_model_iter_next (model, &iter); + } + } + + /* All checks passed, let's save this preset */ + if (!price_imp->save_settings()) + { + gnc_info_dialog (GTK_WINDOW(csv_imp_asst), + "%s", _("The settings have been saved.")); + + // Update the settings store + preview_populate_settings_combo(); + auto model = gtk_combo_box_get_model (settings_combo); + + // Get the first entry in model + GtkTreeIter iter; + bool valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) + { + // Walk through the list, reading each row + gchar *name = nullptr; + gtk_tree_model_get (model, &iter, SET_NAME, &name, -1); + + if (g_strcmp0 (name, new_name.c_str()) == 0) // Set Active, the one Saved. + gtk_combo_box_set_active_iter (settings_combo, &iter); + + g_free (name); + + valid = gtk_tree_model_iter_next (model, &iter); + } + } + else + gnc_error_dialog (GTK_WINDOW(csv_imp_asst), + "%s", _("There was a problem saving the settings, please try again.")); +} + +/* Callback triggered when user adjusts skip start lines + */ +void CsvImpPriceAssist::preview_update_skipped_rows () +{ + /* Update skip rows in the parser */ + price_imp->update_skipped_lines (gtk_spin_button_get_value_as_int (start_row_spin), + gtk_spin_button_get_value_as_int (end_row_spin), + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(skip_alt_rows_button)), + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(skip_errors_button))); + + /* And adjust maximum number of lines that can be skipped at each end accordingly */ + auto adj = gtk_spin_button_get_adjustment (end_row_spin); + gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size() + - price_imp->skip_start_lines() -1); + + adj = gtk_spin_button_get_adjustment (start_row_spin); + gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size() + - price_imp->skip_end_lines() - 1); + + preview_refresh_table (); +} + +/* Callback triggered when user clicks on Over Write option + */ +void CsvImpPriceAssist::preview_over_write (bool over) +{ + price_imp->over_write (over); +} + +/** Event handler for separator changes. This function is called + * whenever one of the widgets for configuring the separators (the + * separator checkbuttons or the custom separator entry) is + * changed. + * @param widget The widget that was changed + * @param info The data that is being configured + */ +void CsvImpPriceAssist::preview_update_separators (GtkWidget* widget) +{ + /* Only manipulate separator characters if the currently open file is + * csv separated. */ + if (price_imp->file_format() != GncImpFileFormat::CSV) + return; + + /* Add the corresponding characters to checked_separators for each + * button that is checked. */ + auto checked_separators = std::string(); + const auto stock_sep_chars = std::string (" \t,:;-"); + for (int i = 0; i < SEP_NUM_OF_TYPES; i++) + { + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(sep_button[i]))) + checked_separators += stock_sep_chars[i]; + } + + /* Add the custom separator if the user checked its button. */ + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(custom_cbutton))) + { + auto custom_sep = gtk_entry_get_text (custom_entry); + if (custom_sep[0] != '\0') /* Don't add a blank separator (bad things will happen!). */ + checked_separators += custom_sep; + } + + /* Set the parse options using the checked_separators list. */ + price_imp->separators (checked_separators); + + /* Parse the data using the new options. We don't want to reguess + * the column types because we want to leave the user's + * configurations intact. */ + try + { + price_imp->tokenize (false); + preview_refresh_table (); + } + catch (std::range_error &e) + { + /* Warn the user there was a problem and try to undo what caused + * the error. (This will cause a reparsing and ideally a usable + * configuration.) */ + gnc_error_dialog (GTK_WINDOW(csv_imp_asst), "Error in parsing"); + /* If we're here because the user changed the file format, we should just wait for the user + * to update the configuration */ + if (!widget) + return; + /* If the user changed the custom separator, erase that custom separator. */ + if (widget == GTK_WIDGET(custom_entry)) + gtk_entry_set_text (GTK_ENTRY(widget), ""); + /* If the user checked a checkbutton, toggle that checkbutton back. */ + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(widget), + !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(widget))); + return; + } +} + +/** Event handler for clicking one of the format type radio + * buttons. This occurs if the format (Fixed-Width or CSV) is changed. + * @param csv_button The "Separated" radio button + * @param info The display of the data being imported + */ +void CsvImpPriceAssist::preview_update_file_format () +{ + /* Set the parsing type correctly. */ + try + { + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(csv_button))) + { + price_imp->file_format (GncImpFileFormat::CSV); + g_signal_handlers_disconnect_by_func(G_OBJECT(treeview), + (gpointer)csv_price_imp_preview_treeview_clicked_cb, (gpointer)this); + gtk_widget_set_visible (separator_table, true); + gtk_widget_set_visible (fw_instructions_hbox, false); + } + else + { + price_imp->file_format (GncImpFileFormat::FIXED_WIDTH); + /* Enable context menu for adding/removing columns. */ + g_signal_connect (G_OBJECT(treeview), "button-press-event", + G_CALLBACK(csv_price_imp_preview_treeview_clicked_cb), (gpointer)this); + gtk_widget_set_visible (separator_table, false); + gtk_widget_set_visible (fw_instructions_hbox, true); + + } + price_imp->tokenize (false); + preview_refresh_table (); + } + catch (std::range_error &e) + { + /* Parsing failed ... */ + gnc_error_dialog (GTK_WINDOW (csv_imp_asst), "%s", e.what()); + return; + } + catch (...) + { + // FIXME Handle file loading errors (possibly thrown by file_format above) + PWARN("Got an error during file loading"); + } +} + +/** Event handler for a new encoding. This is called when the user + * selects a new encoding; the data is reparsed and shown to the + * user. + * @param selector The widget the user uses to select a new encoding + * @param encoding The encoding that the user selected + */ +void +CsvImpPriceAssist::preview_update_encoding (const char* encoding) +{ + /* This gets called twice every time a new encoding is selected. The + * second call actually passes the correct data; thus, we only do + * something the second time this is called. */ + + /* If this is the second time the function is called ... */ + if (encoding_selected_called) + { + std::string previous_encoding = price_imp->m_tokenizer->encoding(); + /* Try converting the new encoding and reparsing. */ + try + { + price_imp->encoding (encoding); + preview_refresh_table (); + } + catch (...) + { + /* If it fails, change back to the old encoding. */ + gnc_error_dialog (GTK_WINDOW (csv_imp_asst), "%s", _("Invalid encoding selected")); + go_charmap_sel_set_encoding (encselector, previous_encoding.c_str()); + } + } + encoding_selected_called = !encoding_selected_called; +} + +void +CsvImpPriceAssist::preview_update_date_format () +{ + price_imp->date_format (gtk_combo_box_get_active (GTK_COMBO_BOX(date_format_combo))); + preview_refresh_table (); +} + +void +CsvImpPriceAssist::preview_update_currency_format () +{ + price_imp->currency_format (gtk_combo_box_get_active (GTK_COMBO_BOX(currency_format_combo))); + preview_refresh_table (); +} + +void +CsvImpPriceAssist::preview_update_currency () +{ + gnc_commodity *comm = get_commodity_from_combo (GTK_COMBO_BOX(currency_selector)); + price_imp->to_currency (comm); + preview_refresh_table (); +} + +void +CsvImpPriceAssist::preview_update_commodity () +{ + gnc_commodity *comm = get_commodity_from_combo (GTK_COMBO_BOX(commodity_selector)); + price_imp->from_commodity (comm); + preview_refresh_table (); +} + +gboolean +csv_imp_preview_queue_rebuild_table (CsvImpPriceAssist *assist) +{ + assist->preview_refresh_table (); + return false; +} + +/* Internally used enum to access the columns in the comboboxes + * the user can click to set a type for each column of the data + */ +enum PreviewHeaderComboCols { COL_TYPE_NAME, COL_TYPE_ID }; +/* Internally used enum to access the first two (fixed) columns + * in the model used to display the prased data. + */ +enum PreviewDataTableCols { + PREV_COL_FCOLOR, + PREV_COL_BCOLOR, + PREV_COL_STRIKE, + PREV_COL_ERROR, + PREV_COL_ERR_ICON, + PREV_N_FIXED_COLS }; + + +void +CsvImpPriceAssist::preview_reparse_col_type (GncPricePropType type) +{ + auto column_types = price_imp->column_types_price(); + + // look for column type and force a reparse + auto col_type = std::find (column_types.begin(), + column_types.end(), type); + if (col_type != column_types.end()) + { + price_imp->set_column_type_price (col_type -column_types.begin(), + type, true); + } +} + +/** Event handler for the user selecting a new column type. When the + * user selects a new column type, that column's text must be changed + * to the selection, and any other columns containing that selection + * must be changed to "None" because we don't allow duplicates. + * @param renderer The renderer of the column the user changed + * @param path There is only 1 row in info->ctreeview, so this is always 0. + * @param new_text The text the user selected + * @param info The display of the data being imported + */ +void CsvImpPriceAssist::preview_update_col_type (GtkComboBox* cbox) +{ + /* Get the new text */ + GtkTreeIter iter; + auto model = gtk_combo_box_get_model (cbox); + gtk_combo_box_get_active_iter (cbox, &iter); + auto new_col_type = GncPricePropType::NONE; + gtk_tree_model_get (model, &iter, COL_TYPE_ID, &new_col_type, -1); + + auto col_num = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT(cbox), "col-num")); + + auto column_types = price_imp->column_types_price(); + auto old_col_type = column_types.at(col_num); + + price_imp->set_column_type_price (col_num, new_col_type); + + // if old_col_type is TO_CURRENCY, force a reparse of commodity + if (old_col_type == GncPricePropType::TO_CURRENCY) + { + // look for a from_commodity column to reparse + preview_reparse_col_type (GncPricePropType::FROM_COMMODITY); + } + + // if old_col_type is FROM_COMMODITY, force a reparse of currency + if (old_col_type == GncPricePropType::FROM_COMMODITY) + { + // look for a to_currency column to reparse + preview_reparse_col_type (GncPricePropType::TO_CURRENCY); + } + + /* Delay rebuilding our data table to avoid critical warnings due to + * pending events still acting on them after this event is processed. + */ + g_idle_add ((GSourceFunc)csv_imp_preview_queue_rebuild_table, this); +} + +/*======================================================================*/ +/*================== Beginning of Gnumeric Code ========================*/ + +/* The following is code copied from Gnumeric 1.7.8 licensed under the + * GNU General Public License version 2 and/or version 3. It is from the file + * gnumeric/gnucash/dialogs/dialog-stf-fixed-page.c, and it has been + * modified slightly to work within GnuCash. */ + +/* + * Copyright 2001 Almer S. Tigelaar + * Copyright 2003 Morten Welinder + * + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +enum +{ + CONTEXT_STF_IMPORT_MERGE_LEFT = 1, + CONTEXT_STF_IMPORT_MERGE_RIGHT = 2, + CONTEXT_STF_IMPORT_SPLIT = 3, + CONTEXT_STF_IMPORT_WIDEN = 4, + CONTEXT_STF_IMPORT_NARROW = 5 +}; + +static GnumericPopupMenuElement const popup_elements[] = +{ + { + N_("Merge with column on _left"), "list-remove", + 0, 1 << CONTEXT_STF_IMPORT_MERGE_LEFT, CONTEXT_STF_IMPORT_MERGE_LEFT + }, + { + N_("Merge with column on _right"), "list-remove", + 0, 1 << CONTEXT_STF_IMPORT_MERGE_RIGHT, CONTEXT_STF_IMPORT_MERGE_RIGHT + }, + { "", nullptr, 0, 0, 0 }, + { + N_("_Split this column"), nullptr, + 0, 1 << CONTEXT_STF_IMPORT_SPLIT, CONTEXT_STF_IMPORT_SPLIT + }, + { "", nullptr, 0, 0, 0 }, + { + N_("_Widen this column"), "go-next", + 0, 1 << CONTEXT_STF_IMPORT_WIDEN, CONTEXT_STF_IMPORT_WIDEN + }, + { + N_("_Narrow this column"), "go-previous", + 0, 1 << CONTEXT_STF_IMPORT_NARROW, CONTEXT_STF_IMPORT_NARROW + }, + { nullptr, nullptr, 0, 0, 0 }, +}; + +uint32_t CsvImpPriceAssist::get_new_col_rel_pos (GtkTreeViewColumn *tcol, int dx) +{ + auto renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT(tcol)); + auto cell = GTK_CELL_RENDERER(renderers->data); + g_list_free (renderers); + PangoFontDescription *font_desc; + g_object_get (G_OBJECT(cell), "font_desc", &font_desc, nullptr); + + PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET(treeview), "x"); + pango_layout_set_font_description (layout, font_desc); + int width; + pango_layout_get_pixel_size (layout, &width, nullptr); + if (width < 1) width = 1; + uint32_t charindex = (dx + width / 2) / width; + g_object_unref (layout); + pango_font_description_free (font_desc); + + return charindex; +} + +gboolean +fixed_context_menu_handler_price (GnumericPopupMenuElement const *element, + gpointer userdata) +{ + auto info = (CsvImpPriceAssist*)userdata; + auto fwtok = dynamic_cast(info->price_imp->m_tokenizer.get()); + + switch (element->index) + { + case CONTEXT_STF_IMPORT_MERGE_LEFT: + fwtok->col_delete (info->fixed_context_col - 1); + break; + case CONTEXT_STF_IMPORT_MERGE_RIGHT: + fwtok->col_delete (info->fixed_context_col); + break; + case CONTEXT_STF_IMPORT_SPLIT: + fwtok->col_split (info->fixed_context_col, info->fixed_context_offset); + break; + case CONTEXT_STF_IMPORT_WIDEN: + fwtok->col_widen (info->fixed_context_col); + break; + case CONTEXT_STF_IMPORT_NARROW: + fwtok->col_narrow (info->fixed_context_col); + break; + default: + ; /* Nothing */ + } + + try + { + info->price_imp->tokenize (false); + } + catch(std::range_error& e) + { + gnc_error_dialog (GTK_WINDOW (info->csv_imp_asst), "%s", e.what()); + return false; + } + info->preview_refresh_table (); + return true; +} + +void +CsvImpPriceAssist::fixed_context_menu (GdkEventButton *event, + int col, int offset) +{ + auto fwtok = dynamic_cast(price_imp->m_tokenizer.get()); + fixed_context_col = col; + fixed_context_offset = offset; + + int sensitivity_filter = 0; + if (!fwtok->col_can_delete (col - 1)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_MERGE_LEFT); + if (!fwtok->col_can_delete (col)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_MERGE_RIGHT); + if (!fwtok->col_can_split (col, offset)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_SPLIT); + if (!fwtok->col_can_widen (col)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_WIDEN); + if (!fwtok->col_can_narrow (col)) + sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_NARROW); + + gnumeric_create_popup_menu (popup_elements, &fixed_context_menu_handler_price, + this, 0, + sensitivity_filter, event); +} + +/*===================== End of Gnumeric Code ===========================*/ +/*======================================================================*/ +void +CsvImpPriceAssist::preview_split_column (int col, int offset) +{ + auto fwtok = dynamic_cast(price_imp->m_tokenizer.get()); + fwtok->col_split (col, offset); + try + { + price_imp->tokenize (false); + } + catch (std::range_error& e) + { + gnc_error_dialog (GTK_WINDOW (csv_imp_asst), "%s", e.what()); + return; + } + preview_refresh_table(); +} + +/** Event handler for clicking on column headers. This function is + * called whenever the user clicks on column headers in + * preview->treeview to modify columns when in fixed-width mode. + * @param button The button at the top of a column of the treeview + * @param event The event that happened (where the user clicked) + * @param info The data being configured + * @returns true if further processing of this even should stop, false + * if other event handlers can have a go at this as well + */ +void +CsvImpPriceAssist::preview_update_fw_columns (GtkTreeView* treeview, GdkEventButton* event) +{ + /* Nothing to do if this was not triggered on our treeview body */ + if (event->window != gtk_tree_view_get_bin_window (treeview)) + return; + + /* Find the column that was clicked. */ + GtkTreeViewColumn *tcol = nullptr; + int cell_x = 0; + auto success = gtk_tree_view_get_path_at_pos (treeview, + (int)event->x, (int)event->y, + nullptr, &tcol, &cell_x, nullptr); + if (!success) + return; + + /* Stop if no column found in this treeview (-1) or + * if column is the error messages column (0) */ + auto tcol_list = gtk_tree_view_get_columns(treeview); + auto tcol_num = g_list_index (tcol_list, tcol); + g_list_free (tcol_list); + if (tcol_num <= 0) + return; + + /* Data columns in the treeview are offset by one + * because the first column is the error column + */ + auto dcol = tcol_num - 1; + auto offset = get_new_col_rel_pos (tcol, cell_x); + if (event->type == GDK_2BUTTON_PRESS && event->button == 1) + /* Double clicks can split columns. */ + preview_split_column (dcol, offset); + else if (event->type == GDK_BUTTON_PRESS && event->button == 3) + /* Right clicking brings up a context menu. */ + fixed_context_menu (event, dcol, offset); +} + +/* Convert state info (errors/skipped) in visual feedback to decorate the preview table */ +void +CsvImpPriceAssist::preview_row_fill_state_cells (GtkListStore *store, GtkTreeIter *iter, + std::string& err_msg, bool skip) +{ + /* Extract error status for all non-skipped lines */ + const char *c_err_msg = nullptr; + const char *icon_name = nullptr; + const char *fcolor = nullptr; + const char *bcolor = nullptr; + if (!skip && !err_msg.empty()) + { + fcolor = "black"; + bcolor = "pink"; + c_err_msg = err_msg.c_str(); + icon_name = "dialog-error"; + } + gtk_list_store_set (store, iter, + PREV_COL_FCOLOR, fcolor, + PREV_COL_BCOLOR, bcolor, + PREV_COL_STRIKE, skip, + PREV_COL_ERROR, c_err_msg, + PREV_COL_ERR_ICON, icon_name, -1); +} + +/* Helper function that creates a combo_box using a model + * with valid column types and selects the given column type + */ +GtkWidget* +CsvImpPriceAssist::preview_cbox_factory (GtkTreeModel* model, uint32_t colnum) +{ + GtkTreeIter iter; + auto cbox = gtk_combo_box_new_with_model(model); + + /* Set up a renderer for this combobox. */ + auto renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT(cbox), + renderer, true); + gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT(cbox), + renderer, "text", COL_TYPE_NAME); + + auto valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) + { + gint stored_col_type; + gtk_tree_model_get (model, &iter, + COL_TYPE_ID, &stored_col_type, -1); + if (stored_col_type == static_cast( price_imp->column_types_price()[colnum])) + break; + valid = gtk_tree_model_iter_next(model, &iter); + } + if (valid) + gtk_combo_box_set_active_iter (GTK_COMBO_BOX(cbox), &iter); + + g_object_set_data (G_OBJECT(cbox), "col-num", GUINT_TO_POINTER(colnum)); + g_signal_connect (G_OBJECT(cbox), "changed", + G_CALLBACK(csv_price_imp_preview_col_type_changed_cb), (gpointer)this); + + gtk_widget_show (cbox); + return cbox; +} + +void +CsvImpPriceAssist::preview_style_column (uint32_t col_num, GtkTreeModel* model) +{ + auto col = gtk_tree_view_get_column (treeview, col_num); + auto renderer = static_cast(gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(col))->data); + + /* First column -the error status column- is rendered differently */ + if (col_num == 0) + { + gtk_tree_view_column_set_attributes (col, renderer, + "icon-name", PREV_COL_ERR_ICON, + "cell-background", PREV_COL_BCOLOR, nullptr); + g_object_set (G_OBJECT(renderer), "stock-size", GTK_ICON_SIZE_MENU, nullptr); + g_object_set (G_OBJECT(col), "sizing", GTK_TREE_VIEW_COLUMN_FIXED, + "fixed-width", 20, nullptr); + gtk_tree_view_column_set_resizable (col, false); + } + else + { + gtk_tree_view_column_set_attributes (col, renderer, + "foreground", PREV_COL_FCOLOR, + "background", PREV_COL_BCOLOR, + "strikethrough", PREV_COL_STRIKE, + "text", col_num + PREV_N_FIXED_COLS -1, nullptr); + + /* We want a monospace font fixed-width data is properly displayed. */ + g_object_set (G_OBJECT(renderer), "family", "monospace", nullptr); + + /* Add a combobox to select column types as column header. Each uses the same + * common model for the dropdown list. The selected value is taken + * from the column_types vector. */ + auto cbox = preview_cbox_factory (GTK_TREE_MODEL(model), col_num - 1); + gtk_tree_view_column_set_widget (col, cbox); + + /* Enable resizing of the columns. */ + gtk_tree_view_column_set_resizable (col, true); + gtk_tree_view_column_set_clickable (col, true); + } +} + +/* Helper to create a shared store for the header comboboxes in the preview treeview. + * It holds the possible column types */ +GtkTreeModel* +make_column_header_model_price (void) +{ + auto combostore = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT); + for (auto col_type : gnc_price_col_type_strs) + { + GtkTreeIter iter; + gtk_list_store_append (combostore, &iter); + gtk_list_store_set (combostore, &iter, + COL_TYPE_NAME, _(col_type.second), + COL_TYPE_ID, static_cast(col_type.first), -1); + } + return GTK_TREE_MODEL(combostore); +} + +/* Updates the preview treeview to show the data as parsed based on the user's + * import parameters. + */ +void CsvImpPriceAssist::preview_refresh_table () +{ + preview_validate_settings (); + + /* Create a new liststore to hold status and data from the file being imported. + The first columns hold status information (row-color, row-errors, row-error-icon,... + All following columns represent the tokenized data as strings. */ + auto ncols = PREV_N_FIXED_COLS + price_imp->column_types_price().size(); + auto model_col_types = g_new (GType, ncols); + model_col_types[PREV_COL_FCOLOR] = G_TYPE_STRING; + model_col_types[PREV_COL_BCOLOR] = G_TYPE_STRING; + model_col_types[PREV_COL_ERROR] = G_TYPE_STRING; + model_col_types[PREV_COL_ERR_ICON] = G_TYPE_STRING; + model_col_types[PREV_COL_STRIKE] = G_TYPE_BOOLEAN; + for (guint i = PREV_N_FIXED_COLS; i < ncols; i++) + model_col_types[i] = G_TYPE_STRING; + auto store = gtk_list_store_newv (ncols, model_col_types); + g_free (model_col_types); + + /* Fill the data liststore with data from importer object. */ + for (auto parse_line : price_imp->m_parsed_lines) + { + /* Fill the state cells */ + GtkTreeIter iter; + gtk_list_store_append (store, &iter); + preview_row_fill_state_cells (store, &iter, + std::get(parse_line), std::get(parse_line)); + + /* Fill the data cells. */ + for (auto cell_str_it = std::get(parse_line).cbegin(); cell_str_it != std::get(parse_line).cend(); cell_str_it++) + { + uint32_t pos = PREV_N_FIXED_COLS + cell_str_it - std::get(parse_line).cbegin(); + gtk_list_store_set (store, &iter, pos, cell_str_it->c_str(), -1); + } + } + gtk_tree_view_set_model (treeview, GTK_TREE_MODEL(store)); + gtk_tree_view_set_tooltip_column (treeview, PREV_COL_ERROR); + + /* Adjust treeview to go with the just created model. This consists of adding + * or removing columns and resetting any parameters related to how + * the columns and data should be rendered. + */ + + /* Start with counting the current number of columns (ntcols) + * we have in the treeview */ + auto columns = gtk_tree_view_get_columns (treeview); + auto ntcols = g_list_length(columns); + g_list_free (columns); + + /* Drop redundant columns if the model has less data columns than the new model + * ntcols = n° of columns in treeview (1 error column + x data columns) + * ncols = n° of columns in model (fixed state columns + x data columns) + */ + while (ntcols > ncols - PREV_N_FIXED_COLS + 1) + { + auto col = gtk_tree_view_get_column (treeview, ntcols - 1); + gtk_tree_view_column_clear (col); + ntcols = gtk_tree_view_remove_column(treeview, col); + } + + /* Insert columns if the model has more data columns than the treeview. */ + while (ntcols < ncols - PREV_N_FIXED_COLS + 1) + { + /* Default cell renderer is text, except for the first (error) column */ + auto renderer = gtk_cell_renderer_text_new(); + if (ntcols == 0) + renderer = gtk_cell_renderer_pixbuf_new(); // Error column uses an icon + auto col = gtk_tree_view_column_new (); + gtk_tree_view_column_pack_start (col, renderer, false); + ntcols = gtk_tree_view_append_column (treeview, col); + } + + /* Reset column attributes as they are undefined after recreating the model */ + auto combostore = make_column_header_model_price (); + for (uint32_t i = 0; i < ntcols; i++) + preview_style_column (i, combostore); + + auto column_types = price_imp->column_types_price(); + + // look for a commodity column, clear the commodity combo + auto col_type_comm = std::find (column_types.begin(), + column_types.end(), GncPricePropType::FROM_COMMODITY); + if (col_type_comm != column_types.end()) + { + g_signal_handlers_block_by_func (commodity_selector, (gpointer) csv_price_imp_preview_commodity_sel_cb, this); + set_commodity_for_combo (GTK_COMBO_BOX(commodity_selector), nullptr); + g_signal_handlers_unblock_by_func (commodity_selector, (gpointer) csv_price_imp_preview_commodity_sel_cb, this); + } + + // look for a currency column, clear the currency combo + auto col_type_curr = std::find (column_types.begin(), + column_types.end(), GncPricePropType::TO_CURRENCY); + if (col_type_curr != column_types.end()) + { + g_signal_handlers_block_by_func (currency_selector, (gpointer) csv_price_imp_preview_currency_sel_cb, this); + set_commodity_for_combo (GTK_COMBO_BOX(currency_selector), nullptr); + g_signal_handlers_unblock_by_func (currency_selector, (gpointer) csv_price_imp_preview_currency_sel_cb, this); + } + + /* Release our reference for the stores to allow proper memory management. */ + g_object_unref (store); + g_object_unref (combostore); + + /* Make the things actually appear. */ + gtk_widget_show_all (GTK_WIDGET(treeview)); +} + +/* Update the preview page based on the current state of the importer. + * Should be called when settings are changed. + */ +void +CsvImpPriceAssist::preview_refresh () +{ + // Set start row + auto adj = gtk_spin_button_get_adjustment (start_row_spin); + gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size()); + gtk_spin_button_set_value (start_row_spin, + price_imp->skip_start_lines()); + + // Set end row + adj = gtk_spin_button_get_adjustment (end_row_spin); + gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size()); + gtk_spin_button_set_value (end_row_spin, + price_imp->skip_end_lines()); + + // Set Alternate rows + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(skip_alt_rows_button), + price_imp->skip_alt_lines()); + + // Set over-write indicator + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(over_write_cbutton), + price_imp->over_write()); + + // Set Import Format + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(csv_button), + (price_imp->file_format() == GncImpFileFormat::CSV)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(fixed_button), + (price_imp->file_format() != GncImpFileFormat::CSV)); + + // This section deals with the combo's and character encoding + gtk_combo_box_set_active (GTK_COMBO_BOX(date_format_combo), + price_imp->date_format()); + gtk_combo_box_set_active (GTK_COMBO_BOX(currency_format_combo), + price_imp->currency_format()); + go_charmap_sel_set_encoding (encselector, price_imp->encoding().c_str()); + + // Set the commodity and currency combos + set_commodity_for_combo(GTK_COMBO_BOX(commodity_selector), + price_imp->from_commodity()); + + set_commodity_for_combo(GTK_COMBO_BOX(currency_selector), + price_imp->to_currency()); + + // Handle separator checkboxes and custom field, only relevant if the file format is csv + if (price_imp->file_format() == GncImpFileFormat::CSV) + { + auto separators = price_imp->separators(); + const auto stock_sep_chars = std::string (" \t,:;-"); + for (int i = 0; i < SEP_NUM_OF_TYPES; i++) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(sep_button[i]), + separators.find (stock_sep_chars[i]) != std::string::npos); + + // If there are any other separators in the separators string, + // add them as custom separators + auto pos = separators.find_first_of (stock_sep_chars); + while (!separators.empty() && pos != std::string::npos) + { + separators.erase(pos); + pos = separators.find_first_of (stock_sep_chars); + } + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(custom_cbutton), + !separators.empty()); + gtk_entry_set_text (GTK_ENTRY(custom_entry), separators.c_str()); + } + // Repopulate the parsed data table + g_idle_add ((GSourceFunc)csv_imp_preview_queue_rebuild_table, this); +} + +/* Check if all selected data can be parsed sufficiently to continue + */ +void CsvImpPriceAssist::preview_validate_settings () +{ + /* Allow the user to proceed only if there are no inconsistencies in the settings */ + auto error_msg = price_imp->verify(); + gtk_assistant_set_page_complete (csv_imp_asst, preview_page, error_msg.empty()); + gtk_label_set_markup(GTK_LABEL(instructions_label), error_msg.c_str()); + gtk_widget_set_visible (GTK_WIDGET(instructions_image), !error_msg.empty()); +} + +/******************************************************* + * Assistant page prepare functions + *******************************************************/ + +void +CsvImpPriceAssist::assist_file_page_prepare () +{ + /* Set the default directory */ + auto starting_dir = gnc_get_default_directory (GNC_PREFS_GROUP); + if (starting_dir) + { + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(file_chooser), starting_dir); + g_free (starting_dir); + } +} + +void +CsvImpPriceAssist::assist_preview_page_prepare () +{ + /* Disable the Forward Assistant Button */ + gtk_assistant_set_page_complete (csv_imp_asst, preview_page, false); + + /* Load the data into the treeview. */ + preview_refresh_table (); +} + +void +CsvImpPriceAssist::assist_confirm_page_prepare () +{ + /* Confirm Page */ +} + +void +CsvImpPriceAssist::assist_summary_page_prepare () +{ + auto text = std::string(""); + text += _("The prices were imported from the file '") + m_file_name + "'."; + text += _("\n\nThere were ") + std::to_string(price_imp->m_prices_added); + text += _(" Prices added, ") + std::to_string(price_imp->m_prices_duplicated); + text += _(" duplicated and ") + std::to_string(price_imp->m_prices_replaced); + text += _(" replaced."); + + gtk_label_set_markup (GTK_LABEL(summary_label), text.c_str()); +} + +void +CsvImpPriceAssist::assist_prepare_cb (GtkWidget *page) +{ + if (page == file_page) + assist_file_page_prepare (); + else if (page == preview_page) + assist_preview_page_prepare (); + else if (page == confirm_page) + assist_confirm_page_prepare (); + else if (page == summary_page) + assist_summary_page_prepare (); +} + +void +CsvImpPriceAssist::assist_finish () +{ + /* Start the import */ + /* Create prices from the parsed data */ + try + { + price_imp->create_prices (); + } + catch (const std::invalid_argument& err) + { + /* Oops! This shouldn't happen when using the import assistant ! + * Inform the user and go back to the preview page. + */ + gnc_error_dialog (GTK_WINDOW(csv_imp_asst), + _("An unexpected error has occurred while creating prices. Please report this as a bug.\n\n" + "Error message:\n%s"), err.what()); + gtk_assistant_set_current_page (csv_imp_asst, 2); + } +} + +void +CsvImpPriceAssist::assist_compmgr_close () +{ + gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(csv_imp_asst)); + gtk_widget_destroy (GTK_WIDGET(csv_imp_asst)); +} + +static void +csv_price_imp_close_handler (gpointer user_data) +{ + auto info = (CsvImpPriceAssist*)user_data; + info->assist_compmgr_close(); +} + +/********************************************************************\ + * gnc_file_csv_price_import * + * opens up a assistant to import prices. * + * * + * Args: none * + * Return: nothing * +\********************************************************************/ +void +gnc_file_csv_price_import(void) +{ + auto info = new CsvImpPriceAssist; + gnc_register_gui_component (ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS, + nullptr, csv_price_imp_close_handler, + info); +} diff --git a/gnucash/import-export/csv-imp/assistant-csv-price-import.glade b/gnucash/import-export/csv-imp/assistant-csv-price-import.glade new file mode 100644 index 0000000000..b1ba9c0a8c --- /dev/null +++ b/gnucash/import-export/csv-imp/assistant-csv-price-import.glade @@ -0,0 +1,1122 @@ + + + + + + 1000 + 1 + 10 + + + + + + + + + + + + + + + + + + + + + + + 1000 + 1 + 10 + + + False + 12 + CSV Price Import + 400 + 500 + + + + + + + + + + + + + + True + False + This assistant will help you import Prices from a CSV file. + +There is a minimum number of columns that have to be present for a successful import, these are Date, Amount, Commodity From and Currency To. If all entries are for the same Commodity / Currency then you can select them and then the columns will be Date and Amount. + +Various options exist for specifying the delimiter as well as a fixed width option. With the fixed width option, double click on the table of rows displayed to set a column width, then right mouse to change if required. + +Examples are "RR.L","21/11/2016",5.345,"GBP" and "USD","2016-11-21",1.56,"GBP" + +There is an option for specifying the start row, end row and an option to skip alternate rows beginning from the start row which can be used if you have some header text. Also there is an option to over write existing prices for that day if required. + +Lastly, for repeated imports the preview page has buttons to Load and Save the settings. To save the settings, tweak the settings to your preferences (optionally starting from an existing preset), then (optionally change the settings name and press the Save Settings button. Note you can't save to built-in presets. + +This operation is not reversable, so make sure you have a working backup. + +Click on 'Forward' to proceed or 'Cancel' to Abort Import. + True + + + intro + True + + + + + True + False + 12 + vertical + + + True + False + +Select location and file name for the Import, then click 'OK'... + + True + + + False + False + 0 + + + + + Select File for Import + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 12 + vertical + 2 + + + True + False + 5 + 5 + + + True + False + 0 + in + + + True + False + 5 + 5 + 5 + + + True + False + + + True + True + True + Delete Settings + + + + True + False + edit-delete + + + + + False + False + end + 0 + + + + + True + True + True + Save Settings + + + + True + False + document-save + + + + + False + False + end + 2 + + + + + + + + + True + False + start + <b>Load and Save Settings</b> + True + False + + + + + 0 + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + none + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + 5 + + + True + False + vertical + + + True + False + + + Separators + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + True + + + + 0 + 0 + + + + + Fixed-Width + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + csv_button + + + 1 + 0 + + + + + True + False + + + 0 + 1 + 2 + + + + + False + False + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 3 + + + Space + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + + + + 0 + 0 + + + + + Tab + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + + + + 1 + 0 + + + + + Comma (,) + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + True + + + + 2 + 0 + + + + + Colon (:) + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + + + + 2 + 1 + + + + + Semicolon (;) + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + + + + 0 + 1 + + + + + Hyphen (-) + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + + + + 1 + 1 + + + + + Custom + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + + + + 0 + 2 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + False + False + + + + 1 + 2 + + + + + + + + False + False + 1 + + + + + False + True + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + dialog-information + + + False + True + 2 + 0 + + + + + True + False + + + True + False + + True + True + + + 0 + 0 + + + + + True + False + start + Double-click anywhere on the table below to insert a column break + True + True + + + 1 + 0 + + + + + True + False + + True + True + + + 0 + 1 + + + + + True + False + start + Right-click anywhere in a column to modify it (widen, narrow, merge) + True + True + + + 1 + 1 + + + + + True + True + 1 + + + + + False + False + 2 + + + + + True + False + + + True + False + + + 0 + 0 + + + + + Allow existing prices to be over written. + True + True + False + False + Normally prices are not over written, select this to change that. This setting is not saved. + True + + + + 0 + 1 + + + + + False + False + 3 + + + + + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + <b>File Format</b> + True + + + + + 0 + 1 + + + + + True + False + 0 + none + + + True + False + 5 + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + vertical + + + True + False + 5 + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + + + 1 + 1 + + + + + True + False + start + Date Format + + + 0 + 1 + + + + + True + False + 0 + + + 1 + 2 + + + + + True + False + start + Currency Format + + + 0 + 2 + + + + + True + False + start + Encoding + + + 0 + 0 + + + + + True + False + 0 + + + 1 + 0 + + + + + True + False + start + Leading Lines to Skip + + + 0 + 4 + + + + + True + False + start + Trailing Lines to Skip + + + 0 + 5 + + + + + True + False + + + True + True + + False + False + start_row_adj + True + + + + False + False + 0 + + + + + 1 + 4 + + + + + True + False + + + True + True + + False + False + end_row_adj + True + + + + False + False + 0 + + + + + 1 + 5 + + + + + True + False + + + 0 + 3 + 2 + + + + + False + True + 0 + + + + + Skip alternate lines + True + True + False + Starting from the first line that is actually imported every second line will be skipped. This option will take the leading lines to skip into account as well. +For example +* if 'Leading Lines to Skip' is set to 3, the first line to import will be line 4. Lines 5, 7, 9,... will be skipped. +* if 'Leading Lines to Skip' is set to 4, the first line to import will be line 5. Lines 6, 8, 10,... will be skipped. + start + True + + + + False + True + 1 + + + + + + + + + True + False + start + <b>Miscellaneous</b> + True + + + + + 1 + 1 + + + + + + + + False + False + 0 + + + + + True + False + + + True + False + 0 + none + + + True + False + 5 + 5 + 5 + + + True + False + + + True + False + liststore1 + + + + 0 + + + + + False + False + 0 + + + + + + + + + True + False + <b>Commodity From</b> + True + + + + + False + False + 0 + + + + + True + False + 0 + none + + + True + False + 5 + 5 + 5 + + + True + False + + + True + False + liststore1 + + + + 0 + + + + + False + False + 0 + + + + + + + + + True + False + <b>Currency To</b> + True + + + + + False + False + 2 + + + + + False + False + 1 + + + + + True + True + + + True + False + + + True + False + vertical + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + both + + + + + + False + True + 0 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + both + + + + + + True + True + 1 + + + + + + + + + True + True + 2 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + dialog-information + + + False + True + 2 + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + Select the type of each column to import. + + + True + True + 1 + + + + + False + False + 5 + 3 + + + + + True + False + + + Skip Errors + True + False + right + True + + + + False + False + end + 0 + + + + + False + False + 4 + + + + + intro + True + + + + + True + False + 12 + vertical + + + True + False + + + True + False + <b>Press Apply to add the Prices. +Cancel to abort.</b> + True + center + True + + + + + True + False + 0 + + + + + confirm + Import Prices Now + + + + + True + False + 12 + vertical + + + True + False + label + True + True + + + True + True + 0 + + + + + summary + Import Summary + True + + + + + False + + + + + + diff --git a/gnucash/import-export/csv-imp/assistant-csv-price-import.h b/gnucash/import-export/csv-imp/assistant-csv-price-import.h new file mode 100644 index 0000000000..213b622e2e --- /dev/null +++ b/gnucash/import-export/csv-imp/assistant-csv-price-import.h @@ -0,0 +1,36 @@ +/*******************************************************************\ + * assistant-csv-price-import.h -- An assistant for importing * + * Prices from a file. * + * * + * Copyright (C) 2017 Robert Fewell * + * * + * 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@gnu.org * +\********************************************************************/ +/** @file assistant-csv-price-import.h + @brief CSV Import Assistant + @author Copyright (c) 2017 Robert Fewell +*/ +#ifndef GNC_ASSISTANT_CSV_IMPORT_PRICE_H +#define GNC_ASSISTANT_CSV_IMPORT_PRICE_H + + +/** The gnc_file_csv_price_import() will let the user import the + * commodity prices from a file. + */ +void gnc_file_csv_price_import (void); +#endif diff --git a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp index 1bfa08e166..f593a0c8cb 100644 --- a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp +++ b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp @@ -58,7 +58,7 @@ extern "C" #include "go-charmap-sel.h" } -#include "gnc-csv-trans-settings.hpp" +#include "gnc-csv-trans-import-settings.hpp" #include "gnc-tx-import.hpp" #include "gnc-fw-tokenizer.hpp" #include "gnc-csv-tokenizer.hpp" @@ -676,8 +676,7 @@ void CsvImpTransAssist::preview_populate_settings_combo() gtk_list_store_clear (GTK_LIST_STORE(model)); // Append the default entry - - auto presets = get_trans_presets (); + auto presets = get_import_presets_trans (); for (auto preset : presets) { GtkTreeIter iter; @@ -705,11 +704,11 @@ void CsvImpTransAssist::preview_handle_save_del_sensitivity (GtkComboBox* combo) /* Handle sensitivity of the delete and save button */ if (gtk_combo_box_get_active_iter (combo, &iter)) { - CsvTransSettings *preset; + CsvTransImpSettings *preset; GtkTreeModel *model = gtk_combo_box_get_model (combo); gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); - if (preset && !trans_preset_is_reserved_name (preset->m_name)) + if (preset && !preset_is_reserved_name (preset->m_name)) { /* Current preset is not read_only, so buttons can be enabled */ can_delete = true; @@ -717,7 +716,7 @@ void CsvImpTransAssist::preview_handle_save_del_sensitivity (GtkComboBox* combo) } } else if (entry_text && (strlen (entry_text) > 0) && - !trans_preset_is_reserved_name (std::string(entry_text))) + !preset_is_reserved_name (std::string(entry_text))) can_save = true; gtk_widget_set_sensitive (save_button, can_save); @@ -734,7 +733,7 @@ CsvImpTransAssist::preview_settings_name (GtkEntry* entry) auto box = gtk_widget_get_parent (GTK_WIDGET(entry)); auto combo = gtk_widget_get_parent (GTK_WIDGET(box)); - + preview_handle_save_del_sensitivity (GTK_COMBO_BOX(combo)); } @@ -750,7 +749,7 @@ CsvImpTransAssist::preview_settings_load () if (!gtk_combo_box_get_active_iter (settings_combo, &iter)) return; - CsvTransSettings *preset = nullptr; + CsvTransImpSettings *preset = nullptr; auto model = gtk_combo_box_get_model (settings_combo); gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); @@ -777,7 +776,7 @@ CsvImpTransAssist::preview_settings_delete () if (!gtk_combo_box_get_active_iter (settings_combo, &iter)) return; - CsvTransSettings *preset = nullptr; + CsvTransImpSettings *preset = nullptr; auto model = gtk_combo_box_get_model (settings_combo); gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); @@ -810,7 +809,7 @@ CsvImpTransAssist::preview_settings_save () while (valid) { // Walk through the list, reading each row - CsvTransSettings *preset; + CsvTransImpSettings *preset; gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1); if (preset && (preset->m_name == std::string(new_name))) diff --git a/gnucash/import-export/csv-imp/gnc-csv-trans-settings.cpp b/gnucash/import-export/csv-imp/gnc-csv-import-settings.cpp similarity index 53% rename from gnucash/import-export/csv-imp/gnc-csv-trans-settings.cpp rename to gnucash/import-export/csv-imp/gnc-csv-import-settings.cpp index 6611875380..7c5e362bfd 100644 --- a/gnucash/import-export/csv-imp/gnc-csv-trans-settings.cpp +++ b/gnucash/import-export/csv-imp/gnc-csv-import-settings.cpp @@ -1,5 +1,5 @@ /*******************************************************************\ - * gnc-csv-trans-settings.c -- Save and Load CSV Import Settings * + * gnc-csv-import-settings.cpp -- Save and Load CSV Import Settings * * * * Copyright (C) 2014 Robert Fewell * * * @@ -20,13 +20,13 @@ * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * * Boston, MA 02110-1301, USA gnu@gnu.org * \********************************************************************/ -/** @file gnc-csv-trans-settings.c +/** @file gnc-csv-import-settings.cpp @brief CSV Import Settings @author Copyright (c) 2014 Robert Fewell @author Copyright (c) 2016 Geert Janssens */ -#include "gnc-csv-trans-settings.hpp" +#include "gnc-csv-import-settings.hpp" #include extern "C" @@ -41,15 +41,15 @@ extern "C" #include "gnc-ui-util.h" } -const std::string csv_group_prefix{"CSV - "}; +const std::string csv_group_prefix{"CSV-"}; const std::string no_settings{N_("No Settings")}; const std::string gnc_exp{N_("GnuCash Export Format")}; + #define CSV_NAME "Name" #define CSV_FORMAT "CsvFormat" #define CSV_SKIP_ALT "SkipAltLines" #define CSV_SKIP_START "SkipStartLines" #define CSV_SKIP_END "SkipEndLines" -#define CSV_MULTI_SPLIT "MultiSplit" #define CSV_SEP "Separators" @@ -60,115 +60,10 @@ const std::string gnc_exp{N_("GnuCash Export Format")}; #define CSV_CURRENCY "CurrencyFormat" #define CSV_ENCODING "Encoding" -#define CSV_COL_TYPES "ColumnTypes" #define CSV_COL_WIDTHS "ColumnWidths" -#define CSV_ACCOUNT "BaseAccount" G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT; -preset_vec presets; - -static std::shared_ptr create_int_no_preset(void) -{ - auto preset = std::make_shared(); - preset->m_name = no_settings; - - return preset; -} - -static std::shared_ptr create_int_gnc_exp_preset(void) -{ - auto preset = std::make_shared(); - preset->m_name = gnc_exp; - preset->m_skip_start_lines = 1; - preset->m_multi_split = true; - - /* FIXME date and currency format should still be aligned with export format! - * That's currently hard to do, because the export uses whatever the user - * had set as global preference. - preset->date_active = 0; - preset->currency_active = 0; - */ - preset->m_column_types = { - GncTransPropType::DATE, - GncTransPropType::UNIQUE_ID, - GncTransPropType::NUM, - GncTransPropType::DESCRIPTION, - GncTransPropType::NOTES, - GncTransPropType::COMMODITY, - GncTransPropType::VOID_REASON, - GncTransPropType::ACTION, - GncTransPropType::MEMO, - GncTransPropType::ACCOUNT, - GncTransPropType::NONE, - GncTransPropType::NONE, - GncTransPropType::DEPOSIT, - GncTransPropType::REC_STATE, - GncTransPropType::REC_DATE, - GncTransPropType::PRICE - }; - - return preset; -} - -/************************************************** - * find - * - * find all settings entries in the state key file - **************************************************/ -const preset_vec& get_trans_presets (void) -{ - - // Search all Groups in the state key file for ones starting with prefix - auto preset_names = std::vector(); - auto keyfile = gnc_state_get_current (); - gsize grouplength; - gchar **groups = g_key_file_get_groups (keyfile, &grouplength); - - /* Start by building a sorted list of candidate presets as found in the state file */ - for (gsize i=0; i < grouplength; i++) - { - auto group = std::string(groups[i]); - auto pos = group.find(csv_group_prefix); - if (pos == std::string::npos) - continue; - - preset_names.push_back(group.substr(csv_group_prefix.size())); - } - // string array from the state file is no longer needed now. - g_strfreev (groups); - - /* We want our settings to appear sorted alphabetically to the user */ - std::sort(preset_names.begin(), preset_names.end()); - - /* Now add each preset to our global list */ - presets.clear(); - - /* Start with the internally generated ones */ - presets.push_back(create_int_no_preset()); - presets.push_back(create_int_gnc_exp_preset()); - - /* Then add all the ones we found in the state file */ - for (auto preset_name : preset_names) - { - auto preset = std::make_shared(); - preset->m_name = preset_name; - preset->load(); - presets.push_back(preset); - } - - return presets; -} - -bool trans_preset_is_reserved_name (const std::string& name) -{ - return ((name == no_settings) || - (name == _(no_settings.c_str())) || - (name == gnc_exp) || - (name == _(gnc_exp.c_str()))); -} - - /************************************************** * handle_load_error * @@ -176,7 +71,7 @@ bool trans_preset_is_reserved_name (const std::string& name) * ignore key-not-found errors though. We'll just * use a default value and go on. **************************************************/ -static bool +bool handle_load_error (GError **key_error, const std::string& group) { if (!*key_error) @@ -193,20 +88,40 @@ handle_load_error (GError **key_error, const std::string& group) return true; } +bool preset_is_reserved_name (const std::string& name) +{ + return ((name == no_settings) || + (name == _(no_settings.c_str())) || + (name == gnc_exp) || + (name == _(gnc_exp.c_str()))); +} + +std::string get_no_settings (void) +{ + return no_settings; +} + +std::string get_gnc_exp (void) +{ + return gnc_exp; +} + +std::string get_prefix (void) +{ + return csv_group_prefix; +} + /************************************************** - * load + * load_common * * load the settings from a state key file **************************************************/ bool -CsvTransSettings::load (void) +CsvImportSettings::load_common (void) { - if (trans_preset_is_reserved_name (m_name)) - return true; - GError *key_error = nullptr; m_load_error = false; - auto group = csv_group_prefix + m_name; + auto group = csv_group_prefix + m_settings_type + " - " + m_name; auto keyfile = gnc_state_get_current (); m_skip_start_lines = g_key_file_get_integer (keyfile, group.c_str(), CSV_SKIP_START, &key_error); @@ -218,9 +133,6 @@ CsvTransSettings::load (void) m_skip_alt_lines = g_key_file_get_boolean (keyfile, group.c_str(), CSV_SKIP_ALT, &key_error); m_load_error |= handle_load_error (&key_error, group); - m_multi_split = g_key_file_get_boolean (keyfile, group.c_str(), CSV_MULTI_SPLIT, &key_error); - m_load_error |= handle_load_error (&key_error, group); - auto csv_format = g_key_file_get_boolean (keyfile, group.c_str(), CSV_FORMAT, &key_error); if (key_error) csv_format = true; // default to true, but above command will return false in case of error m_load_error |= handle_load_error (&key_error, group); @@ -251,40 +163,8 @@ CsvTransSettings::load (void) if (key_char) g_free (key_char); - key_char = g_key_file_get_string (keyfile, group.c_str(), CSV_ACCOUNT, &key_error); - if (key_char && *key_char != '\0') - m_base_account = gnc_account_lookup_by_full_name (gnc_get_current_root_account(), key_char); - m_load_error |= handle_load_error (&key_error, group); - if (key_char) - g_free (key_char); - - m_column_types.clear(); + // Widths gsize list_len; - gchar** col_types_str = g_key_file_get_string_list (keyfile, group.c_str(), CSV_COL_TYPES, - &list_len, &key_error); - for (uint32_t i = 0; i < list_len; i++) - { - auto col_types_it = std::find_if (gnc_csv_col_type_strs.begin(), - gnc_csv_col_type_strs.end(), test_prop_type_str (col_types_str[i])); - if (col_types_it != gnc_csv_col_type_strs.end()) - { - /* Found a valid column type. Now check whether it is allowed - * in the selected mode (two-split vs multi-split) */ - auto prop = sanitize_trans_prop (col_types_it->first, m_multi_split); - m_column_types.push_back(prop); - if (prop != col_types_it->first) - PWARN("Found column type '%s', but this is blacklisted when multi-split mode is %s. " - "Inserting column type 'NONE' instead'.", - col_types_it->second, m_multi_split ? "enabled" : "disabled"); - } - else - PWARN("Found invalid column type '%s'. Inserting column type 'NONE' instead'.", - col_types_str[i]); - - } - if (col_types_str) - g_strfreev (col_types_str); - m_column_widths.clear(); gint *col_widths_int = g_key_file_get_integer_list (keyfile, group.c_str(), CSV_COL_WIDTHS, &list_len, &key_error); @@ -300,36 +180,20 @@ CsvTransSettings::load (void) return m_load_error; } - /************************************************** - * save + * save_common * * save settings to a key file **************************************************/ bool -CsvTransSettings::save (void) +CsvImportSettings::save_common (void) { - if (trans_preset_is_reserved_name (m_name)) - { - PWARN ("Ignoring attempt to save to reserved name '%s'", m_name.c_str()); - return true; - } - - if ((m_name.find('[') != std::string::npos)) - { - PWARN ("Name '%s' contains invalid characters '[]'. Refusing to save", m_name.c_str()); - return true; - } - auto keyfile = gnc_state_get_current (); - auto group = csv_group_prefix + m_name; + auto group = csv_group_prefix + m_settings_type + " - " + m_name; - // Drop previous saved settings with this name - g_key_file_remove_group (keyfile, group.c_str(), nullptr); - - // Start Saving the settings + // Start Saving the Common settings g_key_file_set_string (keyfile, group.c_str(), CSV_NAME, m_name.c_str()); - g_key_file_set_boolean (keyfile, group.c_str(), CSV_MULTI_SPLIT, m_multi_split); + g_key_file_set_integer (keyfile, group.c_str(), CSV_SKIP_START, m_skip_start_lines); g_key_file_set_integer (keyfile, group.c_str(), CSV_SKIP_END, m_skip_end_lines); g_key_file_set_boolean (keyfile, group.c_str(), CSV_SKIP_ALT, m_skip_alt_lines); @@ -345,22 +209,10 @@ CsvTransSettings::save (void) [&cmt_ss, &fmt_num](const GncDateFormat& fmt) { cmt_ss << fmt_num++ << ": '" << fmt.m_fmt << "', "; }); auto cmt = cmt_ss.str().substr(0, static_cast(cmt_ss.tellp()) - 2); - g_key_file_set_comment (keyfile, group.c_str(), CSV_DATE, - cmt.c_str(), nullptr); + g_key_file_set_comment (keyfile, group.c_str(), CSV_DATE, cmt.c_str(), nullptr); g_key_file_set_integer (keyfile, group.c_str(), CSV_CURRENCY, m_currency_format); g_key_file_set_string (keyfile, group.c_str(), CSV_ENCODING, m_encoding.c_str()); - if (m_base_account) - g_key_file_set_string (keyfile, group.c_str(), CSV_ACCOUNT, gnc_account_get_full_name(m_base_account)); - - std::vector col_types_str; - for (auto col_type : m_column_types) - col_types_str.push_back(gnc_csv_col_type_strs[col_type]); - - if (!col_types_str.empty()) - g_key_file_set_string_list (keyfile, group.c_str(), CSV_COL_TYPES, - col_types_str.data(), col_types_str.size()); - if (!m_column_widths.empty()) g_key_file_set_integer_list (keyfile, group.c_str(), CSV_COL_WIDTHS, (gint*)(m_column_widths.data()), m_column_widths.size()); @@ -377,33 +229,20 @@ CsvTransSettings::save (void) { if (key_error) { - g_warning ("Error reading group %s key %s: %s", group.c_str(), CSV_COL_TYPES, key_error->message); + g_warning ("Error reading group %s key %s: %s", group.c_str(), CSV_ENCODING, key_error->message); g_error_free (key_error); } else - g_warning ("Error comparing group %s key %s: '%s' and '%s'", group.c_str(), CSV_COL_TYPES, enc_str.c_str(), group.c_str()); + g_warning ("Error comparing group %s key %s: '%s' and '%s'", group.c_str(), CSV_ENCODING, enc_str.c_str(), group.c_str()); error = true; } return error; } void -CsvTransSettings::remove (void) +CsvImportSettings::remove_common (void) { - if (trans_preset_is_reserved_name (m_name)) - return; - auto keyfile = gnc_state_get_current (); - auto group = csv_group_prefix + m_name; + auto group = csv_group_prefix + m_settings_type + " - " + m_name; g_key_file_remove_group (keyfile, group.c_str(), nullptr); } - - -bool -CsvTransSettings::read_only (void) -{ - return ((m_name == no_settings) || - (m_name == _(no_settings.c_str())) || - (m_name == gnc_exp) || - (m_name == _(gnc_exp.c_str()))); -} diff --git a/gnucash/import-export/csv-imp/gnc-csv-trans-settings.hpp b/gnucash/import-export/csv-imp/gnc-csv-import-settings.hpp similarity index 69% rename from gnucash/import-export/csv-imp/gnc-csv-trans-settings.hpp rename to gnucash/import-export/csv-imp/gnc-csv-import-settings.hpp index 379c6600d8..6102fed144 100644 --- a/gnucash/import-export/csv-imp/gnc-csv-trans-settings.hpp +++ b/gnucash/import-export/csv-imp/gnc-csv-import-settings.hpp @@ -1,5 +1,5 @@ /*******************************************************************\ - * gnc-csv-trans-settings.h -- Save and Load CSV Import Settings * + * gnc-csv-import-settings.hpp -- Save and Load CSV Import Settings * * * * Copyright (C) 2014 Robert Fewell * * * @@ -20,22 +20,24 @@ * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * * Boston, MA 02110-1301, USA gnu@gnu.org * \********************************************************************/ -/** @file gnc-csv-trans-settings.h +/** @file gnc-csv-import-settings.hpp @brief CSV Import Settings @author Copyright (c) 2014 Robert Fewell @author Copyright (c) 2016 Geert Janssens */ -#ifndef GNC_CSV_TRANS_SETTINGS_H -#define GNC_CSV_TRANS_SETTINGS_H +#ifndef GNC_CSV_IMPORT_SETTINGS_H +#define GNC_CSV_IMPORT_SETTINGS_H extern "C" { #include #include "Account.h" +#include "gnc-commodity.h" } #include #include -#include "gnc-trans-props.hpp" +#include +#include #include "gnc-tokenizer.hpp" /** Enumeration for separator checkbutton types. These are the @@ -47,70 +49,63 @@ enum SEP_BUTTON_TYPES {SEP_SPACE, SEP_TAB, SEP_COMMA, SEP_COLON, SEP_SEMICOLON, /** Enumeration for the settings combo's */ enum SETTINGS_COL {SET_GROUP, SET_NAME}; -struct CsvTransSettings +struct CsvImportSettings { - CsvTransSettings() : m_file_format (GncImpFileFormat::CSV), m_encoding {"UTF-8"}, - m_multi_split (false), m_date_format {0}, m_currency_format {0}, + CsvImportSettings() : m_file_format (GncImpFileFormat::CSV), m_encoding {"UTF-8"}, + m_date_format {0}, m_currency_format {0}, m_skip_start_lines{0}, m_skip_end_lines{0}, m_skip_alt_lines (false), - m_separators {","}, m_base_account {nullptr}, - m_load_error {false} { } + m_separators {","}, m_load_error {false} { } /** Save the gathered widget properties to a key File. * * @return true if there was a problem in saving. */ -bool save (void); +bool save_common (void); /** Load the widget properties from a key File. * * @return true if there was a problem. */ -bool load (void); +bool load_common (void); /** Remove the preset from the state file. */ -void remove (void); - -/** Check whether the user is allowed to save (over) or delete this preset or not. - * The internally generated presets are read-only. The others - * can be saved to the state file or deleted. - * - * @return true if there was a problem. - */ -bool read_only (void); +void remove_common (void); +std::string m_settings_type; // Settings Type, TRANS, PRICE etc. +// Common Settings std::string m_name; // Name given to this preset by the user GncImpFileFormat m_file_format; // CSV import Format std::string m_encoding; // File encoding -bool m_multi_split; // Assume multiple lines per transaction int m_date_format; // Date Active id int m_currency_format; // Currency Active id uint32_t m_skip_start_lines; // Number of header rows to skip uint32_t m_skip_end_lines; // Number of footer rows to skip bool m_skip_alt_lines; // Skip alternate rows std::string m_separators; // Separators for csv format - -Account *m_base_account; // Base account -std::vector m_column_types; // The Column types in order -std::vector m_column_widths; // The Column widths - bool m_load_error; // Was there an error while parsing the state file ? +std::vector m_column_widths; // The Column widths }; -using preset_vec = std::vector>; -/** Creates a vector of CsvTransSettings which combines - * - one or more internally defined presets - * - all preset found in the state key file. - * - * @return a reference to the populated vector. - */ -const preset_vec& get_trans_presets (void); +std::string get_no_settings (void); +std::string get_gnc_exp (void); +std::string get_prefix (void); /** Check whether name can be used as a preset name. * The names of the internal presets are considered reserved. * A preset with such a name should not be saved or deleted. */ -bool trans_preset_is_reserved_name (const std::string& name); +bool preset_is_reserved_name (const std::string& name); + +/************************************************** + * handle_load_error + * + * record possible errors in the log file + * ignore key-not-found errors though. We'll just + * use a default value and go on. + **************************************************/ +bool +handle_load_error (GError **key_error, const std::string& group); #endif diff --git a/gnucash/import-export/csv-imp/gnc-csv-price-import-settings.cpp b/gnucash/import-export/csv-imp/gnc-csv-price-import-settings.cpp new file mode 100644 index 0000000000..8ebe9eba6b --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-csv-price-import-settings.cpp @@ -0,0 +1,258 @@ +/*******************************************************************\ + * gnc-csv-price-import-settings.cpp -- Price CSV Import Settings * + * * + * Copyright (C) 2017 Robert Fewell * + * * + * 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@gnu.org * +\********************************************************************/ +/** @file gnc-csv-price-import-settings.cpp + @brief CSV Import Settings + @author Copyright (c) 2014 Robert Fewell + @author Copyright (c) 2016 Geert Janssens +*/ + +#include "gnc-csv-import-settings.hpp" +#include "gnc-csv-price-import-settings.hpp" +#include + +extern "C" +{ +#include + +#include +#include + +#include "Account.h" +#include "gnc-state.h" +#include "gnc-ui-util.h" +} + +const std::string settings_type{"PRICE"}; + +#define CSV_COL_TYPES "ColumnTypes" + +#define CSV_TO_CURR "PriceToCurrency" +#define CSV_FROM_COMM "PriceFromCommodity" + +G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT; + +preset_vec_price presets_price; + +static std::shared_ptr create_int_no_preset(void) +{ + auto preset = std::make_shared(); + preset->m_name = get_no_settings(); + preset->m_settings_type = settings_type; + + return preset; +} + +static std::shared_ptr create_int_gnc_exp_preset(void) +{ + auto preset = std::make_shared(); + preset->m_name = get_gnc_exp(); + preset->m_skip_start_lines = 1; + + /* FIXME date and currency format should still be aligned with export format! + * That's currently hard to do, because the export uses whatever the user + * had set as global preference. + preset->date_active = 0; + preset->currency_active = 0; + */ + preset->m_column_types_price = { + GncPricePropType::DATE, + GncPricePropType::AMOUNT, + GncPricePropType::FROM_COMMODITY, + GncPricePropType::TO_CURRENCY + }; + return preset; +} + +/************************************************** + * find + * + * find all settings entries in the state key file + * based on settings type. + **************************************************/ +const preset_vec_price& get_import_presets_price (void) +{ + // Search all Groups in the state key file for ones starting with prefix + auto preset_names = std::vector(); + auto keyfile = gnc_state_get_current (); + gsize grouplength; + gchar **groups = g_key_file_get_groups (keyfile, &grouplength); + + /* Start by building a sorted list of candidate presets as found in the state file */ + for (gsize i=0; i < grouplength; i++) + { + auto group = std::string(groups[i]); + auto gp = get_prefix() + settings_type + " - "; + auto pos = group.find(gp); + if (pos == std::string::npos) + continue; + + preset_names.push_back(group.substr(gp.size())); + } + // string array from the state file is no longer needed now. + g_strfreev (groups); + + /* We want our settings to appear sorted alphabetically to the user */ + std::sort(preset_names.begin(), preset_names.end()); + + /* Now add each preset to our global list */ + presets_price.clear(); + + /* Start with the internally generated ones */ + presets_price.push_back(create_int_no_preset()); + //presets_price.push_back(create_int_gnc_exp_preset()); // Not Required + + /* Then add all the ones we found in the state file */ + for (auto preset_name : preset_names) + { + auto preset = std::make_shared(); + preset->m_settings_type = settings_type; + preset->m_name = preset_name; + preset->load(); + presets_price.push_back(preset); + } + return presets_price; +} + +/************************************************** + * load + * + * load the settings from a state key file + **************************************************/ +bool +CsvPriceImpSettings::load (void) +{ + if (preset_is_reserved_name (m_name)) + return true; + + GError *key_error = nullptr; + m_load_error = false; + auto keyfile = gnc_state_get_current (); + auto group = get_prefix() + m_settings_type + " - " + m_name; + + // Start Loading the settings + m_load_error = load_common(); // load the common settings + + gchar *key_char = g_key_file_get_string (keyfile, group.c_str(), CSV_TO_CURR, &key_error); + if (key_char && *key_char != '\0') + m_to_currency = parse_commodity_price_comm (key_char); + m_load_error |= handle_load_error (&key_error, group); + if (key_char) + g_free (key_char); + + key_char = g_key_file_get_string (keyfile, group.c_str(), CSV_FROM_COMM, &key_error); + if (key_char && *key_char != '\0') + m_from_commodity = parse_commodity_price_comm (key_char); + m_load_error |= handle_load_error (&key_error, group); + if (key_char) + g_free (key_char); + + gsize list_len; + m_column_types_price.clear(); + gchar** col_types_str_price = g_key_file_get_string_list (keyfile, group.c_str(), CSV_COL_TYPES, + &list_len, &key_error); + for (uint32_t i = 0; i < list_len; i++) + { + auto col_types_it = std::find_if (gnc_price_col_type_strs.begin(), + gnc_price_col_type_strs.end(), test_price_prop_type_str (col_types_str_price[i])); + if (col_types_it != gnc_price_col_type_strs.end()) + { + // Found a valid column type + m_column_types_price.push_back(col_types_it->first); + } + else + PWARN("Found invalid column type '%s'. Inserting column type 'NONE' instead'.", + col_types_str_price[i]); + } + if (col_types_str_price) + g_strfreev (col_types_str_price); + + return m_load_error; +} + +/************************************************** + * save + * + * save settings to a key file + **************************************************/ +bool +CsvPriceImpSettings::save (void) +{ + if (preset_is_reserved_name (m_name)) + { + PWARN ("Ignoring attempt to save to reserved name '%s'", m_name.c_str()); + return true; + } + + if ((m_name.find('[') != std::string::npos)) + { + PWARN ("Name '%s' contains invalid characters '[]'. Refusing to save", m_name.c_str()); + return true; + } + + auto keyfile = gnc_state_get_current (); + auto group = get_prefix() + m_settings_type + " - " + m_name; + + // Drop previous saved settings with this name + g_key_file_remove_group (keyfile, group.c_str(), nullptr); + + // Start Saving the settings + bool error = save_common(); // save the common settings + + if (error) + return error; + + if (m_to_currency) + { + auto unique_name = g_strconcat (gnc_commodity_get_namespace (m_to_currency), "::", + gnc_commodity_get_mnemonic (m_to_currency), nullptr); + g_key_file_set_string (keyfile, group.c_str(), CSV_TO_CURR, unique_name); + g_free (unique_name); + } + + if (m_from_commodity) + { + auto unique_name = g_strconcat (gnc_commodity_get_namespace (m_from_commodity), "::", + gnc_commodity_get_mnemonic (m_from_commodity), nullptr); + g_key_file_set_string (keyfile, group.c_str(), CSV_FROM_COMM, unique_name); + g_free (unique_name); + } + + std::vector col_types_str_price; + for (auto col_type : m_column_types_price) + col_types_str_price.push_back(gnc_price_col_type_strs[col_type]); + + if (!col_types_str_price.empty()) + g_key_file_set_string_list (keyfile, group.c_str(), CSV_COL_TYPES, + col_types_str_price.data(), col_types_str_price.size()); + + return error; +} + +void +CsvPriceImpSettings::remove (void) +{ + if (preset_is_reserved_name (m_name)) + return; + + remove_common(); +} diff --git a/gnucash/import-export/csv-imp/gnc-csv-price-import-settings.hpp b/gnucash/import-export/csv-imp/gnc-csv-price-import-settings.hpp new file mode 100644 index 0000000000..e0d52b480e --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-csv-price-import-settings.hpp @@ -0,0 +1,79 @@ +/*******************************************************************\ + * gnc-csv-price-import-settings.hpp -- Price CSV Import Settings * + * * + * Copyright (C) 2017 Robert Fewell * + * * + * 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@gnu.org * +\********************************************************************/ +/** @file gnc-csv-price-import-settings.hpp + @brief CSV Import Settings + @author Copyright (c) 2014 Robert Fewell + @author Copyright (c) 2016 Geert Janssens +*/ +#ifndef GNC_CSV_PRICE_IMPORT_SETTINGS_H +#define GNC_CSV_PRICE_IMPORT_SETTINGS_H + +extern "C" { +#include +#include "Account.h" +#include "gnc-commodity.h" +} + +#include +#include +#include "gnc-price-props.hpp" +#include "gnc-tokenizer.hpp" +#include "gnc-csv-import-settings.hpp" + +struct CsvPriceImpSettings : public CsvImportSettings +{ + CsvPriceImpSettings() : m_from_commodity {nullptr}, m_to_currency {nullptr} { } + +/** Save the gathered widget properties to a key File. + * + * @return true if there was a problem in saving. + */ +bool save (void); + +/** Load the widget properties from a key File. + * + * @return true if there was a problem. + */ +bool load (void); + +/** Remove the preset from the state file. + */ +void remove (void); + +// Price Settings +gnc_commodity *m_from_commodity; // Price From Commodity +gnc_commodity *m_to_currency; // Price To Currency +std::vector m_column_types_price; // The Price Column types in order +}; + +using preset_vec_price = std::vector>; + +/** Creates a vector of CsvPriceImpSettings which combines + * - one or more internally defined presets + * - all preset found in the state key file. + * + * @return a reference to the populated vector. + */ +const preset_vec_price& get_import_presets_price (void); + +#endif diff --git a/gnucash/import-export/csv-imp/gnc-csv-trans-import-settings.cpp b/gnucash/import-export/csv-imp/gnc-csv-trans-import-settings.cpp new file mode 100644 index 0000000000..17ac2b79c3 --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-csv-trans-import-settings.cpp @@ -0,0 +1,262 @@ +/*******************************************************************\ + * gnc-csv-trans-import-settings.cpp -- Trans CSV Import Settings * + * * + * Copyright (C) 2014 Robert Fewell * + * * + * 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@gnu.org * +\********************************************************************/ +/** @file gnc-csv-trans-import-settings.cpp + @brief CSV Import Settings + @author Copyright (c) 2014 Robert Fewell + @author Copyright (c) 2016 Geert Janssens +*/ + +#include "gnc-csv-import-settings.hpp" +#include "gnc-csv-trans-import-settings.hpp" +#include + +extern "C" +{ +#include + +#include +#include + +#include "Account.h" +#include "gnc-state.h" +#include "gnc-ui-util.h" +} + +const std::string settings_type{"TRANS"}; + +#define CSV_COL_TYPES "ColumnTypes" + +#define CSV_ACCOUNT "BaseAccount" +#define CSV_MULTI_SPLIT "MultiSplit" + +G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT; + +preset_vec_trans presets_trans; + +static std::shared_ptr create_int_no_preset(void) +{ + auto preset = std::make_shared(); + preset->m_name = get_no_settings(); + preset->m_settings_type = settings_type; + + return preset; +} + +static std::shared_ptr create_int_gnc_exp_preset(void) +{ + auto preset = std::make_shared(); + preset->m_name = get_gnc_exp(); + preset->m_skip_start_lines = 1; + preset->m_multi_split = true; + + /* FIXME date and currency format should still be aligned with export format! + * That's currently hard to do, because the export uses whatever the user + * had set as global preference. + preset->date_active = 0; + preset->currency_active = 0; + */ + preset->m_column_types = { + GncTransPropType::DATE, + GncTransPropType::UNIQUE_ID, + GncTransPropType::NUM, + GncTransPropType::DESCRIPTION, + GncTransPropType::NOTES, + GncTransPropType::COMMODITY, + GncTransPropType::VOID_REASON, + GncTransPropType::ACTION, + GncTransPropType::MEMO, + GncTransPropType::ACCOUNT, + GncTransPropType::NONE, + GncTransPropType::NONE, + GncTransPropType::DEPOSIT, + GncTransPropType::REC_STATE, + GncTransPropType::REC_DATE, + GncTransPropType::PRICE + }; + return preset; +} + +/************************************************** + * find + * + * find all settings entries in the state key file + * based on settings type. + **************************************************/ +const preset_vec_trans& get_import_presets_trans (void) +{ + // Search all Groups in the state key file for ones starting with prefix + auto preset_names = std::vector(); + auto keyfile = gnc_state_get_current (); + gsize grouplength; + gchar **groups = g_key_file_get_groups (keyfile, &grouplength); + + /* Start by building a sorted list of candidate presets as found in the state file */ + for (gsize i=0; i < grouplength; i++) + { + auto group = std::string(groups[i]); + auto gp = get_prefix() + settings_type + " - "; + auto pos = group.find(gp); + if (pos == std::string::npos) + continue; + + preset_names.push_back(group.substr(gp.size())); + } + // string array from the state file is no longer needed now. + g_strfreev (groups); + + /* We want our settings to appear sorted alphabetically to the user */ + std::sort(preset_names.begin(), preset_names.end()); + + /* Now add each preset to our global list */ + presets_trans.clear(); + + /* Start with the internally generated ones */ + presets_trans.push_back(create_int_no_preset()); + presets_trans.push_back(create_int_gnc_exp_preset()); + + /* Then add all the ones we found in the state file */ + for (auto preset_name : preset_names) + { + auto preset = std::make_shared(); + preset->m_settings_type = settings_type; + preset->m_name = preset_name; + preset->load(); + presets_trans.push_back(preset); + } + return presets_trans; +} + +/************************************************** + * load + * + * load the settings from a state key file + **************************************************/ +bool +CsvTransImpSettings::load (void) +{ + if (preset_is_reserved_name (m_name)) + return true; + + GError *key_error = nullptr; + m_load_error = false; + auto keyfile = gnc_state_get_current (); + auto group = get_prefix() + m_settings_type + " - " + m_name; + + // Start Loading the settings + m_load_error = load_common(); // load the common settings + + m_multi_split = g_key_file_get_boolean (keyfile, group.c_str(), CSV_MULTI_SPLIT, &key_error); + m_load_error |= handle_load_error (&key_error, group); + + gchar *key_char = g_key_file_get_string (keyfile, group.c_str(), CSV_ACCOUNT, &key_error); + if (key_char && *key_char != '\0') + m_base_account = gnc_account_lookup_by_full_name (gnc_get_current_root_account(), key_char); + m_load_error |= handle_load_error (&key_error, group); + if (key_char) + g_free (key_char); + + gsize list_len; + m_column_types.clear(); + gchar** col_types_str = g_key_file_get_string_list (keyfile, group.c_str(), CSV_COL_TYPES, + &list_len, &key_error); + for (uint32_t i = 0; i < list_len; i++) + { + auto col_types_it = std::find_if (gnc_csv_col_type_strs.begin(), + gnc_csv_col_type_strs.end(), test_prop_type_str (col_types_str[i])); + if (col_types_it != gnc_csv_col_type_strs.end()) + { + /* Found a valid column type. Now check whether it is allowed + * in the selected mode (two-split vs multi-split) */ + auto prop = sanitize_trans_prop (col_types_it->first, m_multi_split); + m_column_types.push_back(prop); + if (prop != col_types_it->first) + PWARN("Found column type '%s', but this is blacklisted when multi-split mode is %s. " + "Inserting column type 'NONE' instead'.", + col_types_it->second, m_multi_split ? "enabled" : "disabled"); + } + else + PWARN("Found invalid column type '%s'. Inserting column type 'NONE' instead'.", + col_types_str[i]); + } + if (col_types_str) + g_strfreev (col_types_str); + + return m_load_error; +} + +/************************************************** + * save + * + * save settings to a key file + **************************************************/ +bool +CsvTransImpSettings::save (void) +{ + if (preset_is_reserved_name (m_name)) + { + PWARN ("Ignoring attempt to save to reserved name '%s'", m_name.c_str()); + return true; + } + + if ((m_name.find('[') != std::string::npos)) + { + PWARN ("Name '%s' contains invalid characters '[]'. Refusing to save", m_name.c_str()); + return true; + } + + auto keyfile = gnc_state_get_current (); + auto group = get_prefix() + m_settings_type + " - " + m_name; + + // Drop previous saved settings with this name + g_key_file_remove_group (keyfile, group.c_str(), nullptr); + + // Start Saving the settings + bool error = save_common(); // save the common settings + + if (error) + return error; + + g_key_file_set_boolean (keyfile, group.c_str(), CSV_MULTI_SPLIT, m_multi_split); + + if (m_base_account) + g_key_file_set_string (keyfile, group.c_str(), CSV_ACCOUNT, gnc_account_get_full_name(m_base_account)); + + std::vector col_types_str; + for (auto col_type : m_column_types) + col_types_str.push_back(gnc_csv_col_type_strs[col_type]); + + if (!col_types_str.empty()) + g_key_file_set_string_list (keyfile, group.c_str(), CSV_COL_TYPES, + col_types_str.data(), col_types_str.size()); + + return error; +} + +void +CsvTransImpSettings::remove (void) +{ + if (preset_is_reserved_name (m_name)) + return; + + remove_common(); +} diff --git a/gnucash/import-export/csv-imp/gnc-csv-trans-import-settings.hpp b/gnucash/import-export/csv-imp/gnc-csv-trans-import-settings.hpp new file mode 100644 index 0000000000..c93ef3a7ce --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-csv-trans-import-settings.hpp @@ -0,0 +1,79 @@ +/*******************************************************************\ + * gnc-csv-trans-import-settings.hpp -- Trans CSV Import Settings * + * * + * Copyright (C) 2017 Robert Fewell * + * * + * 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@gnu.org * +\********************************************************************/ +/** @file gnc-csv-trans-import-settings.hpp + @brief CSV Import Settings + @author Copyright (c) 2014 Robert Fewell + @author Copyright (c) 2016 Geert Janssens +*/ +#ifndef GNC_CSV_TRANS_IMPORT_SETTINGS_H +#define GNC_CSV_TRANS_IMPORT_SETTINGS_H + +extern "C" { +#include +#include "Account.h" +#include "gnc-commodity.h" +} + +#include +#include +#include "gnc-trans-props.hpp" +#include "gnc-tokenizer.hpp" +#include "gnc-csv-import-settings.hpp" + +struct CsvTransImpSettings : public CsvImportSettings +{ + CsvTransImpSettings() : m_base_account {nullptr}, m_multi_split (false) { } + +/** Save the gathered widget properties to a key File. + * + * @return true if there was a problem in saving. + */ +bool save (void); + +/** Load the widget properties from a key File. + * + * @return true if there was a problem. + */ +bool load (void); + +/** Remove the preset from the state file. + */ +void remove (void); + +// Transaction Settings +Account *m_base_account; // Base account +bool m_multi_split; // Assume multiple lines per transaction +std::vector m_column_types; // The Column types in order +}; + +using preset_vec_trans = std::vector>; + +/** Creates a vector of CsvTransImpSettings which combines + * - one or more internally defined presets + * - all preset found in the state key file. + * + * @return a reference to the populated vector. + */ +const preset_vec_trans& get_import_presets_trans (void); + +#endif diff --git a/gnucash/import-export/csv-imp/gnc-plugin-csv-import-ui.xml b/gnucash/import-export/csv-imp/gnc-plugin-csv-import-ui.xml index c119a648cb..bdd3043cb4 100644 --- a/gnucash/import-export/csv-imp/gnc-plugin-csv-import-ui.xml +++ b/gnucash/import-export/csv-imp/gnc-plugin-csv-import-ui.xml @@ -5,6 +5,7 @@ + diff --git a/gnucash/import-export/csv-imp/gnc-plugin-csv-import.c b/gnucash/import-export/csv-imp/gnc-plugin-csv-import.c index 25954beb9d..3cecf66fe8 100644 --- a/gnucash/import-export/csv-imp/gnc-plugin-csv-import.c +++ b/gnucash/import-export/csv-imp/gnc-plugin-csv-import.c @@ -30,6 +30,7 @@ #include "assistant-csv-account-import.h" #include "assistant-csv-trans-import.h" +#include "assistant-csv-price-import.h" static void gnc_plugin_csv_import_class_init (GncPluginCsvImportClass *klass); static void gnc_plugin_csv_import_init (GncPluginCsvImport *plugin); @@ -38,6 +39,7 @@ static void gnc_plugin_csv_import_finalize (GObject *object); /* Command callbacks */ static void gnc_plugin_csv_import_tree_cmd (GtkAction *action, GncMainWindowActionData *data); static void gnc_plugin_csv_import_trans_cmd (GtkAction *action, GncMainWindowActionData *data); +static void gnc_plugin_csv_import_price_cmd (GtkAction *action, GncMainWindowActionData *data); #define PLUGIN_ACTIONS_NAME "gnc-plugin-csv-import-actions" #define PLUGIN_UI_FILENAME "gnc-plugin-csv-import-ui.xml" @@ -54,6 +56,11 @@ static GtkActionEntry gnc_plugin_actions [] = N_("Import Transactions from a CSV file"), G_CALLBACK (gnc_plugin_csv_import_trans_cmd) }, + { + "CsvImportPriceAction", "go-previous", N_("Import _Prices from a CSV file..."), NULL, + N_("Import Prices from a CSV file"), + G_CALLBACK (gnc_plugin_csv_import_price_cmd) + }, }; static guint gnc_plugin_n_actions = G_N_ELEMENTS (gnc_plugin_actions); @@ -157,6 +164,13 @@ gnc_plugin_csv_import_trans_cmd (GtkAction *action, gnc_file_csv_trans_import (); } +static void +gnc_plugin_csv_import_price_cmd (GtkAction *action, + GncMainWindowActionData *data) +{ + gnc_file_csv_price_import (); +} + /************************************************************ * Plugin Bootstrapping * ************************************************************/ diff --git a/gnucash/import-export/csv-imp/gnc-price-import.cpp b/gnucash/import-export/csv-imp/gnc-price-import.cpp new file mode 100644 index 0000000000..0bc1ead8a1 --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-price-import.cpp @@ -0,0 +1,757 @@ +/********************************************************************\ + * gnc-price-import.cpp - import prices from csv files * + * * + * 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@gnu.org * + * * +\********************************************************************/ + +#include + +extern "C" { +#include +#if PLATFORM(WINDOWS) +#include +#endif + +#include + +#include "gnc-ui-util.h" //get book +#include "gnc-commodity.h" +#include "gnc-pricedb.h" +} + +#include +#include + +#include "gnc-price-import.hpp" +#include "gnc-price-props.hpp" +#include "gnc-csv-tokenizer.hpp" +#include "gnc-fw-tokenizer.hpp" +#include "gnc-csv-price-import-settings.hpp" + +G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT; + +const int num_currency_formats_price = 3; +const gchar* currency_format_user_price[] = {N_("Locale"), + N_("Period: 123,456.78"), + N_("Comma: 123.456,78") + }; + + +/** Constructor for GncPriceImport. + */ +GncPriceImport::GncPriceImport(GncImpFileFormat format) +{ + /* All of the data pointers are initially NULL. This is so that, if + * gnc_csv_parse_data_free is called before all of the data is + * initialized, only the data that needs to be freed is freed. */ + m_skip_errors = false; + file_format(m_settings.m_file_format = format); +} + +/** Destructor for GncPriceImport. + */ +GncPriceImport::~GncPriceImport() +{ +} + +/** Sets the file format for the file to import, which + * may cause the file to be reloaded as well if the + * previously set file format was different and a + * filename was already set. + * @param format the new format to set + * @exception std::ifstream::failure if file reloading fails + */ +void GncPriceImport::file_format(GncImpFileFormat format) +{ + if (m_tokenizer && m_settings.m_file_format == format) + return; + + auto new_encoding = std::string("UTF-8"); + auto new_imp_file = std::string(); + + // Recover common settings from old tokenizer + if (m_tokenizer) + { + new_encoding = m_tokenizer->encoding(); + new_imp_file = m_tokenizer->current_file(); + if (file_format() == GncImpFileFormat::FIXED_WIDTH) + { + auto fwtok = dynamic_cast(m_tokenizer.get()); + if (!fwtok->get_columns().empty()) + m_settings.m_column_widths = fwtok->get_columns(); + } + } + + m_settings.m_file_format = format; + m_tokenizer = gnc_tokenizer_factory(m_settings.m_file_format); + + // Set up new tokenizer with common settings + // recovered from old tokenizer + m_tokenizer->encoding(new_encoding); + load_file(new_imp_file); + + // Restore potentially previously set separators or column_widths + if ((file_format() == GncImpFileFormat::CSV) + && !m_settings.m_separators.empty()) + separators (m_settings.m_separators); + else if ((file_format() == GncImpFileFormat::FIXED_WIDTH) + && !m_settings.m_column_widths.empty()) + { + auto fwtok = dynamic_cast(m_tokenizer.get()); + fwtok->columns (m_settings.m_column_widths); + } +} + +GncImpFileFormat GncPriceImport::file_format() +{ + return m_settings.m_file_format; +} + +void GncPriceImport::over_write (bool over) +{ + m_over_write = over; +} +bool GncPriceImport::over_write () { return m_over_write; } + +/** Sets a from commodity. This is the commodity all import data relates to. + * When a from commodity is set, there can't be any from columns selected + * in the import data. + * @param from_commodity pointer to a commodity or NULL. + */ +void GncPriceImport::from_commodity (gnc_commodity* from_commodity) +{ + m_settings.m_from_commodity = from_commodity; + if (m_settings.m_from_commodity) + { + auto col_type_comm = std::find (m_settings.m_column_types_price.begin(), + m_settings.m_column_types_price.end(), GncPricePropType::FROM_COMMODITY); + + if (col_type_comm != m_settings.m_column_types_price.end()) + set_column_type_price (col_type_comm -m_settings.m_column_types_price.begin(), + GncPricePropType::NONE); + + // force a refresh of the to_currency if the from_commodity is changed + std::vector commodities = { GncPricePropType::TO_CURRENCY }; + reset_formatted_column (commodities); + } +} +gnc_commodity *GncPriceImport::from_commodity () { return m_settings.m_from_commodity; } + +/** Sets a to currency. This is the to currency all import data relates to. + * When a to currency is set, there can't be any to currency columns selected + * in the import data. + * @param to_currency pointer to a commodity or NULL. + */ +void GncPriceImport::to_currency (gnc_commodity* to_currency) +{ + m_settings.m_to_currency = to_currency; + if (m_settings.m_to_currency) + { + auto col_type_currency = std::find (m_settings.m_column_types_price.begin(), + m_settings.m_column_types_price.end(), GncPricePropType::TO_CURRENCY); + + if (col_type_currency != m_settings.m_column_types_price.end()) + set_column_type_price (col_type_currency -m_settings.m_column_types_price.begin(), + GncPricePropType::NONE); + + // force a refresh of the from_commodity if the to_currency is changed + std::vector commodities = { GncPricePropType::FROM_COMMODITY }; + reset_formatted_column (commodities); + } +} +gnc_commodity *GncPriceImport::to_currency () { return m_settings.m_to_currency; } + +void GncPriceImport::reset_formatted_column (std::vector& col_types) +{ + for (auto col_type: col_types) + { + auto col = std::find (m_settings.m_column_types_price.begin(), + m_settings.m_column_types_price.end(), col_type); + if (col != m_settings.m_column_types_price.end()) + set_column_type_price (col - m_settings.m_column_types_price.begin(), col_type, true); + } +} + +void GncPriceImport::currency_format (int currency_format) +{ + m_settings.m_currency_format = currency_format; + + /* Reparse all currency related columns */ + std::vector commodities = { GncPricePropType::AMOUNT }; + reset_formatted_column (commodities); +} +int GncPriceImport::currency_format () { return m_settings.m_currency_format; } + +void GncPriceImport::date_format (int date_format) +{ + m_settings.m_date_format = date_format; + + /* Reparse all date related columns */ + std::vector dates = { GncPricePropType::DATE }; + reset_formatted_column (dates); +} +int GncPriceImport::date_format () { return m_settings.m_date_format; } + +/** Converts raw file data using a new encoding. This function must be + * called after load_file only if load_file guessed + * the wrong encoding. + * @param encoding Encoding that data should be translated using + */ +void GncPriceImport::encoding (const std::string& encoding) +{ + // TODO investigate if we can catch conversion errors and report them + if (m_tokenizer) + { + m_tokenizer->encoding(encoding); // May throw + try + { + tokenize(false); + } + catch (...) + { }; + } + + m_settings.m_encoding = encoding; +} + +std::string GncPriceImport::encoding () { return m_settings.m_encoding; } + +void GncPriceImport::update_skipped_lines(boost::optional start, boost::optional end, + boost::optional alt, boost::optional errors) +{ + if (start) + m_settings.m_skip_start_lines = *start; + if (end) + m_settings.m_skip_end_lines = *end; + if (alt) + m_settings.m_skip_alt_lines = *alt; + if (errors) + m_skip_errors = *errors; + + for (uint32_t i = 0; i < m_parsed_lines.size(); i++) + { + std::get(m_parsed_lines[i]) = + ((i < skip_start_lines()) || // start rows to skip + (i >= m_parsed_lines.size() - skip_end_lines()) || // end rows to skip + (((i - skip_start_lines()) % 2 == 1) && // skip every second row... + skip_alt_lines()) || // ...if requested + (m_skip_errors && !std::get(m_parsed_lines[i]).empty())); // skip lines with errors + } +} + +uint32_t GncPriceImport::skip_start_lines () { return m_settings.m_skip_start_lines; } +uint32_t GncPriceImport::skip_end_lines () { return m_settings.m_skip_end_lines; } +bool GncPriceImport::skip_alt_lines () { return m_settings.m_skip_alt_lines; } +bool GncPriceImport::skip_err_lines () { return m_skip_errors; } + +void GncPriceImport::separators (std::string separators) +{ + if (file_format() != GncImpFileFormat::CSV) + return; + + m_settings.m_separators = separators; + auto csvtok = dynamic_cast(m_tokenizer.get()); + csvtok->set_separators (separators); + +} +std::string GncPriceImport::separators () { return m_settings.m_separators; } + +void GncPriceImport::settings (const CsvPriceImpSettings& settings) +{ + /* First apply file format as this may recreate the tokenizer */ + file_format (settings.m_file_format); + /* Only then apply the other settings */ + m_settings = settings; + from_commodity (m_settings.m_from_commodity); + to_currency (m_settings.m_to_currency); + encoding (m_settings.m_encoding); + + if (file_format() == GncImpFileFormat::CSV) + separators (m_settings.m_separators); + else if (file_format() == GncImpFileFormat::FIXED_WIDTH) + { + auto fwtok = dynamic_cast(m_tokenizer.get()); + fwtok->columns (m_settings.m_column_widths); + } + try + { + tokenize(false); + } + catch (...) + { }; + + /* Tokenizing will clear column types, reset them here + * based on the loaded settings. + */ + std::copy_n (settings.m_column_types_price.begin(), + std::min (m_settings.m_column_types_price.size(), settings.m_column_types_price.size()), + m_settings.m_column_types_price.begin()); +} + +bool GncPriceImport::save_settings () +{ + if (preset_is_reserved_name (m_settings.m_name)) + return true; + + /* separators are already copied to m_settings in the separators + * function above. However this is not the case for the column + * widths in fw mode, so do this now. + */ + if (file_format() == GncImpFileFormat::FIXED_WIDTH) + { + auto fwtok = dynamic_cast(m_tokenizer.get()); + m_settings.m_column_widths = fwtok->get_columns(); + } + return m_settings.save(); +} + +void GncPriceImport::settings_name (std::string name) { m_settings.m_name = name; } +std::string GncPriceImport::settings_name () { return m_settings.m_name; } + +/** Loads a file into a GncPriceImport. This is the first function + * that must be called after creating a new GncPriceImport. As long as + * this function didn't run successfully, the importer can't proceed. + * @param filename Name of the file that should be opened + * @exception may throw std::ifstream::failure on any io error + */ +void GncPriceImport::load_file (const std::string& filename) +{ + /* Get the raw data first and handle an error if one occurs. */ + try + { + m_tokenizer->load_file (filename); + return; + } + catch (std::ifstream::failure& ios_err) + { + // Just log the error and pass it on the call stack for proper handling + PWARN ("Error: %s", ios_err.what()); + throw; + } +} + +/** Splits a file into cells. This requires having an encoding that + * works (see GncPriceImport::convert_encoding). Tokenizing related options + * should be set to the user's selections before calling this + * function. + * Notes: - this function must be called with guessColTypes set to true once + * before calling it with guessColTypes set to false. + * - if guessColTypes is true, all the column types will be set + * GncPricePropType::NONE right now as real guessing isn't implemented yet + * @param guessColTypes true to guess what the types of columns are based on the cell contents + * @exception std::range_error if tokenizing failed + */ +void GncPriceImport::tokenize (bool guessColTypes) +{ + if (!m_tokenizer) + return; + + uint32_t max_cols = 0; + m_tokenizer->tokenize(); + m_parsed_lines.clear(); + for (auto tokenized_line : m_tokenizer->get_tokens()) + { + m_parsed_lines.push_back (std::make_tuple (tokenized_line, std::string(), + std::make_shared(date_format(), currency_format()), + false)); + auto length = tokenized_line.size(); + if (length > max_cols) + max_cols = length; + } + + /* If it failed, generate an error. */ + if (m_parsed_lines.size() == 0) + { + throw (std::range_error ("Tokenizing failed.")); + return; + } + + m_settings.m_column_types_price.resize(max_cols, GncPricePropType::NONE); + + /* Force reinterpretation of already set columns and/or base_account */ + for (uint32_t i = 0; i < m_settings.m_column_types_price.size(); i++) + set_column_type_price (i, m_settings.m_column_types_price[i], true); + + if (guessColTypes) + { + /* Guess column_types based + * on the contents of each column. */ + /* TODO Make it actually guess. */ + } +} + +struct ErrorListPrice +{ +public: + void add_error (std::string msg); + std::string str(); + bool empty() { return m_error.empty(); } +private: + std::string m_error; +}; + +void ErrorListPrice::add_error (std::string msg) +{ + m_error += "- " + msg + "\n"; +} + +std::string ErrorListPrice::str() +{ + return m_error.substr(0, m_error.size() - 1); +} + +/* Test for the required minimum number of columns selected and + * the selection is consistent. + * @param An ErrorListPrice object to which all found issues are added. + */ +void GncPriceImport::verify_column_selections (ErrorListPrice& error_msg) +{ + /* Verify if a date column is selected and it's parsable. + */ + if (!check_for_column_type(GncPricePropType::DATE)) + error_msg.add_error( _("Please select a date column.")); + + /* Verify an amount column is selected. + */ + if (!check_for_column_type(GncPricePropType::AMOUNT)) + error_msg.add_error( _("Please select an amount column.")); + + /* Verify a Currency to column is selected. + */ + if (!check_for_column_type(GncPricePropType::TO_CURRENCY)) + { + if (!m_settings.m_to_currency) + error_msg.add_error( _("Please select a 'Currency to' column or set a Currency in the 'Currency To' field.")); + } + + /* Verify a Commodity from column is selected. + */ + if (!check_for_column_type(GncPricePropType::FROM_COMMODITY)) + { + if (!m_settings.m_from_commodity) + error_msg.add_error( _("Please select a 'Commodity from' column or set a Commodity in the 'Commodity From' field.")); + } + + /* Verify a 'Commodity from' does not equal 'Currency to'. + */ + if ((m_settings.m_to_currency) && (m_settings.m_from_commodity)) + { + if (gnc_commodity_equal (m_settings.m_to_currency, m_settings.m_from_commodity)) + error_msg.add_error( _("'Commodity From' can not be the same as 'Currency To'.")); + } +} + +/* Check whether the chosen settings can successfully parse + * the import data. This will check: + * - there's at least one line selected for import + * - the minimum number of columns is selected + * - the values in the selected columns can be parsed meaningfully. + * @return An empty string if all checks passed or the reason + * verification failed otherwise. + */ +std::string GncPriceImport::verify () +{ + auto newline = std::string(); + auto error_msg = ErrorListPrice(); + + /* Check if the import file did actually contain any information */ + if (m_parsed_lines.size() == 0) + { + error_msg.add_error(_("No valid data found in the selected file. It may be empty or the selected encoding is wrong.")); + return error_msg.str(); + } + + /* Check if at least one line is selected for importing */ + auto skip_alt_offset = m_settings.m_skip_alt_lines ? 1 : 0; + if (m_settings.m_skip_start_lines + m_settings.m_skip_end_lines + skip_alt_offset >= m_parsed_lines.size()) + { + error_msg.add_error(_("No lines are selected for importing. Please reduce the number of lines to skip.")); + return error_msg.str(); + } + + verify_column_selections (error_msg); + + update_skipped_lines (boost::none, boost::none, boost::none, boost::none); + + auto have_line_errors = false; + for (auto line : m_parsed_lines) + { + if (!std::get(line) && !std::get(line).empty()) + { + have_line_errors = true; + break; + } + } + + if (have_line_errors) + error_msg.add_error( _("Not all fields could be parsed. Please correct the issues reported for each line or adjust the lines to skip.")); + + return error_msg.str(); +} + +/** Checks whether the parsed line contains all essential properties. + * @param parsed_line The line we are checking + * @exception std::invalid_argument in an essential property is missing + */ +static void price_properties_verify_essentials (std::vector::iterator& parsed_line) +{ + std::string error_message; + std::shared_ptr price_props; + std::tie(std::ignore, error_message, price_props, std::ignore) = *parsed_line; + + auto price_error = price_props->verify_essentials(); + + error_message.clear(); + if (!price_error.empty()) + { + error_message += price_error; + error_message += "\n"; + } + + if (!error_message.empty()) + throw std::invalid_argument(error_message); +} + +void GncPriceImport::create_price (std::vector::iterator& parsed_line) +{ + StrVec line; + std::string error_message; + std::shared_ptr price_props = nullptr; + bool skip_line = false; + std::tie(line, error_message, price_props, skip_line) = *parsed_line; + + if (skip_line) + return; + + error_message.clear(); + + // Add a TO_CURRENCY property with the selected 'currency to' if no 'currency to' column was set by the user + auto line_to_currency = price_props->get_to_currency(); + if (!line_to_currency) + { + if (m_settings.m_to_currency) + price_props->set_to_currency(m_settings.m_to_currency); + else + { + // Oops - the user didn't select a 'currency to' column *and* we didn't get a selected value either! + // Note if you get here this suggests a bug in the code! + error_message = _("No 'Currency to' column selected and no selected Currency specified either.\n" + "This should never happen. Please report this as a bug."); + PINFO("User warning: %s", error_message.c_str()); + throw std::invalid_argument(error_message); + } + } + + // Add a FROM_COMMODITY property with the selected 'commodity from' if no 'commodity from' column was set by the user + auto line_from_commodity = price_props->get_from_commodity(); + if (!line_from_commodity) + { + if (m_settings.m_from_commodity) + price_props->set_from_commodity(m_settings.m_from_commodity); + else + { + // Oops - the user didn't select a 'commodity from' column *and* we didn't get a selected value either! + // Note if you get here this suggests a bug in the code! + error_message = _("No 'Commodity from' column selected and no selected Commodity specified either.\n" + "This should never happen. Please report this as a bug."); + PINFO("User warning: %s", error_message.c_str()); + throw std::invalid_argument(error_message); + } + } + + /* If column parsing was successful, convert price properties into a price. */ + try + { + price_properties_verify_essentials (parsed_line); + + QofBook* book = gnc_get_current_book(); + GNCPriceDB *pdb = gnc_pricedb_get_db (book); + + /* If all went well, add this price to the list. */ + auto price_created = price_props->create_price (book, pdb, m_over_write); + if (price_created == ADDED) + m_prices_added++; + else if (price_created == DUPLICATED) + m_prices_duplicated++; + else if (price_created == REPLACED) + m_prices_replaced++; + } + catch (const std::invalid_argument& e) + { + error_message = e.what(); + PINFO("User warning: %s", error_message.c_str()); + } +} + +/** Creates a list of prices from parsed data. The parsed data + * will first be validated. If any errors are found in lines that are marked + * for processing (ie not marked to skip) this function will + * throw an error. + * @param skip_errors true skip over lines with errors + * @exception throws std::invalid_argument if data validation or processing fails. + */ +void GncPriceImport::create_prices () +{ + /* Start with verifying the current data. */ + auto verify_result = verify(); + if (!verify_result.empty()) + throw std::invalid_argument (verify_result); + + m_prices_added = 0; + m_prices_duplicated = 0; + m_prices_replaced = 0; + + /* Iterate over all parsed lines */ + for (auto parsed_lines_it = m_parsed_lines.begin(); + parsed_lines_it != m_parsed_lines.end(); + ++parsed_lines_it) + { + /* Skip current line if the user specified so */ + if ((std::get(*parsed_lines_it))) + continue; + + /* Should not throw anymore, otherwise verify needs revision */ + create_price (parsed_lines_it); + } + PINFO("Number of lines is %d, added %d, duplicated %d, replaced %d", + (int)m_parsed_lines.size(), m_prices_added, m_prices_duplicated, m_prices_replaced); +} + +bool +GncPriceImport::check_for_column_type (GncPricePropType type) +{ + return (std::find (m_settings.m_column_types_price.begin(), + m_settings.m_column_types_price.end(), type) + != m_settings.m_column_types_price.end()); +} + +/* A helper function intended to be called only from set_column_type_price */ +void GncPriceImport::update_price_props (uint32_t row, uint32_t col, GncPricePropType prop_type) +{ + if (prop_type == GncPricePropType::NONE) + return; /* Only deal with price related properties. */ + + auto price_props = std::make_shared (*(std::get(m_parsed_lines[row])).get()); + + if (col >= std::get(m_parsed_lines[row]).size()) + price_props->reset (prop_type); //reset errors + else + { + auto value = std::get(m_parsed_lines[row]).at(col); + bool enable_test_empty = true; + try + { + // set the from_commodity based on combo so we can test for same. + if (prop_type == GncPricePropType::TO_CURRENCY) + { + if (m_settings.m_from_commodity) + price_props->set_from_commodity (m_settings.m_from_commodity); + + if (m_settings.m_to_currency) + enable_test_empty = false; + } + // set the to_currency based on combo so we can test for same. + if (prop_type == GncPricePropType::FROM_COMMODITY) + { + if (m_settings.m_to_currency) + price_props->set_to_currency (m_settings.m_to_currency); + + if (m_settings.m_from_commodity) + enable_test_empty = false; + } + price_props->set(prop_type, value, enable_test_empty); + } + catch (const std::exception& e) + { + /* Do nothing, just prevent the exception from escalating up + * However log the error if it happens on a row that's not skipped + */ + if (!std::get(m_parsed_lines[row])) + PINFO("User warning: %s", e.what()); + } + } + /* Store the result */ + std::get(m_parsed_lines[row]) = price_props; +} + +void +GncPriceImport::set_column_type_price (uint32_t position, GncPricePropType type, bool force) +{ + if (position >= m_settings.m_column_types_price.size()) + return; + + auto old_type = m_settings.m_column_types_price[position]; + if ((type == old_type) && !force) + return; /* Nothing to do */ + + // Column types should be unique, so remove any previous occurrence of the new type + std::replace(m_settings.m_column_types_price.begin(), m_settings.m_column_types_price.end(), + type, GncPricePropType::NONE); + + m_settings.m_column_types_price.at (position) = type; + + // If the user has set a 'commodity from' column, we can't have a commodity from selected + if (type == GncPricePropType::FROM_COMMODITY) + from_commodity (nullptr); + + // If the user has set a 'currency to' column, we can't have a currency to selected + if (type == GncPricePropType::TO_CURRENCY) + to_currency (nullptr); + + /* Update the preparsed data */ + for (auto parsed_lines_it = m_parsed_lines.begin(); + parsed_lines_it != m_parsed_lines.end(); + ++parsed_lines_it) + { + /* Reset date and currency formats for each price props object + * to ensure column updates use the most recent one + */ + std::get(*parsed_lines_it)->set_date_format (m_settings.m_date_format); + std::get(*parsed_lines_it)->set_currency_format (m_settings.m_currency_format); + + uint32_t row = parsed_lines_it - m_parsed_lines.begin(); + + /* If the column type actually changed, first reset the property + * represented by the old column type + */ + if (old_type != type) + { + auto old_col = std::get(*parsed_lines_it).size(); // Deliberately out of bounds to trigger a reset! + if ((old_type > GncPricePropType::NONE) + && (old_type <= GncPricePropType::PRICE_PROPS)) + update_price_props (row, old_col, old_type); + } + /* Then set the property represented by the new column type */ + if ((type > GncPricePropType::NONE) + && (type <= GncPricePropType::PRICE_PROPS)) + update_price_props (row, position, type); + + /* Report errors if there are any */ + auto price_errors = std::get(*parsed_lines_it)->errors(); + std::get(*parsed_lines_it) = + price_errors + + (price_errors.empty() ? std::string() : "\n"); + } +} + +std::vector GncPriceImport::column_types_price () +{ + return m_settings.m_column_types_price; +} + diff --git a/gnucash/import-export/csv-imp/gnc-price-import.hpp b/gnucash/import-export/csv-imp/gnc-price-import.hpp new file mode 100644 index 0000000000..7d181ec168 --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-price-import.hpp @@ -0,0 +1,173 @@ +/********************************************************************\ + * gnc-price-import.hpp - import prices from csv files * + * * + * 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@gnu.org * +\********************************************************************/ + +/** @file + @brief Class to import prices from CSV or fixed width files + * + gnc-price-import.hpp + @author Copyright (c) 2015 Geert Janssens + @author Copyright (c) 2017 Robert Fewell + */ + +#ifndef GNC_PRICE_IMPORT_HPP +#define GNC_PRICE_IMPORT_HPP + +extern "C" { +#include "config.h" +#include "gnc-commodity.h" +} + +#include +#include +#include +#include + +#include "gnc-tokenizer.hpp" +#include "gnc-price-props.hpp" +#include "gnc-csv-price-import-settings.hpp" +#include + +/* A set of currency formats that the user sees. */ +extern const int num_currency_formats_price; +extern const gchar* currency_format_user_price[]; + +/** An enum describing the columns found in a parse_line_t. Currently these are: + * - a tokenized line of input + * - an optional error string + * - a struct to hold user selected properties for a price + * - a boolean to mark the line as skipped by error and/or user or not */ +enum parse_line_cols { + PL_INPUT, + PL_ERROR, + PL_PREPRICE, + PL_SKIP +}; + +/** Tuple to hold + * - a tokenized line of input + * - an optional error string + * - a struct to hold user selected properties for a price */ +using parse_line_t = std::tuple, + bool>; +struct ErrorListPrice; + +/** The actual PriceImport class + * It's intended to use in the following sequence of actions: + * - set a file format + * - load a file + * - optionally convert it's encoding + * - parse the file into lines, which in turn are split up in to columns + * the result of this step can be queried from tokenizer + * - the user should now map the columns to types, which is stored in column_types + * - last step is convert the mapped columns into a list of prices to add */ +class GncPriceImport +{ +public: + // Constructor - Destructor + GncPriceImport(GncImpFileFormat format = GncImpFileFormat::UNKNOWN); + ~GncPriceImport(); + + void file_format(GncImpFileFormat format); + GncImpFileFormat file_format(); + + void over_write (bool over); + bool over_write (); + + void from_commodity (gnc_commodity *from_commodity); + gnc_commodity *from_commodity (); + + void to_currency (gnc_commodity *to_currency); + gnc_commodity *to_currency (); + + void currency_format (int currency_format); + int currency_format (); + + void date_format (int date_format); + int date_format (); + + void encoding (const std::string& encoding); + std::string encoding (); + + void update_skipped_lines (boost::optional start, boost::optional end, + boost::optional alt, boost::optional errors); + uint32_t skip_start_lines (); + uint32_t skip_end_lines (); + bool skip_alt_lines (); + bool skip_err_lines (); + + void separators (std::string separators); + std::string separators (); + + void settings (const CsvPriceImpSettings& settings); + bool save_settings (); + + void settings_name (std::string name); + std::string settings_name (); + + + void load_file (const std::string& filename); + + void tokenize (bool guessColTypes); + + std::string verify(); + + /** This function will attempt to convert all tokenized lines into + * prices using the column types the user has set. + */ + void create_prices (); + bool check_for_column_type (GncPricePropType type); + void set_column_type_price (uint32_t position, GncPricePropType type, bool force = false); + std::vector column_types_price (); + + std::unique_ptr m_tokenizer; /**< Will handle file loading/encoding conversion/splitting into fields */ + std::vector m_parsed_lines; /**< source file parsed into a two-dimensional array of strings. + Per line also holds possible error messages and objects with extracted + price properties. */ + int m_prices_added; + int m_prices_duplicated; + int m_prices_replaced; + +private: + /** A helper function used by create_prices. It will attempt + * to convert a single tokenized line into a price using + * the column types the user has set. + */ + void create_price (std::vector::iterator& parsed_line); + + void verify_column_selections (ErrorListPrice& error_msg); + + /* Internal helper function to force reparsing of columns subject to format changes */ + void reset_formatted_column (std::vector& col_types); + + /* Two internal helper functions that should only be called from within + * set_column_type_price for consistency (otherwise error messages may not be (re)set) + */ + void update_price_props (uint32_t row, uint32_t col, GncPricePropType prop_type); + + CsvPriceImpSettings m_settings; + bool m_skip_errors; + bool m_over_write; +}; + + +#endif diff --git a/gnucash/import-export/csv-imp/gnc-price-props.cpp b/gnucash/import-export/csv-imp/gnc-price-props.cpp new file mode 100644 index 0000000000..151bd38ad2 --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-price-props.cpp @@ -0,0 +1,349 @@ +/********************************************************************\ + * gnc-price-props.cpp - encapsulate price properties for use * + * in the csv importer * + * * + * 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@gnu.org * + * * +\********************************************************************/ + +extern "C" { +#include +#if PLATFORM(WINDOWS) +#include +#endif + +#include +#include + +#include "engine-helpers.h" +#include "gnc-ui-util.h" +} + +#include +#include +#include +#include "gnc-price-props.hpp" + +G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT; + +/* This map contains a set of strings representing the different column types. */ +std::map gnc_price_col_type_strs = { + { GncPricePropType::NONE, N_("None") }, + { GncPricePropType::DATE, N_("Date") }, + { GncPricePropType::AMOUNT, N_("Amount") }, + { GncPricePropType::FROM_COMMODITY, N_("Commodity From") }, + { GncPricePropType::TO_CURRENCY, N_("Currency To") }, +}; + +/** Convert str into a GncNumeric using the user-specified (import) currency format. + * @param str The string to be parsed + * @param currency_format The currency format to use. + * @return a GncNumeric + * @exception May throw std::invalid argument if string can't be parsed properly + */ +GncNumeric parse_amount_price (const std::string &str, int currency_format) +{ + /* If a cell is empty or just spaces return invalid amount */ + if(!boost::regex_search(str, boost::regex("[0-9]"))) + throw std::invalid_argument (_("Value doesn't appear to contain a valid number.")); + + auto expr = boost::make_u32regex("[[:Sc:]]"); + std::string str_no_symbols = boost::u32regex_replace(str, expr, ""); + + /* Convert based on user chosen currency format */ + gnc_numeric val; + char *endptr; + switch (currency_format) + { + case 0: + /* Currency locale */ + if (!(xaccParseAmount (str_no_symbols.c_str(), TRUE, &val, &endptr))) + throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format.")); + break; + case 1: + /* Currency decimal period */ + if (!(xaccParseAmountExtended (str_no_symbols.c_str(), TRUE, '-', '.', ',', "\003\003", "$+", &val, &endptr))) + throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format.")); + break; + case 2: + /* Currency decimal comma */ + if (!(xaccParseAmountExtended (str_no_symbols.c_str(), TRUE, '-', ',', '.', "\003\003", "$+", &val, &endptr))) + throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format.")); + break; + } + + return GncNumeric(val); +} + +/** Convert comm_str into a gnc_commodity. + * @param comm_str The string to be parsed + * @return a gnc_commodity + * @exception May throw std::invalid argument if string can't be parsed properly + */ +gnc_commodity* parse_commodity_price_comm (const std::string& comm_str) +{ + if (comm_str.empty()) + return nullptr; + + auto table = gnc_commodity_table_get_table (gnc_get_current_book()); + gnc_commodity* comm = nullptr; + + /* First try commodity as a unique name. */ + if (comm_str.find("::")) + comm = gnc_commodity_table_lookup_unique (table, comm_str.c_str()); + + /* Then try mnemonic in the currency namespace */ + if (!comm) + comm = gnc_commodity_table_lookup (table, + GNC_COMMODITY_NS_CURRENCY, comm_str.c_str()); + + if (!comm) + { + /* If that fails try mnemonic in all other namespaces */ + auto namespaces = gnc_commodity_table_get_namespaces(table); + for (auto ns = namespaces; ns; ns = ns->next) + { + gchar* ns_str = (gchar*)ns->data; + if (g_utf8_collate(ns_str, GNC_COMMODITY_NS_CURRENCY) == 0) + continue; + + comm = gnc_commodity_table_lookup (table, + ns_str, comm_str.c_str()); + if (comm) + break; + } + } + + if (!comm) + throw std::invalid_argument (_("Value can't be parsed into a valid commodity.")); + else + return comm; +} + +void GncImportPrice::set (GncPricePropType prop_type, const std::string& value, bool enable_test_empty) +{ + try + { + // Drop any existing error for the prop_type we're about to set + m_errors.erase(prop_type); + + // conditional test for empty values + if (value.empty() && enable_test_empty) + throw std::invalid_argument (_("Column value can not be empty.")); + + gnc_commodity *comm = nullptr; + switch (prop_type) + { + case GncPricePropType::DATE: + m_date = boost::none; + m_date = GncDate(value, GncDate::c_formats[m_date_format].m_fmt); // Throws if parsing fails + break; + + case GncPricePropType::AMOUNT: + m_amount = boost::none; + m_amount = parse_amount_price (value, m_currency_format); // Throws if parsing fails + break; + + case GncPricePropType::FROM_COMMODITY: + m_from_commodity = boost::none; + comm = parse_commodity_price_comm (value); // Throws if parsing fails + if (comm) + { + if (m_to_currency == comm) + throw std::invalid_argument (_("'Commodity From' can not be the same as 'Currency To' column type.")); + m_from_commodity = comm; + } + break; + + case GncPricePropType::TO_CURRENCY: + m_to_currency = boost::none; + comm = parse_commodity_price_comm (value); // Throws if parsing fails + if (comm) + { + if (m_from_commodity == comm) + throw std::invalid_argument (_("'Currency To' can not be the same as 'Commodity From' column type.")); + if (gnc_commodity_is_currency (comm) != true) + throw std::invalid_argument (_("Value parsed into an invalid currency for a currency column type.")); + m_to_currency = comm; + } + break; + + default: + /* Issue a warning for all other prop_types. */ + PWARN ("%d is an invalid property for a Price", static_cast(prop_type)); + break; + } + } + catch (const std::invalid_argument& e) + { + auto err_str = std::string(_(gnc_price_col_type_strs[prop_type])) + + std::string(_(" could not be understood.\n")) + + e.what(); + m_errors.emplace(prop_type, err_str); + throw std::invalid_argument (err_str); + } + catch (const std::out_of_range& e) + { + auto err_str = std::string(_(gnc_price_col_type_strs[prop_type])) + + std::string(_(" could not be understood.\n")) + + e.what(); + m_errors.emplace(prop_type, err_str); + throw std::invalid_argument (err_str); + } +} + +void GncImportPrice::reset (GncPricePropType prop_type) +{ + try + { + // set enable_test_empty to false to allow empty values + set (prop_type, std::string(), false); + } + catch (...) + { + // Set with an empty string will effectively clear the property + // but can also set an error for the property. Clear that error here. + m_errors.erase(prop_type); + } +} + +std::string GncImportPrice::verify_essentials (void) +{ + /* Make sure this price has the minimum required set of properties defined */ + if (m_date == boost::none) + return _("No date column."); + else if (m_amount == boost::none) + return _("No amount column."); + else if (m_to_currency == boost::none) + return _("No 'Currency to' column."); + else if (m_from_commodity == boost::none) + return _("No 'Commodity from' column."); + else if (gnc_commodity_equal (*m_from_commodity, *m_to_currency)) + return _("'Commodity from' can not be the same as 'Currency to'."); + else + return std::string(); +} + +Result GncImportPrice::create_price (QofBook* book, GNCPriceDB *pdb, bool over) +{ + /* Gently refuse to create the price if the basics are not set correctly + * This should have been tested before calling this function though! + */ + auto check = verify_essentials(); + if (!check.empty()) + { + PWARN ("Refusing to create price because essentials not set properly: %s", check.c_str()); + return FAILED; + } + + Timespec date; + timespecFromTime64 (&date, static_cast(GncDateTime(*m_date, DayPart::neutral))); + date.tv_nsec = 0; + + bool rev = false; + auto amount = *m_amount; + Result ret_val = ADDED; + + GNCPrice *old_price = gnc_pricedb_lookup_day (pdb, *m_from_commodity, *m_to_currency, date); + + // Should old price be over writen + if ((old_price != nullptr) && (over == true)) + { + DEBUG("Over write"); + gnc_pricedb_remove_price (pdb, old_price); + gnc_price_unref (old_price); + old_price = nullptr; + ret_val = REPLACED; + } + + if (gnc_commodity_is_currency (*m_from_commodity)) // Currency Import + { + // Check for currency in reverse direction. + if (old_price != nullptr) + { + // Check for price in reverse direction. + if (gnc_commodity_equiv (gnc_price_get_currency (old_price), *m_from_commodity)) + rev = true; + } + DEBUG("Commodity from is a Currency"); + + // Check for price less than 1, reverse if so. + if (*m_amount < GncNumeric(1,1)) + rev = true; + + } + DEBUG("Date is %s, Rev is %d, Commodity from is '%s', Currency is '%s', Amount is %s", gnc_print_date (date), + rev, gnc_commodity_get_fullname (*m_from_commodity), gnc_commodity_get_fullname (*m_to_currency), + amount.to_string().c_str()); + + // Create the new price + if (old_price == nullptr) + { + DEBUG("Create"); + GNCPrice *price = gnc_price_create (book); + gnc_price_begin_edit (price); + if (rev) + { + amount = amount.inv(); //invert the amount + gnc_price_set_commodity (price, *m_to_currency); + gnc_price_set_currency (price, *m_from_commodity); + } + else + { + gnc_price_set_commodity (price, *m_from_commodity); + gnc_price_set_currency (price, *m_to_currency); + } + auto amount_conv = amount.convert(CURRENCY_DENOM); + gnc_price_set_value (price, static_cast(amount_conv)); + + gnc_price_set_time (price, date); + gnc_price_set_source (price, PRICE_SOURCE_USER_PRICE); +//FIXME Not sure which one gnc_price_set_source (price, PRICE_SOURCE_FQ); + gnc_price_set_typestr (price, PRICE_TYPE_LAST); + gnc_price_commit_edit (price); + + bool perr = gnc_pricedb_add_price (pdb, price); + + gnc_price_unref (price); + + if (perr == false) + throw std::invalid_argument (_("Failed to create price from selected columns.")); + } + else + { + gnc_price_unref (old_price); + ret_val = DUPLICATED; + } + return ret_val; +} + +static std::string gen_err_str (std::map& errors) +{ + auto full_error = std::string(); + for (auto error : errors) + { + full_error += (full_error.empty() ? "" : "\n") + error.second; + } + return full_error; +} + +std::string GncImportPrice::errors () +{ + return gen_err_str (m_errors); +} + diff --git a/gnucash/import-export/csv-imp/gnc-price-props.hpp b/gnucash/import-export/csv-imp/gnc-price-props.hpp new file mode 100644 index 0000000000..3a358612af --- /dev/null +++ b/gnucash/import-export/csv-imp/gnc-price-props.hpp @@ -0,0 +1,116 @@ +/********************************************************************\ + * gnc-price-props.hpp - encapsulate price properties for use * + * in the csv importer * + * * + * 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@gnu.org * + * * +\********************************************************************/ + +#ifndef GNC_PRICE_PROPS_HPP +#define GNC_PRICE_PROPS_HPP + +extern "C" { +#include +#if PLATFORM(WINDOWS) +#include +#endif + +#include +#include "gnc-pricedb.h" +#include "gnc-commodity.h" +} + +#include +#include +#include +#include +#include +#include + +/** Enumeration for column types. These are the different types of + * columns that can exist in a CSV/Fixed-Width file. There should be + * no two columns with the same type except for the GncPricePropType::NONE + * type. */ +enum class GncPricePropType { + NONE, + DATE, + AMOUNT, + FROM_COMMODITY, + TO_CURRENCY, + PRICE_PROPS = TO_CURRENCY +}; + +enum Result { FAILED, ADDED, DUPLICATED, REPLACED }; + +/** Maps all column types to a string representation. + * The actual definition is in gnc-price-props.cpp. + * Attention: that definition should be adjusted for any + * changes to enum class GncPricePropType ! */ +extern std::map gnc_price_col_type_strs; + +/** Functor to check if the above map has an element of which + * the value equals name. To be used with std::find_if. + */ +struct test_price_prop_type_str +{ + test_price_prop_type_str( const char* name ) : m_name(name) {} + bool operator()( const std::pair& v ) const + { + return !g_strcmp0(v.second, m_name); + } +private: + const char *m_name; +}; + +gnc_commodity* parse_commodity_price_comm (const std::string& comm_str); +GncNumeric parse_amount_price (const std::string &str, int currency_format); + +struct GncImportPrice +{ +public: + GncImportPrice (int date_format, int currency_format) : m_date_format{date_format}, + m_currency_format{currency_format}{}; + + void set (GncPricePropType prop_type, const std::string& value, bool enable_test_empty); + void set_date_format (int date_format) { m_date_format = date_format ;} + void set_currency_format (int currency_format) { m_currency_format = currency_format ;} + void reset (GncPricePropType prop_type); + std::string verify_essentials (void); + Result create_price (QofBook* book, GNCPriceDB *pdb, bool over); + + gnc_commodity* get_from_commodity () { if (m_from_commodity) return *m_from_commodity; else return nullptr; } + void set_from_commodity (gnc_commodity* comm) { if (comm) m_from_commodity = comm; else m_from_commodity = boost::none; } + + gnc_commodity* get_to_currency () { if (m_to_currency) return *m_to_currency; else return nullptr; } + void set_to_currency (gnc_commodity* curr) { if (curr) m_to_currency = curr; else m_to_currency = boost::none; } + + std::string errors(); + +private: + int m_date_format; + int m_currency_format; + boost::optional m_date; + boost::optional m_amount; + boost::optional m_from_commodity; + boost::optional m_to_currency; + bool created = false; + + std::map m_errors; +}; + +#endif diff --git a/gnucash/import-export/csv-imp/gnc-tx-import.cpp b/gnucash/import-export/csv-imp/gnc-tx-import.cpp index daa85b4d98..a43f29c543 100644 --- a/gnucash/import-export/csv-imp/gnc-tx-import.cpp +++ b/gnucash/import-export/csv-imp/gnc-tx-import.cpp @@ -39,7 +39,7 @@ extern "C" { #include "gnc-trans-props.hpp" #include "gnc-csv-tokenizer.hpp" #include "gnc-fw-tokenizer.hpp" -#include "gnc-csv-trans-settings.hpp" +#include "gnc-csv-trans-import-settings.hpp" G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT; @@ -293,7 +293,7 @@ void GncTxImport::separators (std::string separators) } std::string GncTxImport::separators () { return m_settings.m_separators; } -void GncTxImport::settings (const CsvTransSettings& settings) +void GncTxImport::settings (const CsvTransImpSettings& settings) { /* First apply file format as this may recreate the tokenizer */ file_format (settings.m_file_format); @@ -329,7 +329,7 @@ void GncTxImport::settings (const CsvTransSettings& settings) bool GncTxImport::save_settings () { - if (trans_preset_is_reserved_name (m_settings.m_name)) + if (preset_is_reserved_name (m_settings.m_name)) return true; /* separators are already copied to m_settings in the separators diff --git a/gnucash/import-export/csv-imp/gnc-tx-import.hpp b/gnucash/import-export/csv-imp/gnc-tx-import.hpp index 0c14a7489d..4fc339cbb3 100644 --- a/gnucash/import-export/csv-imp/gnc-tx-import.hpp +++ b/gnucash/import-export/csv-imp/gnc-tx-import.hpp @@ -43,7 +43,7 @@ extern "C" { #include "gnc-tokenizer.hpp" #include "gnc-trans-props.hpp" -#include "gnc-csv-trans-settings.hpp" +#include "gnc-csv-trans-import-settings.hpp" #include @@ -136,7 +136,7 @@ public: void separators (std::string separators); std::string separators (); - void settings (const CsvTransSettings& settings); + void settings (const CsvTransImpSettings& settings); bool save_settings (); void settings_name (std::string name); @@ -189,8 +189,8 @@ private: void update_pre_trans_props (uint32_t row, uint32_t col, GncTransPropType prop_type); void update_pre_split_props (uint32_t row, uint32_t col, GncTransPropType prop_type); - struct CsvTranSettings; - CsvTransSettings m_settings; + struct CsvTranImpSettings; //FIXME do we need this line + CsvTransImpSettings m_settings; bool m_skip_errors; bool m_req_mapped_accts; diff --git a/po/POTFILES.in b/po/POTFILES.in index 190bd219a6..b304d9f217 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -287,17 +287,21 @@ gnucash/import-export/csv-exp/gnc-plugin-csv-export.c [type: gettext/gsettings]gnucash/import-export/csv-exp/gschemas/org.gnucash.dialogs.export.csv.gschema.xml.in.in gnucash/import-export/csv-imp/assistant-csv-account-import.c gnucash/import-export/csv-imp/assistant-csv-account-import.glade +gnucash/import-export/csv-imp/assistant-csv-price-import.cpp +gnucash/import-export/csv-imp/assistant-csv-price-import.glade gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp gnucash/import-export/csv-imp/assistant-csv-trans-import.glade gnucash/import-export/csv-imp/csv-account-import.c gnucash/import-export/csv-imp/gnc-csv-account-map.c gnucash/import-export/csv-imp/gnc-csv-gnumeric-popup.c gnucash/import-export/csv-imp/gnc-csv-tokenizer.cpp -gnucash/import-export/csv-imp/gnc-csv-trans-settings.cpp +gnucash/import-export/csv-imp/gnc-csv-import-settings.cpp gnucash/import-export/csv-imp/gnc-dummy-tokenizer.cpp gnucash/import-export/csv-imp/gnc-fw-tokenizer.cpp gnucash/import-export/csv-imp/gncmod-csv-import.c gnucash/import-export/csv-imp/gnc-plugin-csv-import.c +gnucash/import-export/csv-imp/gnc-price-import.cpp +gnucash/import-export/csv-imp/gnc-price-props.cpp gnucash/import-export/csv-imp/gnc-tokenizer.cpp gnucash/import-export/csv-imp/gnc-trans-props.cpp gnucash/import-export/csv-imp/gnc-tx-import.cpp