mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Allow CSV imports to be configured with single or multi-account mode (#1943)
* Allow CSV imports to be configured to a single account or multiple accounts * Initialize import directly from accounts page * Fix brakeman warnings * Fix schema * Fix Synth check
This commit is contained in:
parent
e907b073ed
commit
c5da8ea550
20 changed files with 118 additions and 57 deletions
|
@ -4,6 +4,10 @@ class Import::ConfirmsController < ApplicationController
|
||||||
before_action :set_import
|
before_action :set_import
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
if @import.mapping_steps.empty?
|
||||||
|
return redirect_to import_path(@import)
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to import_clean_path(@import), alert: "You have invalid data, please edit until all errors are resolved" unless @import.cleaned?
|
redirect_to import_clean_path(@import), alert: "You have invalid data, please edit until all errors are resolved" unless @import.cleaned?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ class Import::UploadsController < ApplicationController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if csv_valid?(csv_str)
|
if csv_valid?(csv_str)
|
||||||
|
@import.account = Current.family.accounts.find_by(id: params.dig(:import, :account_id))
|
||||||
@import.assign_attributes(raw_file_str: csv_str, col_sep: upload_params[:col_sep])
|
@import.assign_attributes(raw_file_str: csv_str, col_sep: upload_params[:col_sep])
|
||||||
@import.save!(validate: false)
|
@import.save!(validate: false)
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@ class ImportsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
import = Current.family.imports.create! import_params
|
account = Current.family.accounts.find_by(id: params.dig(:import, :account_id))
|
||||||
|
import = Current.family.imports.create!(type: import_params[:type], account: account)
|
||||||
|
|
||||||
redirect_to import_upload_path(import)
|
redirect_to import_upload_path(import)
|
||||||
end
|
end
|
||||||
|
|
|
@ -106,7 +106,7 @@ class Family < ApplicationRecord
|
||||||
# If family has any entries in different currencies, they need a provider for historical exchange rates
|
# If family has any entries in different currencies, they need a provider for historical exchange rates
|
||||||
uniq_currencies = entries.pluck(:currency).uniq
|
uniq_currencies = entries.pluck(:currency).uniq
|
||||||
return true if uniq_currencies.count > 1
|
return true if uniq_currencies.count > 1
|
||||||
return true if uniq_currencies.first != self.currency
|
return true if uniq_currencies.count > 0 && uniq_currencies.first != self.currency
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
class Import < ApplicationRecord
|
class Import < ApplicationRecord
|
||||||
TYPES = %w[TransactionImport TradeImport AccountImport MintImport].freeze
|
TYPES = %w[TransactionImport TradeImport AccountImport MintImport].freeze
|
||||||
SIGNAGE_CONVENTIONS = %w[inflows_positive inflows_negative]
|
SIGNAGE_CONVENTIONS = %w[inflows_positive inflows_negative]
|
||||||
|
SEPARATORS = [ [ "Comma (,)", "," ], [ "Semicolon (;)", ";" ] ].freeze
|
||||||
|
|
||||||
NUMBER_FORMATS = {
|
NUMBER_FORMATS = {
|
||||||
"1,234.56" => { separator: ".", delimiter: "," }, # US/UK/Asia
|
"1,234.56" => { separator: ".", delimiter: "," }, # US/UK/Asia
|
||||||
|
@ -10,6 +11,7 @@ class Import < ApplicationRecord
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
belongs_to :family
|
belongs_to :family
|
||||||
|
belongs_to :account, optional: true
|
||||||
|
|
||||||
before_validation :set_default_number_format
|
before_validation :set_default_number_format
|
||||||
|
|
||||||
|
@ -25,7 +27,7 @@ class Import < ApplicationRecord
|
||||||
}, validate: true, default: "pending"
|
}, validate: true, default: "pending"
|
||||||
|
|
||||||
validates :type, inclusion: { in: TYPES }
|
validates :type, inclusion: { in: TYPES }
|
||||||
validates :col_sep, inclusion: { in: [ ",", ";" ] }
|
validates :col_sep, inclusion: { in: SEPARATORS.map(&:last) }
|
||||||
validates :signage_convention, inclusion: { in: SIGNAGE_CONVENTIONS }
|
validates :signage_convention, inclusion: { in: SIGNAGE_CONVENTIONS }
|
||||||
validates :number_format, presence: true, inclusion: { in: NUMBER_FORMATS.keys }
|
validates :number_format, presence: true, inclusion: { in: NUMBER_FORMATS.keys }
|
||||||
|
|
||||||
|
@ -98,12 +100,17 @@ class Import < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def dry_run
|
def dry_run
|
||||||
{
|
mappings = {
|
||||||
transactions: rows.count,
|
transactions: rows.count,
|
||||||
accounts: Import::AccountMapping.for_import(self).creational.count,
|
|
||||||
categories: Import::CategoryMapping.for_import(self).creational.count,
|
categories: Import::CategoryMapping.for_import(self).creational.count,
|
||||||
tags: Import::TagMapping.for_import(self).creational.count
|
tags: Import::TagMapping.for_import(self).creational.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mappings.merge(
|
||||||
|
accounts: Import::AccountMapping.for_import(self).creational.count,
|
||||||
|
) if account.nil?
|
||||||
|
|
||||||
|
mappings
|
||||||
end
|
end
|
||||||
|
|
||||||
def required_column_keys
|
def required_column_keys
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class Import::AccountMapping < Import::Mapping
|
class Import::AccountMapping < Import::Mapping
|
||||||
validates :mappable, presence: true, if: -> { key.blank? || !create_when_empty }
|
validates :mappable, presence: true, if: :requires_mapping?
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def mapping_values(import)
|
def mapping_values(import)
|
||||||
|
@ -42,4 +42,9 @@ class Import::AccountMapping < Import::Mapping
|
||||||
self.mappable = account
|
self.mappable = account
|
||||||
save!
|
save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def requires_mapping?
|
||||||
|
(key.blank? || !create_when_empty) && import.account.nil?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,11 @@ class TradeImport < Import
|
||||||
mappings.each(&:create_mappable!)
|
mappings.each(&:create_mappable!)
|
||||||
|
|
||||||
rows.each do |row|
|
rows.each do |row|
|
||||||
account = mappings.accounts.mappable_for(row.account)
|
mapped_account = if account
|
||||||
|
account
|
||||||
|
else
|
||||||
|
mappings.accounts.mappable_for(row.account)
|
||||||
|
end
|
||||||
|
|
||||||
# Try to find or create security with ticker only
|
# Try to find or create security with ticker only
|
||||||
security = find_or_create_security(
|
security = find_or_create_security(
|
||||||
|
@ -12,15 +16,15 @@ class TradeImport < Import
|
||||||
exchange_operating_mic: row.exchange_operating_mic
|
exchange_operating_mic: row.exchange_operating_mic
|
||||||
)
|
)
|
||||||
|
|
||||||
entry = account.entries.build \
|
entry = mapped_account.entries.build \
|
||||||
date: row.date_iso,
|
date: row.date_iso,
|
||||||
amount: row.signed_amount,
|
amount: row.signed_amount,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
currency: row.currency.presence || account.currency,
|
currency: row.currency.presence || mapped_account.currency,
|
||||||
entryable: Account::Trade.new(
|
entryable: Account::Trade.new(
|
||||||
security: security,
|
security: security,
|
||||||
qty: row.qty,
|
qty: row.qty,
|
||||||
currency: row.currency.presence || account.currency,
|
currency: row.currency.presence || mapped_account.currency,
|
||||||
price: row.price
|
price: row.price
|
||||||
),
|
),
|
||||||
import: self
|
import: self
|
||||||
|
@ -31,7 +35,9 @@ class TradeImport < Import
|
||||||
end
|
end
|
||||||
|
|
||||||
def mapping_steps
|
def mapping_steps
|
||||||
[ Import::AccountMapping ]
|
base = []
|
||||||
|
base << Import::AccountMapping if account.nil?
|
||||||
|
base
|
||||||
end
|
end
|
||||||
|
|
||||||
def required_column_keys
|
def required_column_keys
|
||||||
|
@ -39,14 +45,19 @@ class TradeImport < Import
|
||||||
end
|
end
|
||||||
|
|
||||||
def column_keys
|
def column_keys
|
||||||
%i[date ticker exchange_operating_mic currency qty price account name]
|
base = %i[date ticker exchange_operating_mic currency qty price name]
|
||||||
|
base.unshift(:account) if account.nil?
|
||||||
|
base
|
||||||
end
|
end
|
||||||
|
|
||||||
def dry_run
|
def dry_run
|
||||||
{
|
mappings = { transactions: rows.count }
|
||||||
transactions: rows.count,
|
|
||||||
|
mappings.merge(
|
||||||
accounts: Import::AccountMapping.for_import(self).creational.count
|
accounts: Import::AccountMapping.for_import(self).creational.count
|
||||||
}
|
) if account.nil?
|
||||||
|
|
||||||
|
mappings
|
||||||
end
|
end
|
||||||
|
|
||||||
def csv_template
|
def csv_template
|
||||||
|
@ -57,7 +68,9 @@ class TradeImport < Import
|
||||||
05/17/2024,TSLA,XNAS,USD,2,700.50,Retirement Account,Tesla Inc. Purchase
|
05/17/2024,TSLA,XNAS,USD,2,700.50,Retirement Account,Tesla Inc. Purchase
|
||||||
CSV
|
CSV
|
||||||
|
|
||||||
CSV.parse(template, headers: true)
|
csv = CSV.parse(template, headers: true)
|
||||||
|
csv.delete("account") if account.present?
|
||||||
|
csv
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -4,11 +4,16 @@ class TransactionImport < Import
|
||||||
mappings.each(&:create_mappable!)
|
mappings.each(&:create_mappable!)
|
||||||
|
|
||||||
rows.each do |row|
|
rows.each do |row|
|
||||||
account = mappings.accounts.mappable_for(row.account)
|
mapped_account = if account
|
||||||
|
account
|
||||||
|
else
|
||||||
|
mappings.accounts.mappable_for(row.account)
|
||||||
|
end
|
||||||
|
|
||||||
category = mappings.categories.mappable_for(row.category)
|
category = mappings.categories.mappable_for(row.category)
|
||||||
tags = row.tags_list.map { |tag| mappings.tags.mappable_for(tag) }.compact
|
tags = row.tags_list.map { |tag| mappings.tags.mappable_for(tag) }.compact
|
||||||
|
|
||||||
entry = account.entries.build \
|
entry = mapped_account.entries.build \
|
||||||
date: row.date_iso,
|
date: row.date_iso,
|
||||||
amount: row.signed_amount,
|
amount: row.signed_amount,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
@ -27,11 +32,15 @@ class TransactionImport < Import
|
||||||
end
|
end
|
||||||
|
|
||||||
def column_keys
|
def column_keys
|
||||||
%i[date amount name currency category tags account notes]
|
base = %i[date amount name currency category tags notes]
|
||||||
|
base.unshift(:account) if account.nil?
|
||||||
|
base
|
||||||
end
|
end
|
||||||
|
|
||||||
def mapping_steps
|
def mapping_steps
|
||||||
[ Import::CategoryMapping, Import::TagMapping, Import::AccountMapping ]
|
base = [ Import::CategoryMapping, Import::TagMapping ]
|
||||||
|
base << Import::AccountMapping if account.nil?
|
||||||
|
base
|
||||||
end
|
end
|
||||||
|
|
||||||
def csv_template
|
def csv_template
|
||||||
|
@ -42,6 +51,8 @@ class TransactionImport < Import
|
||||||
05/17/2024,-12.50,Coffee Shop,,,coffee,,
|
05/17/2024,-12.50,Coffee Shop,,,coffee,,
|
||||||
CSV
|
CSV
|
||||||
|
|
||||||
CSV.parse(template, headers: true)
|
csv = CSV.parse(template, headers: true)
|
||||||
|
csv.delete("account") if account.present?
|
||||||
|
csv
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% unless account.crypto? %>
|
<% unless account.crypto? %>
|
||||||
<%= link_to new_import_path,
|
<%= button_to imports_path({ import: { type: account.investment? ? "TradeImport" : "TransactionImport", account_id: account.id } }),
|
||||||
data: { turbo_frame: :modal },
|
data: { turbo_frame: :_top },
|
||||||
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg" do %>
|
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg" do %>
|
||||||
<%= lucide_icon "download", class: "w-5 h-5 text-secondary" %>
|
<%= lucide_icon "download", class: "w-5 h-5 text-secondary" %>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<%# locals: (import:) %>
|
<%# locals: (import:) %>
|
||||||
|
|
||||||
<div class="flex items-center justify-between border border-secondary rounded-lg bg-green-500/5 p-5 gap-4">
|
<div class="flex items-center justify-between border border-secondary rounded-lg bg-green-500/5 p-5 gap-4 mb-4">
|
||||||
<%= lucide_icon("check-circle", class: "w-5 h-5 shrink-0 text-green-500") %>
|
<%= lucide_icon("check-circle", class: "w-5 h-5 shrink-0 text-green-500") %>
|
||||||
<p class="text-sm text-primary italic">We have pre-configured your Mint import for you. Please proceed to the next step.</p>
|
<p class="text-sm text-primary italic">We have pre-configured your Mint import for you. Please proceed to the next step.</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,10 @@
|
||||||
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% unless import.account.present? %>
|
||||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" }, disabled: import.complete? %>
|
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" }, disabled: import.complete? %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" }, disabled: import.complete? %>
|
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" }, disabled: import.complete? %>
|
||||||
<%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" }, disabled: import.complete? %>
|
<%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" }, disabled: import.complete? %>
|
||||||
<%= form.select :tags_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Tags (optional)" }, disabled: import.complete? %>
|
<%= form.select :tags_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Tags (optional)" }, disabled: import.complete? %>
|
||||||
|
|
|
@ -20,7 +20,11 @@
|
||||||
<%= form.select :ticker_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Ticker" } %>
|
<%= form.select :ticker_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Ticker" } %>
|
||||||
<%= form.select :exchange_operating_mic_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Exchange Operating MIC" } %>
|
<%= form.select :exchange_operating_mic_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Exchange Operating MIC" } %>
|
||||||
<%= form.select :price_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Price" } %>
|
<%= form.select :price_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Price" } %>
|
||||||
|
|
||||||
|
<% unless import.account.present? %>
|
||||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
||||||
|
|
||||||
<% unless Security.provider %>
|
<% unless Security.provider %>
|
||||||
|
|
|
@ -16,7 +16,10 @@
|
||||||
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% unless import.account.present? %>
|
||||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
||||||
<%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" } %>
|
<%= form.select :category_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Category (optional)" } %>
|
||||||
<%= form.select :tags_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Tags (optional)" } %>
|
<%= form.select :tags_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Tags (optional)" } %>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<% mappings = mapping_class.for_import(import) %>
|
<% mappings = mapping_class.for_import(import) %>
|
||||||
<% is_last_step = step_idx == import.mapping_steps.count - 1 %>
|
<% is_last_step = step_idx == import.mapping_steps.count - 1 %>
|
||||||
|
|
||||||
<% if mapping_class == Import::AccountMapping %>
|
<% if mapping_class == Import::AccountMapping && import.account.nil? %>
|
||||||
<% if import.requires_account? %>
|
<% if import.requires_account? %>
|
||||||
<div class="flex items-center justify-between p-4 mb-4 gap-4 text-secondary bg-red-100 border border-red-200 rounded-lg w-[650px]">
|
<div class="flex items-center justify-between p-4 mb-4 gap-4 text-secondary bg-red-100 border border-red-200 rounded-lg w-[650px]">
|
||||||
<%= tag.p t(".no_accounts"), class: "text-sm" %>
|
<%= tag.p t(".no_accounts"), class: "text-sm" %>
|
||||||
|
|
|
@ -19,33 +19,33 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tabs-target="tab" id="csv-paste-tab">
|
<% ["csv-paste-tab", "csv-upload-tab"].each do |tab| %>
|
||||||
|
<%= tag.div id: tab, data: { tabs_target: "tab" }, class: tab == "csv-upload-tab" ? "hidden" : "" do %>
|
||||||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
||||||
<%= form.select :col_sep, [["Comma (,)", ","], ["Semicolon (;)", ";"]], label: true %>
|
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||||||
|
|
||||||
|
<% unless @import.type == "MintImport" %>
|
||||||
|
<%= form.select :account_id, @import.family.accounts.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if tab == "csv-paste-tab" %>
|
||||||
<%= form.text_area :raw_file_str,
|
<%= form.text_area :raw_file_str,
|
||||||
rows: 10,
|
rows: 10,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: "Paste your CSV file contents here",
|
placeholder: "Paste your CSV file contents here",
|
||||||
"data-auto-submit-form-target": "auto" %>
|
"data-auto-submit-form-target": "auto" %>
|
||||||
|
<% else %>
|
||||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-tabs-target="tab" id="csv-upload-tab" class="hidden">
|
|
||||||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
|
||||||
<%= form.select :col_sep, [["Comma (,)", ","], ["Semicolon (;)", ";"]], label: true %>
|
|
||||||
|
|
||||||
<label for="import_csv_file" class="flex flex-col items-center justify-center w-full h-56 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50">
|
<label for="import_csv_file" class="flex flex-col items-center justify-center w-full h-56 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50">
|
||||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
<%= form.file_field :csv_file, class: "ml-32", "data-auto-submit-form-target": "auto" %>
|
<%= form.file_field :csv_file, class: "ml-32", "data-auto-submit-form-target": "auto" %>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<%= link_to import_path(import), class: "text-sm text-primary hover:underline" do %>
|
<%= link_to import_path(import), class: "text-sm text-primary hover:underline" do %>
|
||||||
|
<% if import.account.present? %>
|
||||||
|
<%= import.account.name + " " %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= t(".label", type: import.type.titleize, datetime: import.updated_at.strftime("%b %-d, %Y at %l:%M %p")) %>
|
<%= t(".label", type: import.type.titleize, datetime: import.updated_at.strftime("%b %-d, %Y at %l:%M %p")) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{ name: "Clean", path: import_clean_path(import), is_complete: import.cleaned?, step_number: 3 },
|
{ name: "Clean", path: import_clean_path(import), is_complete: import.cleaned?, step_number: 3 },
|
||||||
{ name: "Map", path: import_confirm_path(import), is_complete: import.publishable?, step_number: 4 },
|
{ name: "Map", path: import_confirm_path(import), is_complete: import.publishable?, step_number: 4 },
|
||||||
{ name: "Confirm", path: import_path(import), is_complete: import.complete?, step_number: 5 }
|
{ name: "Confirm", path: import_path(import), is_complete: import.complete?, step_number: 5 }
|
||||||
] %>
|
].reject { |step| step[:name] == "Map" && import.mapping_steps.empty? } %>
|
||||||
|
|
||||||
<ul class="flex items-center gap-2">
|
<ul class="flex items-center gap-2">
|
||||||
<% steps.each_with_index do |step, idx| %>
|
<% steps.each_with_index do |step, idx| %>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddOptionalAccountForImport < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
rename_column :imports, :original_account_id, :account_id
|
||||||
|
end
|
||||||
|
end
|
4
db/schema.rb
generated
4
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2025_02_20_200735) do
|
ActiveRecord::Schema[7.2].define(version: 2025_03_03_141007) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -398,7 +398,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_20_200735) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "col_sep", default: ","
|
t.string "col_sep", default: ","
|
||||||
t.uuid "family_id", null: false
|
t.uuid "family_id", null: false
|
||||||
t.uuid "original_account_id"
|
t.uuid "account_id"
|
||||||
t.string "type", null: false
|
t.string "type", null: false
|
||||||
t.string "date_col_label", default: "date"
|
t.string "date_col_label", default: "date"
|
||||||
t.string "amount_col_label", default: "amount"
|
t.string "amount_col_label", default: "amount"
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Import::RowsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
get import_row_path(import, row)
|
get import_row_path(import, row)
|
||||||
|
|
||||||
assert_row_fields(row, [ :date, :ticker, :qty, :price, :currency, :account, :name ])
|
assert_row_fields(row, [ :date, :ticker, :qty, :price, :currency, :account, :name, :account ])
|
||||||
|
|
||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue