2024-05-17 09:09:32 -04:00
|
|
|
class Import < ApplicationRecord
|
2024-10-01 10:47:59 -04:00
|
|
|
TYPES = %w[TransactionImport TradeImport AccountImport MintImport].freeze
|
|
|
|
SIGNAGE_CONVENTIONS = %w[inflows_positive inflows_negative]
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
belongs_to :family
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
scope :ordered, -> { order(created_at: :desc) }
|
2024-05-17 09:09:32 -04:00
|
|
|
|
|
|
|
enum :status, { pending: "pending", complete: "complete", importing: "importing", failed: "failed" }, validate: true
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
validates :type, inclusion: { in: TYPES }
|
|
|
|
validates :col_sep, inclusion: { in: [ ",", ";" ] }
|
|
|
|
validates :signage_convention, inclusion: { in: SIGNAGE_CONVENTIONS }
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
has_many :rows, dependent: :destroy
|
|
|
|
has_many :mappings, dependent: :destroy
|
|
|
|
has_many :accounts, dependent: :destroy
|
|
|
|
has_many :entries, dependent: :destroy, class_name: "Account::Entry"
|
2024-05-22 14:12:56 +02:00
|
|
|
|
2024-05-17 09:09:32 -04:00
|
|
|
def publish_later
|
2024-10-01 10:47:59 -04:00
|
|
|
raise "Import is not publishable" unless publishable?
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
update! status: :importing
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
ImportJob.perform_later(self)
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def publish
|
|
|
|
import!
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
family.sync
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
update! status: :complete
|
|
|
|
rescue => error
|
|
|
|
update! status: :failed, error: error.message
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def csv_rows
|
|
|
|
@csv_rows ||= parsed_csv
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def csv_headers
|
|
|
|
parsed_csv.headers
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def csv_sample
|
|
|
|
@csv_sample ||= parsed_csv.first(2)
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def dry_run
|
2024-10-01 10:47:59 -04:00
|
|
|
{
|
|
|
|
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
|
|
|
|
}
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def required_column_keys
|
|
|
|
[]
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def column_keys
|
|
|
|
raise NotImplementedError, "Subclass must implement column_keys"
|
|
|
|
end
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def generate_rows_from_csv
|
|
|
|
rows.destroy_all
|
|
|
|
|
|
|
|
mapped_rows = csv_rows.map do |row|
|
|
|
|
{
|
|
|
|
account: row[account_col_label].to_s,
|
|
|
|
date: row[date_col_label].to_s,
|
2024-10-10 15:14:38 -04:00
|
|
|
qty: sanitize_number(row[qty_col_label]).to_s,
|
2024-10-01 10:47:59 -04:00
|
|
|
ticker: row[ticker_col_label].to_s,
|
2024-10-10 15:14:38 -04:00
|
|
|
price: sanitize_number(row[price_col_label]).to_s,
|
|
|
|
amount: sanitize_number(row[amount_col_label]).to_s,
|
2024-10-01 10:47:59 -04:00
|
|
|
currency: (row[currency_col_label] || default_currency).to_s,
|
|
|
|
name: (row[name_col_label] || default_row_name).to_s,
|
|
|
|
category: row[category_col_label].to_s,
|
|
|
|
tags: row[tags_col_label].to_s,
|
|
|
|
entity_type: row[entity_type_col_label].to_s,
|
|
|
|
notes: row[notes_col_label].to_s
|
|
|
|
}
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
rows.insert_all!(mapped_rows)
|
|
|
|
end
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def sync_mappings
|
|
|
|
mapping_steps.each do |mapping|
|
|
|
|
mapping.sync(self)
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
2024-10-01 10:47:59 -04:00
|
|
|
end
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def mapping_steps
|
|
|
|
[]
|
|
|
|
end
|
2024-05-22 10:02:03 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def uploaded?
|
|
|
|
raw_file_str.present?
|
|
|
|
end
|
2024-05-23 08:09:33 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def configured?
|
|
|
|
uploaded? && rows.any?
|
|
|
|
end
|
2024-05-22 10:02:03 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def cleaned?
|
|
|
|
configured? && rows.all?(&:valid?)
|
|
|
|
end
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def publishable?
|
|
|
|
cleaned? && mappings.all?(&:valid?)
|
|
|
|
end
|
2024-05-17 09:09:32 -04:00
|
|
|
|
2024-10-10 15:51:36 -04:00
|
|
|
def has_unassigned_account?
|
|
|
|
mappings.accounts.where(key: "").any?
|
|
|
|
end
|
|
|
|
|
2024-10-10 15:14:38 -04:00
|
|
|
def requires_account?
|
2024-10-10 15:51:36 -04:00
|
|
|
family.accounts.empty? && has_unassigned_account?
|
2024-10-10 15:14:38 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
private
|
|
|
|
def import!
|
|
|
|
# no-op, subclasses can implement for customization of algorithm
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def default_row_name
|
|
|
|
"Imported item"
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def default_currency
|
|
|
|
family.currency
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
def parsed_csv
|
|
|
|
@parsed_csv ||= CSV.parse(
|
|
|
|
(raw_file_str || "").strip,
|
|
|
|
headers: true,
|
|
|
|
col_sep: col_sep,
|
|
|
|
converters: [ ->(str) { str&.strip } ]
|
|
|
|
)
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|
2024-10-10 15:14:38 -04:00
|
|
|
|
|
|
|
def sanitize_number(value)
|
|
|
|
return "" if value.nil?
|
2024-10-10 18:57:00 -04:00
|
|
|
value.gsub(/[^\d.\-]/, "")
|
2024-10-10 15:14:38 -04:00
|
|
|
end
|
2024-05-17 09:09:32 -04:00
|
|
|
end
|