mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-28 17:49:38 +02:00
Allow inline account creation when importing CSV (#1291)
* Allow inline account creation when importing CSV * Sanitize numeric inputs for CSV * CSV import date validation * Lint fix
This commit is contained in:
parent
1746533842
commit
cd9f20747c
6 changed files with 44 additions and 10 deletions
|
@ -71,10 +71,10 @@ class Import < ApplicationRecord
|
||||||
{
|
{
|
||||||
account: row[account_col_label].to_s,
|
account: row[account_col_label].to_s,
|
||||||
date: row[date_col_label].to_s,
|
date: row[date_col_label].to_s,
|
||||||
qty: row[qty_col_label].to_s,
|
qty: sanitize_number(row[qty_col_label]).to_s,
|
||||||
ticker: row[ticker_col_label].to_s,
|
ticker: row[ticker_col_label].to_s,
|
||||||
price: row[price_col_label].to_s,
|
price: sanitize_number(row[price_col_label]).to_s,
|
||||||
amount: row[amount_col_label].to_s,
|
amount: sanitize_number(row[amount_col_label]).to_s,
|
||||||
currency: (row[currency_col_label] || default_currency).to_s,
|
currency: (row[currency_col_label] || default_currency).to_s,
|
||||||
name: (row[name_col_label] || default_row_name).to_s,
|
name: (row[name_col_label] || default_row_name).to_s,
|
||||||
category: row[category_col_label].to_s,
|
category: row[category_col_label].to_s,
|
||||||
|
@ -113,6 +113,10 @@ class Import < ApplicationRecord
|
||||||
cleaned? && mappings.all?(&:valid?)
|
cleaned? && mappings.all?(&:valid?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requires_account?
|
||||||
|
family.accounts.empty? && mappings.accounts.where(key: "").any?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def import!
|
def import!
|
||||||
# no-op, subclasses can implement for customization of algorithm
|
# no-op, subclasses can implement for customization of algorithm
|
||||||
|
@ -134,4 +138,9 @@ class Import < ApplicationRecord
|
||||||
converters: [ ->(str) { str&.strip } ]
|
converters: [ ->(str) { str&.strip } ]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sanitize_number(value)
|
||||||
|
return "" if value.nil?
|
||||||
|
value.gsub(/[^\d.]/, "")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Import::Row < ApplicationRecord
|
||||||
validates :amount, numericality: true, allow_blank: true
|
validates :amount, numericality: true, allow_blank: true
|
||||||
validates :currency, presence: true
|
validates :currency, presence: true
|
||||||
|
|
||||||
validate :date_matches_user_format
|
validate :date_valid
|
||||||
validate :required_columns
|
validate :required_columns
|
||||||
validate :currency_is_valid
|
validate :currency_is_valid
|
||||||
|
|
||||||
|
@ -54,13 +54,21 @@ class Import::Row < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def date_matches_user_format
|
def date_valid
|
||||||
return if date.blank?
|
return if date.blank?
|
||||||
|
|
||||||
parsed_date = Date.strptime(date, import.date_format) rescue nil
|
parsed_date = Date.strptime(date, import.date_format) rescue nil
|
||||||
|
|
||||||
if parsed_date.nil?
|
if parsed_date.nil?
|
||||||
errors.add(:date, "must exactly match the format: #{import.date_format}")
|
errors.add(:date, "must exactly match the format: #{import.date_format}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
min_date = Account::Entry.min_supported_date
|
||||||
|
max_date = Date.current
|
||||||
|
|
||||||
|
if parsed_date < min_date || parsed_date > max_date
|
||||||
|
errors.add(:date, "must be between #{min_date} and #{max_date}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<div class="bg-white border border-alpha-black-100 rounded-lg p-3 flex items-center justify-between">
|
<div class="bg-white border border-alpha-black-100 rounded-lg p-3 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %>
|
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %>
|
||||||
<p class="text-red-500">You have errors in your data</p>
|
<p class="text-red-500 text-sm"><%= t(".errors_notice") %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
|
|
|
@ -3,12 +3,20 @@
|
||||||
<% 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 import.requires_account? && mapping_class == Import::AccountMapping %>
|
||||||
|
<div class="flex items-center justify-between p-4 mb-4 gap-4 text-gray-500 bg-red-100 border border-red-200 rounded-lg w-[650px]">
|
||||||
|
<%= tag.p t(".no_accounts"), class: "text-sm" %>
|
||||||
|
|
||||||
|
<%= link_to t(".create_account"), new_account_path, class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="bg-gray-25 rounded-xl p-1 space-y-1 w-[650px]">
|
<div class="bg-gray-25 rounded-xl p-1 space-y-1 w-[650px]">
|
||||||
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-gray-500 uppercase px-5 py-3">
|
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-gray-500 uppercase px-5 py-3">
|
||||||
<p>CSV <%= mapping_label(mapping_class) %></p>
|
<p><%= t(".csv_mapping_label", mapping: mapping_label(mapping_class)) %></p>
|
||||||
<p>Maybe <%= mapping_label(mapping_class) %></p>
|
<p><%= t(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %></p>
|
||||||
<p class="justify-self-end">Rows</p>
|
<p class="justify-self-end"><%= t(".rows_label") %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border border-alpha-black-25 rounded-md shadow-xs divide-y divide-alpha-black-100 text-sm">
|
<div class="border border-alpha-black-25 rounded-md shadow-xs divide-y divide-alpha-black-100 text-sm">
|
||||||
|
|
|
@ -28,6 +28,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-screen-md mx-auto flex justify-center">
|
<div class="max-w-screen-md mx-auto flex flex-col items-center">
|
||||||
<%= render partial: "import/confirms/mappings", locals: { import: @import, mapping_class: step_mapping_class, step_idx: step_idx } %>
|
<%= render partial: "import/confirms/mappings", locals: { import: @import, mapping_class: step_mapping_class, step_idx: step_idx } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,12 +4,21 @@ en:
|
||||||
cleans:
|
cleans:
|
||||||
show:
|
show:
|
||||||
description: Edit your data in the table below. Red cells are invalid.
|
description: Edit your data in the table below. Red cells are invalid.
|
||||||
|
errors_notice: You have errors in your data. Hover over the error to see details.
|
||||||
title: Clean your data
|
title: Clean your data
|
||||||
configurations:
|
configurations:
|
||||||
show:
|
show:
|
||||||
description: Select the columns that correspond to each field in your CSV.
|
description: Select the columns that correspond to each field in your CSV.
|
||||||
title: Configure your import
|
title: Configure your import
|
||||||
confirms:
|
confirms:
|
||||||
|
mappings:
|
||||||
|
create_account: Create account
|
||||||
|
csv_mapping_label: "%{mapping} in CSV"
|
||||||
|
maybe_mapping_label: "%{mapping} in Maybe"
|
||||||
|
no_accounts: You don't have any accounts yet. Please create an account that
|
||||||
|
we can use for (unassigned) rows in your CSV or go back to the Clean step
|
||||||
|
and provide an account name we can use.
|
||||||
|
rows_label: Rows
|
||||||
show:
|
show:
|
||||||
account_mapping_description: Assign all of your imported file's accounts to
|
account_mapping_description: Assign all of your imported file's accounts to
|
||||||
Maybe's existing accounts. You can also add new accounts or leave them
|
Maybe's existing accounts. You can also add new accounts or leave them
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue