feat: Support optional balance date column in account CSV imports (#736)

* Initial plan

* Add ability to specify balance date in AccountImport CSV

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Restore original Ruby version

* Fix linting issues - remove trailing whitespace

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Add error handling for date parsing in AccountImport

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Revert unintended Gemfile.lock changes

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>
This commit is contained in:
Copilot
2026-01-22 15:25:43 +01:00
committed by GitHub
parent 051197137c
commit 558bf7eeda
3 changed files with 115 additions and 8 deletions

View File

@@ -18,7 +18,19 @@ class AccountImport < Import
account.save!
manager = Account::OpeningBalanceManager.new(account)
result = manager.set_opening_balance(balance: row.amount.to_d)
# Parse date if provided, otherwise use default
balance_date = if row.date.present?
begin
Date.strptime(row.date, date_format)
rescue ArgumentError => e
raise OpeningBalanceError, "Invalid date format for '#{row.date}': #{e.message}"
end
else
nil
end
result = manager.set_opening_balance(balance: row.amount.to_d, date: balance_date)
# Re-raise since we should never have an error here
if result.error
@@ -37,7 +49,7 @@ class AccountImport < Import
end
def column_keys
%i[entity_type name amount currency]
%i[entity_type name amount currency date]
end
def dry_run
@@ -48,10 +60,10 @@ class AccountImport < Import
def csv_template
template = <<-CSV
Account type*,Name*,Balance*,Currency
Checking,Main Checking Account,1000.00,USD
Savings,Emergency Fund,5000.00,USD
Credit Card,Rewards Card,-500.00,USD
Account type*,Name*,Balance*,Currency,Balance Date
Checking,Main Checking Account,1000.00,USD,01/01/2024
Savings,Emergency Fund,5000.00,USD,01/15/2024
Credit Card,Rewards Card,-500.00,USD,02/01/2024
CSV
CSV.parse(template, headers: true)

View File

@@ -6,5 +6,13 @@
<%= form.select :amount_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Balance" }, required: true %>
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Default", label: "Currency" } %>
<div class="flex items-center gap-4">
<%= form.select :date_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Balance Date" } %>
<%= form.select :date_format,
Family::DATE_FORMATS,
{ label: "Date Format", prompt: "Select format" },
required: @import.date_col_label.present? %>
</div>
<%= form.submit "Apply configuration", disabled: import.complete? %>
<% end %>

View File

@@ -63,8 +63,95 @@ class AccountImportTest < ActiveSupport::TestCase
end
end
test "column_keys returns expected keys" do
assert_equal %i[entity_type name amount currency], @import.column_keys
test "import creates accounts with explicit balance dates" do
import_csv = <<~CSV
type,name,amount,currency,date
depository,Main Checking,1000.00,USD,01/15/2024
depository,Savings Account,5000.00,USD,02/01/2024
CSV
@import.update!(
raw_file_str: import_csv,
entity_type_col_label: "type",
name_col_label: "name",
amount_col_label: "amount",
currency_col_label: "currency",
date_col_label: "date",
date_format: "%m/%d/%Y"
)
@import.generate_rows_from_csv
# Create mappings for account types
@import.mappings.create! key: "depository", value: "Depository", type: "Import::AccountTypeMapping"
@import.reload
# Perform the import
@import.publish
# Check if import succeeded
if @import.failed?
fail "Import failed with error: #{@import.error}"
end
assert_equal "complete", @import.status
# Verify accounts were created with correct dates
accounts = @import.accounts.order(:name)
checking_account = accounts.find { |a| a.name == "Main Checking" }
savings_account = accounts.find { |a| a.name == "Savings Account" }
checking_valuation = checking_account.valuations.opening_anchor.first
savings_valuation = savings_account.valuations.opening_anchor.first
assert_equal Date.parse("2024-01-15"), checking_valuation.entry.date
assert_equal Date.parse("2024-02-01"), savings_valuation.entry.date
end
test "import creates accounts with default dates when date column not provided" do
import_csv = <<~CSV
type,name,amount,currency
depository,Main Checking,1000.00,USD
CSV
@import.update!(
raw_file_str: import_csv,
entity_type_col_label: "type",
name_col_label: "name",
amount_col_label: "amount",
currency_col_label: "currency"
)
@import.generate_rows_from_csv
# Create mappings for account types
@import.mappings.create! key: "depository", value: "Depository", type: "Import::AccountTypeMapping"
@import.reload
# Perform the import
@import.publish
# Check if import succeeded
if @import.failed?
fail "Import failed with error: #{@import.error}"
end
assert_equal "complete", @import.status
# Verify account was created with default date (2 years ago or 1 day before oldest entry)
account = @import.accounts.first
valuation = account.valuations.opening_anchor.first
# Default date should be 2 years ago when there are no other entries
expected_default_date = 2.years.ago.to_date
assert_equal expected_default_date, valuation.entry.date
end
test "column_keys returns expected keys including date" do
assert_equal %i[entity_type name amount currency date], @import.column_keys
end
test "required_column_keys returns expected keys" do