mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +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
|
@ -106,7 +106,7 @@ class Family < ApplicationRecord
|
|||
# If family has any entries in different currencies, they need a provider for historical exchange rates
|
||||
uniq_currencies = entries.pluck(:currency).uniq
|
||||
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
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class Import < ApplicationRecord
|
||||
TYPES = %w[TransactionImport TradeImport AccountImport MintImport].freeze
|
||||
SIGNAGE_CONVENTIONS = %w[inflows_positive inflows_negative]
|
||||
SEPARATORS = [ [ "Comma (,)", "," ], [ "Semicolon (;)", ";" ] ].freeze
|
||||
|
||||
NUMBER_FORMATS = {
|
||||
"1,234.56" => { separator: ".", delimiter: "," }, # US/UK/Asia
|
||||
|
@ -10,6 +11,7 @@ class Import < ApplicationRecord
|
|||
}.freeze
|
||||
|
||||
belongs_to :family
|
||||
belongs_to :account, optional: true
|
||||
|
||||
before_validation :set_default_number_format
|
||||
|
||||
|
@ -25,7 +27,7 @@ class Import < ApplicationRecord
|
|||
}, validate: true, default: "pending"
|
||||
|
||||
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 :number_format, presence: true, inclusion: { in: NUMBER_FORMATS.keys }
|
||||
|
||||
|
@ -98,12 +100,17 @@ class Import < ApplicationRecord
|
|||
end
|
||||
|
||||
def dry_run
|
||||
{
|
||||
mappings = {
|
||||
transactions: rows.count,
|
||||
accounts: Import::AccountMapping.for_import(self).creational.count,
|
||||
categories: Import::CategoryMapping.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
|
||||
|
||||
def required_column_keys
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Import::AccountMapping < Import::Mapping
|
||||
validates :mappable, presence: true, if: -> { key.blank? || !create_when_empty }
|
||||
validates :mappable, presence: true, if: :requires_mapping?
|
||||
|
||||
class << self
|
||||
def mapping_values(import)
|
||||
|
@ -42,4 +42,9 @@ class Import::AccountMapping < Import::Mapping
|
|||
self.mappable = account
|
||||
save!
|
||||
end
|
||||
|
||||
private
|
||||
def requires_mapping?
|
||||
(key.blank? || !create_when_empty) && import.account.nil?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,11 @@ class TradeImport < Import
|
|||
mappings.each(&:create_mappable!)
|
||||
|
||||
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
|
||||
security = find_or_create_security(
|
||||
|
@ -12,15 +16,15 @@ class TradeImport < Import
|
|||
exchange_operating_mic: row.exchange_operating_mic
|
||||
)
|
||||
|
||||
entry = account.entries.build \
|
||||
entry = mapped_account.entries.build \
|
||||
date: row.date_iso,
|
||||
amount: row.signed_amount,
|
||||
name: row.name,
|
||||
currency: row.currency.presence || account.currency,
|
||||
currency: row.currency.presence || mapped_account.currency,
|
||||
entryable: Account::Trade.new(
|
||||
security: security,
|
||||
qty: row.qty,
|
||||
currency: row.currency.presence || account.currency,
|
||||
currency: row.currency.presence || mapped_account.currency,
|
||||
price: row.price
|
||||
),
|
||||
import: self
|
||||
|
@ -31,7 +35,9 @@ class TradeImport < Import
|
|||
end
|
||||
|
||||
def mapping_steps
|
||||
[ Import::AccountMapping ]
|
||||
base = []
|
||||
base << Import::AccountMapping if account.nil?
|
||||
base
|
||||
end
|
||||
|
||||
def required_column_keys
|
||||
|
@ -39,14 +45,19 @@ class TradeImport < Import
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def dry_run
|
||||
{
|
||||
transactions: rows.count,
|
||||
mappings = { transactions: rows.count }
|
||||
|
||||
mappings.merge(
|
||||
accounts: Import::AccountMapping.for_import(self).creational.count
|
||||
}
|
||||
) if account.nil?
|
||||
|
||||
mappings
|
||||
end
|
||||
|
||||
def csv_template
|
||||
|
@ -57,7 +68,9 @@ class TradeImport < Import
|
|||
05/17/2024,TSLA,XNAS,USD,2,700.50,Retirement Account,Tesla Inc. Purchase
|
||||
CSV
|
||||
|
||||
CSV.parse(template, headers: true)
|
||||
csv = CSV.parse(template, headers: true)
|
||||
csv.delete("account") if account.present?
|
||||
csv
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -4,11 +4,16 @@ class TransactionImport < Import
|
|||
mappings.each(&:create_mappable!)
|
||||
|
||||
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)
|
||||
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,
|
||||
amount: row.signed_amount,
|
||||
name: row.name,
|
||||
|
@ -27,11 +32,15 @@ class TransactionImport < Import
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def mapping_steps
|
||||
[ Import::CategoryMapping, Import::TagMapping, Import::AccountMapping ]
|
||||
base = [ Import::CategoryMapping, Import::TagMapping ]
|
||||
base << Import::AccountMapping if account.nil?
|
||||
base
|
||||
end
|
||||
|
||||
def csv_template
|
||||
|
@ -42,6 +51,8 @@ class TransactionImport < Import
|
|||
05/17/2024,-12.50,Coffee Shop,,,coffee,,
|
||||
CSV
|
||||
|
||||
CSV.parse(template, headers: true)
|
||||
csv = CSV.parse(template, headers: true)
|
||||
csv.delete("account") if account.present?
|
||||
csv
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue