Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add start balance to manual accounts #735

Merged
7 changes: 5 additions & 2 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ def update
end

def create
@account = Current.family.accounts.build(account_params.except(:accountable_type))
@account = Current.family.accounts.build(account_params.except(:accountable_type, :start_date))
@account.accountable = Accountable.from_type(account_params[:accountable_type])&.new

if @account.save
@valuation = @account.valuations.new(date: account_params[:start_date] || Date.today, value: @account.balance, currency: @account.currency)
@valuation.save!

redirect_to accounts_path, notice: t(".success")
else
render "new", status: :unprocessable_entity
Expand Down Expand Up @@ -86,6 +89,6 @@ def set_account
end

def account_params
params.require(:account).permit(:name, :accountable_type, :balance, :currency, :subtype, :is_active)
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :currency, :subtype, :is_active)
end
end
10 changes: 3 additions & 7 deletions app/models/account/balance/calculator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def initialize(account, options = {})
def calculate
prior_balance = implied_start_balance

calculated_balances = ((@calc_start_date + 1.day)...Date.current).map do |date|
calculated_balances = ((@calc_start_date + 1.day)..Date.current).map do |date|
valuation = normalized_valuations.find { |v| v["date"] == date }

if valuation
Expand All @@ -30,8 +30,7 @@ def calculate

@daily_balances = [
{ date: @calc_start_date, balance: implied_start_balance, currency: @account.currency, updated_at: Time.current },
*calculated_balances,
{ date: Date.current, balance: @account.balance, currency: @account.currency, updated_at: Time.current } # Last balance must always match "source of truth"
*calculated_balances
]

if @account.foreign_currency?
Expand Down Expand Up @@ -66,10 +65,7 @@ def normalize_entries_to_account_currency(entries, value_key)
value = entry.send(value_key)

if currency != @account.currency
rate = ExchangeRate.find_by(base_currency: currency, converted_currency: @account.currency, date: date)
raise "Rate for #{currency} to #{@account.currency} not found" unless rate

value *= rate.rate
value = ExchangeRate.convert(value:, from: currency, to: @account.currency, date:)
currency = @account.currency
end

Expand Down
4 changes: 4 additions & 0 deletions app/models/account/syncable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def sync(start_date = nil)
calculator.calculate
self.balances.upsert_all(calculator.daily_balances, unique_by: :index_account_balances_on_account_id_date_currency_unique)
self.balances.where("date < ?", effective_start_date).delete_all
new_balance = calculator.daily_balances.select { |b| b[:currency] == self.currency }.last[:balance]
self.balance = new_balance
self.save!

update!(status: "ok", last_sync_date: Date.today)
rescue => e
update!(status: "error")
Expand Down
7 changes: 7 additions & 0 deletions app/models/exchange_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,12 @@ def find_rate_or_fetch(from:, to:, date:)
def get_rate_series(from, to, date_range)
where(base_currency: from, converted_currency: to, date: date_range).order(:date)
end

def convert(value:, from:, to:, date:)
rate = ExchangeRate.find_by(base_currency: from, converted_currency: to, date:)
raise "Conversion from: #{from} to: #{to} on: #{date} not found" unless rate

value * rate.rate
end
end
end
5 changes: 3 additions & 2 deletions app/views/accounts/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@
<%= form_with model: @account, url: accounts_path, scope: :account, html: { class: "m-5 mt-1 flex flex-col justify-between grow", data: { turbo: false } } do |f| %>
<div class="space-y-4 grow">
<%= f.hidden_field :accountable_type %>
<%= f.text_field :name, placeholder: t("accounts.new.name.placeholder"), required: "required", label: t("accounts.new.name.label"), autofocus: true %>
<%= f.text_field :name, placeholder: t(".name.placeholder"), required: "required", label: t(".name.label"), autofocus: true %>
<%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
<%= f.money_field :balance_money, label: "Balance", required: "required" %>
<%= f.money_field :balance_money, label: t(".balance.label"), required: "required" %>
<%= f.date_field :date, label: t(".start_date.label"), required: true, max: Date.today, value: Date.today %>
</div>
<%= f.submit "Add #{@account.accountable.model_name.human.downcase}" %>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/transactions/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true %>
<%= f.money_field :amount_money, label: t(".amount"), required: true %>
<%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") }, required: true %>
<%= f.date_field :date, label: t(".date"), required: true %>
<%= f.date_field :date, label: t(".date"), required: true, max: Date.today %>
</section>

<section>
Expand Down
2 changes: 1 addition & 1 deletion app/views/transactions/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
</div>
</summary>
<%= f.date_field :date, label: "Date", "data-auto-submit-form-target": "auto" %>
<%= f.date_field :date, label: "Date", max: Date.today, "data-auto-submit-form-target": "auto" %>
<div class="h-2"></div>
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-400" }, {class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled"} %>
</details>
Expand Down
2 changes: 1 addition & 1 deletion app/views/valuations/_form_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</div>
</div>
<div class="flex items-center justify-between grow">
<%= f.date_field :date, required: "required", class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %>
<%= f.date_field :date, required: "required", max: Date.today, class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %>
<%= f.number_field :value, required: "required", placeholder: "0.00", step: "0.01", class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs text-gray-900 text-sm px-3 py-1.5 text-right" %>
</div>
<div class="w-[296px] flex gap-2 justify-end items-center">
Expand Down
4 changes: 4 additions & 0 deletions config/locales/views/account/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ en:
index:
new_account: New account
new:
balance:
label: Balance
currency:
all_others: All Others
popular: Popular
name:
label: Account name
placeholder: Example account name
select_accountable_type: What would you like to add?
start_date:
label: Start date
title: Add an account
show:
confirm_accept: Delete "%{name}"
Expand Down
12 changes: 11 additions & 1 deletion test/controllers/accounts_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
assert_response :ok
end

test "create" do
test "should create account" do
assert_difference -> { Account.count }, +1 do
post accounts_path, params: { account: { accountable_type: "Account::Credit" } }
assert_redirected_to accounts_url
end
end

test "should create a valuation together with account" do
balance = 700
start_date = 3.days.ago.to_date
post accounts_path, params: { account: { accountable_type: "Account::Credit", balance:, start_date: } }

new_valuation = Valuation.order(:created_at).last
assert new_valuation.value == balance
assert new_valuation.date == start_date
end
end
2 changes: 1 addition & 1 deletion test/fixtures/account/expected_balances.csv
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ date_offset,collectable,checking,savings_with_valuation_overrides,credit_card,eu
-3,550,5000,20500,1000,12000,13046.4,10000
-2,550,5000,20500,1000,12000,12982.8,10000
-1,550,5000,20500,1000,12000,13014,10000
0,550,5000,20000,1000,12000,13000.8,10000
0,550,5000,20500,1000,12000,13000.8,10000
2 changes: 1 addition & 1 deletion test/fixtures/family/expected_snapshots.csv
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ date_offset,net_worth,assets,liabilities,depositories,investments,loans,credits,
-3,48096.40,49096.40,1000.00,48546.40,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
-2,48032.80,49032.80,1000.00,48482.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
-1,48064.00,49064.00,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
0,47550.80,48550.80,1000.00,48000.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
0,48050.80,49050.80,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
8 changes: 8 additions & 0 deletions test/models/account/syncable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,12 @@ class Account::SyncableTest < ActiveSupport::TestCase

assert_equal 31, account.balances.count
end

test "account balance is updated after sync" do
account = accounts(:savings_with_valuation_overrides)

assert_changes -> { account.balance }, to: 20500 do
account.sync
end
end
end