diff --git a/app/controllers/import/rows_controller.rb b/app/controllers/import/rows_controller.rb index b5b9092c..a3905b14 100644 --- a/app/controllers/import/rows_controller.rb +++ b/app/controllers/import/rows_controller.rb @@ -2,9 +2,7 @@ class Import::RowsController < ApplicationController before_action :set_import_row def update - @row.assign_attributes(row_params) - @row.save!(validate: false) - @row.sync_mappings + @row.update_and_sync(row_params) redirect_to import_row_path(@row.import, @row) end diff --git a/app/controllers/import/uploads_controller.rb b/app/controllers/import/uploads_controller.rb index 42e6c975..d30e0082 100644 --- a/app/controllers/import/uploads_controller.rb +++ b/app/controllers/import/uploads_controller.rb @@ -12,7 +12,7 @@ class Import::UploadsController < ApplicationController @import.assign_attributes(raw_file_str: csv_str, col_sep: upload_params[:col_sep]) @import.save!(validate: false) - redirect_to import_configuration_path(@import), notice: "CSV uploaded successfully." + redirect_to import_configuration_path(@import, template_hint: true), notice: "CSV uploaded successfully." else flash.now[:alert] = "Must be valid CSV with headers and at least one row of data" diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index e95e6e11..c1b51c23 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -1,5 +1,5 @@ class ImportsController < ApplicationController - before_action :set_import, only: %i[show publish destroy revert] + before_action :set_import, only: %i[show publish destroy revert apply_template] def publish @import.publish_later @@ -19,7 +19,11 @@ class ImportsController < ApplicationController def create account = Current.family.accounts.find_by(id: params.dig(:import, :account_id)) - import = Current.family.imports.create!(type: import_params[:type], account: account) + import = Current.family.imports.create!( + type: import_params[:type], + account: account, + date_format: Current.family.date_format, + ) redirect_to import_upload_path(import) end @@ -37,6 +41,15 @@ class ImportsController < ApplicationController redirect_to imports_path, notice: "Import is reverting in the background." end + def apply_template + if @import.suggested_template + @import.apply_template!(@import.suggested_template) + redirect_to import_configuration_path(@import), notice: "Template applied." + else + redirect_to import_configuration_path(@import), alert: "No template found, please manually configure your import." + end + end + def destroy @import.destroy diff --git a/app/models/import.rb b/app/models/import.rb index 600f30a7..662b4cee 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -146,8 +146,20 @@ class Import < ApplicationRecord end def sync_mappings - mapping_steps.each do |mapping| - mapping.sync(self) + transaction do + mapping_steps.each do |mapping_class| + mappables_by_key = mapping_class.mappables_by_key(self) + + updated_mappings = mappables_by_key.map do |key, mappable| + mapping = mappings.find_or_initialize_by(key: key, import: self, type: mapping_class.name) + mapping.mappable = mappable + mapping.create_when_empty = key.present? && mappable.nil? + mapping + end + + updated_mappings.each { |m| m.save(validate: false) } + mapping_class.where.not(id: updated_mappings.map(&:id)).destroy_all + end end end @@ -183,6 +195,28 @@ class Import < ApplicationRecord family.accounts.empty? && has_unassigned_account? end + # Used to optionally pre-fill the configuration for the current import + def suggested_template + family.imports + .complete + .where(account: account, type: type) + .order(created_at: :desc) + .first + end + + def apply_template!(import_template) + update!( + import_template.attributes.slice( + "date_col_label", "amount_col_label", "name_col_label", + "category_col_label", "tags_col_label", "account_col_label", + "qty_col_label", "ticker_col_label", "price_col_label", + "entity_type_col_label", "notes_col_label", "currency_col_label", + "date_format", "signage_convention", "number_format", + "exchange_operating_mic_col_label" + ) + ) + end + private def import! # no-op, subclasses can implement for customization of algorithm diff --git a/app/models/import/account_mapping.rb b/app/models/import/account_mapping.rb index 9cf43f7a..67280da4 100644 --- a/app/models/import/account_mapping.rb +++ b/app/models/import/account_mapping.rb @@ -2,8 +2,11 @@ class Import::AccountMapping < Import::Mapping validates :mappable, presence: true, if: :requires_mapping? class << self - def mapping_values(import) - import.rows.map(&:account).uniq + def mappables_by_key(import) + unique_values = import.rows.map(&:account).uniq + accounts = import.family.accounts.where(name: unique_values).index_by(&:name) + + unique_values.index_with { |value| accounts[value] } end end diff --git a/app/models/import/account_type_mapping.rb b/app/models/import/account_type_mapping.rb index 8b60edcd..2d4b5431 100644 --- a/app/models/import/account_type_mapping.rb +++ b/app/models/import/account_type_mapping.rb @@ -2,8 +2,8 @@ class Import::AccountTypeMapping < Import::Mapping validates :value, presence: true class << self - def mapping_values(import) - import.rows.map(&:entity_type).uniq + def mappables_by_key(import) + import.rows.map(&:entity_type).uniq.index_with { nil } end end diff --git a/app/models/import/category_mapping.rb b/app/models/import/category_mapping.rb index 12302603..4b633ea4 100644 --- a/app/models/import/category_mapping.rb +++ b/app/models/import/category_mapping.rb @@ -1,7 +1,10 @@ class Import::CategoryMapping < Import::Mapping class << self - def mapping_values(import) - import.rows.map(&:category).uniq + def mappables_by_key(import) + unique_values = import.rows.map(&:category).uniq + categories = import.family.categories.where(name: unique_values).index_by(&:name) + + unique_values.index_with { |value| categories[value] } end end diff --git a/app/models/import/mapping.rb b/app/models/import/mapping.rb index a0a4bc8b..b783ef95 100644 --- a/app/models/import/mapping.rb +++ b/app/models/import/mapping.rb @@ -18,19 +18,8 @@ class Import::Mapping < ApplicationRecord find_by(key: key)&.mappable end - def sync(import) - unique_values = mapping_values(import).uniq - - unique_values.each do |value| - mapping = find_or_initialize_by(key: value, import: import, create_when_empty: value.present?) - mapping.save(validate: false) if mapping.new_record? - end - - where(import: import).where.not(key: unique_values).destroy_all - end - - def mapping_values(import) - raise NotImplementedError, "Subclass must implement mapping_values" + def mappables_by_key(import) + raise NotImplementedError, "Subclass must implement mappables_by_key" end end diff --git a/app/models/import/row.rb b/app/models/import/row.rb index d4316a60..622a9d0a 100644 --- a/app/models/import/row.rb +++ b/app/models/import/row.rb @@ -30,11 +30,10 @@ class Import::Row < ApplicationRecord end end - def sync_mappings - Import::CategoryMapping.sync(import) if import.column_keys.include?(:category) - Import::TagMapping.sync(import) if import.column_keys.include?(:tags) - Import::AccountMapping.sync(import) if import.column_keys.include?(:account) - Import::AccountTypeMapping.sync(import) if import.column_keys.include?(:entity_type) + def update_and_sync(params) + assign_attributes(params) + save!(validate: false) + import.sync_mappings end private diff --git a/app/models/import/tag_mapping.rb b/app/models/import/tag_mapping.rb index 899b4dc5..4b9a1be0 100644 --- a/app/models/import/tag_mapping.rb +++ b/app/models/import/tag_mapping.rb @@ -1,7 +1,11 @@ class Import::TagMapping < Import::Mapping class << self - def mapping_values(import) - import.rows.map(&:tags_list).flatten.uniq + def mappables_by_key(import) + unique_values = import.rows.map(&:tags_list).flatten.uniq + + tags = import.family.tags.where(name: unique_values).index_by(&:name) + + unique_values.index_with { |value| tags[value] } end end diff --git a/app/views/import/configurations/show.html.erb b/app/views/import/configurations/show.html.erb index a807c97e..aaefd4e6 100644 --- a/app/views/import/configurations/show.html.erb +++ b/app/views/import/configurations/show.html.erb @@ -4,14 +4,33 @@ <%= content_for :previous_path, import_upload_path(@import) %> -
We found a configuration from a previous import for this account. Would you like to apply it to this import?
+ +<%= t(".description") %>