diff --git a/app/controllers/account/cashes_controller.rb b/app/controllers/account/cashes_controller.rb new file mode 100644 index 00000000..6afa3241 --- /dev/null +++ b/app/controllers/account/cashes_controller.rb @@ -0,0 +1,14 @@ +class Account::CashesController < ApplicationController + layout :with_sidebar + + before_action :set_account + + def index + end + + private + + def set_account + @account = Current.family.accounts.find(params[:account_id]) + end +end diff --git a/app/controllers/account/entries_controller.rb b/app/controllers/account/entries_controller.rb index 70c04e67..70de7c62 100644 --- a/app/controllers/account/entries_controller.rb +++ b/app/controllers/account/entries_controller.rb @@ -13,7 +13,7 @@ class Account::EntriesController < ApplicationController end def trades - @trades = @account.entries.account_trades.reverse_chronological + @trades = @account.entries.where(entryable_type: [ "Account::Transaction", "Account::Trade" ]).reverse_chronological end def new diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index a02e1c91..30a0e0d7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -31,7 +31,8 @@ class AccountsController < ApplicationController end def show - @balance_series = @account.series(period: @period) + @series = @account.series(period: @period) + @trend = @series.trend end def edit diff --git a/app/helpers/account/cashes_helper.rb b/app/helpers/account/cashes_helper.rb new file mode 100644 index 00000000..ed8c2dfc --- /dev/null +++ b/app/helpers/account/cashes_helper.rb @@ -0,0 +1,13 @@ +module Account::CashesHelper + def brokerage_cash(account) + currency = Money::Currency.new(account.currency) + + account.holdings.build \ + date: Date.current, + qty: account.balance, + price: 1, + amount: account.balance, + currency: account.currency, + security: Security.new(ticker: currency.iso_code, name: currency.name) + end +end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 6817d7fb..6994578f 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -25,11 +25,12 @@ module AccountsHelper def account_tabs(account) holdings_tab = { key: "holdings", label: t("accounts.show.holdings"), path: account_path(account, tab: "holdings"), content_path: account_holdings_path(account) } + cash_tab = { key: "cash", label: t("accounts.show.cash"), path: account_path(account, tab: "cash"), content_path: account_cashes_path(account) } value_tab = { key: "valuations", label: t("accounts.show.value"), path: account_path(account, tab: "valuations"), content_path: valuation_account_entries_path(account) } transactions_tab = { key: "transactions", label: t("accounts.show.transactions"), path: account_path(account, tab: "transactions"), content_path: transaction_account_entries_path(account) } trades_tab = { key: "trades", label: t("accounts.show.trades"), path: account_path(account, tab: "trades"), content_path: trade_account_entries_path(account) } - return [ holdings_tab, trades_tab ] if account.investment? + return [ holdings_tab, cash_tab, trades_tab ] if account.investment? [ value_tab, transactions_tab ] end diff --git a/app/models/account.rb b/app/models/account.rb index 2ce6495f..e71390d6 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -28,8 +28,10 @@ class Account < ApplicationRecord delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy + delegate :value, :series, to: :accountable + class << self - def by_group(period: Period.all, currency: Money.default_currency) + def by_group(period: Period.all, currency: Money.default_currency.iso_code) grouped_accounts = { assets: ValueGroup.new("Assets", currency), liabilities: ValueGroup.new("Liabilities", currency) } Accountable.by_classification.each do |classification, types| @@ -82,18 +84,6 @@ class Account < ApplicationRecord classification == "asset" ? "up" : "down" end - def series(period: Period.all, currency: self.currency) - balance_series = balances.in_period(period).where(currency: Money::Currency.new(currency).iso_code) - - if balance_series.empty? && period.date_range.end == Date.current - TimeSeries.new([ { date: Date.current, value: balance_money.exchange_to(currency) } ]) - else - TimeSeries.from_collection(balance_series, :balance_money) - end - rescue Money::ConversionError - TimeSeries.new([]) - end - def update_balance!(balance) valuation = entries.account_valuations.find_by(date: Date.current) diff --git a/app/models/account/holding.rb b/app/models/account/holding.rb index 512caddb..73dd8c02 100644 --- a/app/models/account/holding.rb +++ b/app/models/account/holding.rb @@ -10,6 +10,8 @@ class Account::Holding < ApplicationRecord scope :chronological, -> { order(:date) } scope :current, -> { where(date: Date.current).order(amount: :desc) } + scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) } + scope :known_value, -> { where.not(amount: nil) } scope :for, ->(security) { where(security_id: security).order(:date) } delegate :name, to: :security @@ -18,7 +20,7 @@ class Account::Holding < ApplicationRecord def weight return nil unless amount - portfolio_value = account.holdings.current.where.not(amount: nil).sum(&:amount) + portfolio_value = account.holdings.current.known_value.sum(&:amount) portfolio_value.zero? ? 1 : amount / portfolio_value * 100 end diff --git a/app/models/concerns/accountable.rb b/app/models/concerns/accountable.rb index 916e6cfd..9579929e 100644 --- a/app/models/concerns/accountable.rb +++ b/app/models/concerns/accountable.rb @@ -17,4 +17,20 @@ module Accountable included do has_one :account, as: :accountable, touch: true end + + def value + account.balance_money + end + + def series(period: Period.all, currency: account.currency) + balance_series = account.balances.in_period(period).where(currency: currency) + + if balance_series.empty? && period.date_range.end == Date.current + TimeSeries.new([ { date: Date.current, value: account.balance_money.exchange_to(currency) } ]) + else + TimeSeries.from_collection(balance_series, :balance_money) + end + rescue Money::ConversionError + TimeSeries.new([]) + end end diff --git a/app/models/investment.rb b/app/models/investment.rb index 145a617d..15ef92af 100644 --- a/app/models/investment.rb +++ b/app/models/investment.rb @@ -13,4 +13,35 @@ class Investment < ApplicationRecord [ "Roth 401k", "roth_401k" ], [ "Angel", "angel" ] ].freeze + + def value + account.balance_money + holdings_value + end + + def holdings_value + account.holdings.current.known_value.sum(&:amount) || Money.new(0, account.currency) + end + + def series(period: Period.all, currency: account.currency) + balance_series = account.balances.in_period(period).where(currency: currency) + holding_series = account.holdings.known_value.in_period(period).where(currency: currency) + + holdings_by_date = holding_series.group_by(&:date).transform_values do |holdings| + holdings.sum(&:amount) + end + + combined_series = balance_series.map do |balance| + holding_amount = holdings_by_date[balance.date] || 0 + + { date: balance.date, value: Money.new(balance.balance + holding_amount, currency) } + end + + if combined_series.empty? && period.date_range.end == Date.current + TimeSeries.new([ { date: Date.current, value: self.value.exchange_to(currency) } ]) + else + TimeSeries.new(combined_series) + end + rescue Money::ConversionError + TimeSeries.new([]) + end end diff --git a/app/models/provider/synth.rb b/app/models/provider/synth.rb index 05502d28..b9523051 100644 --- a/app/models/provider/synth.rb +++ b/app/models/provider/synth.rb @@ -24,6 +24,11 @@ class Provider::Synth prices: prices, success?: true, raw_response: prices.to_json + rescue StandardError => error + SecurityPriceResponse.new \ + success?: false, + error: error, + raw_response: error end def fetch_exchange_rate(from:, to:, date:) diff --git a/app/views/account/cashes/_cash.html.erb b/app/views/account/cashes/_cash.html.erb new file mode 100644 index 00000000..e5e2065d --- /dev/null +++ b/app/views/account/cashes/_cash.html.erb @@ -0,0 +1,21 @@ +<%# locals: (holding:) %> + +<%= turbo_frame_tag dom_id(holding) do %> +
+
+ <%= render "shared/circle_logo", name: holding.name %> +
+ <%= tag.p holding.name %> + <%= tag.p holding.ticker, class: "text-gray-500 text-xs uppercase" %> +
+
+ +
+ <% if holding.amount_money %> + <%= tag.p format_money holding.amount_money %> + <% else %> + <%= tag.p "?", class: "text-gray-500" %> + <% end %> +
+
+<% end %> diff --git a/app/views/account/cashes/index.html.erb b/app/views/account/cashes/index.html.erb new file mode 100644 index 00000000..1d0b8c82 --- /dev/null +++ b/app/views/account/cashes/index.html.erb @@ -0,0 +1,18 @@ +<%= turbo_frame_tag dom_id(@account, "cash") do %> +
+
+ <%= tag.h2 t(".cash"), class: "font-medium text-lg" %> +
+ +
+
+ <%= tag.p t(".name"), class: "col-span-9" %> + <%= tag.p t(".value"), class: "col-span-3 justify-self-end" %> +
+ +
+ <%= render partial: "account/cashes/cash", collection: [brokerage_cash(@account)], as: :holding %> +
+
+
+<% end %> diff --git a/app/views/account/entries/_entry_group.html.erb b/app/views/account/entries/_entry_group.html.erb index c47d7130..2399dd4b 100644 --- a/app/views/account/entries/_entry_group.html.erb +++ b/app/views/account/entries/_entry_group.html.erb @@ -1,4 +1,4 @@ -<%# locals: (date:, entries:, selectable: true, **opts) %> +<%# locals: (date:, entries:, selectable: true, combine_transfers: false, **opts) %>
@@ -15,7 +15,11 @@ <%= totals_by_currency(collection: entries, money_method: :amount_money, negate: true) %>
- <%= render entries.reject { |e| e.transfer_id.present? }, selectable:, **opts %> - <%= render transfer_entries(entries), selectable:, **opts %> + <% if combine_transfers %> + <%= render entries.reject { |e| e.transfer_id.present? }, selectable:, **opts %> + <%= render transfer_entries(entries), selectable: false, **opts %> + <% else %> + <%= render entries, selectable:, **opts %> + <% end %>
diff --git a/app/views/account/entries/entryables/transaction/_transaction.html.erb b/app/views/account/entries/entryables/transaction/_transaction.html.erb index 851af438..0e7f7ebe 100644 --- a/app/views/account/entries/entryables/transaction/_transaction.html.erb +++ b/app/views/account/entries/entryables/transaction/_transaction.html.erb @@ -1,8 +1,9 @@ <%# locals: (entry:, selectable: true, editable: true, short: false, show_tags: false, **opts) %> <% transaction, account = entry.account_transaction, entry.account %> +<% is_investment_transfer = entry.account.investment? && entry.transfer.present? %>
- <% name_col_span = entry.marked_as_transfer? ? "col-span-10" : short ? "col-span-6" : "col-span-4" %> + <% name_col_span = unconfirmed_transfer?(entry) ? "col-span-10" : short ? "col-span-6" : "col-span-4" %>
<% if selectable %> <%= check_box_tag dom_id(entry, "selection"), @@ -51,6 +52,12 @@ <% end %>
+ <% if is_investment_transfer %> +
+ <%= tag.p entry.inflow? ? t(".deposit") : t(".withdrawal") %> +
+ <% end %> + <% unless entry.marked_as_transfer? %> <% unless short %>
"> @@ -82,7 +89,7 @@ <% end %> <% end %> -
+
ml-auto"> <%= content_tag :p, format_money(-entry.amount_money), class: ["text-green-600": entry.inflow?] %> diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 478a6ff3..3d3de088 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -53,13 +53,13 @@
<%= tag.p t(".total_value"), class: "text-sm font-medium text-gray-500" %> - <%= tag.p format_money(@account.balance_money, precision: 0), class: "text-gray-900 text-3xl font-medium" %> + <%= tag.p format_money(@account.value, precision: 0), class: "text-gray-900 text-3xl font-medium" %>
- <% if @balance_series.trend.direction.flat? %> + <% if @series.trend.direction.flat? %> <%= tag.span t(".no_change"), class: "text-gray-500" %> <% else %> - <%= tag.span format_money(@balance_series.trend.value), style: "color: #{@balance_series.trend.color}" %> - <%= tag.span "(#{@balance_series.trend.percent}%)", style: "color: #{@balance_series.trend.color}" %> + <%= tag.span format_money(@series.trend.value), style: "color: #{@trend.color}" %> + <%= tag.span "(#{@trend.percent}%)", style: "color: #{@trend.color}" %> <% end %> <%= tag.span period_label(@period), class: "text-gray-500" %> @@ -70,7 +70,7 @@ <% end %>
- <%= render partial: "shared/line_chart", locals: { series: @balance_series } %> + <%= render partial: "shared/line_chart", locals: { series: @series } %>
diff --git a/app/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index 4d37607d..695be9c8 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -28,7 +28,7 @@
<% @transaction_entries.group_by(&:date).each do |date, entries| %> - <%= render "account/entries/entry_group", date:, entries: %> + <%= render "account/entries/entry_group", date:, combine_transfers: true, entries: %> <% end %>
diff --git a/config/locales/views/account/cashes/en.yml b/config/locales/views/account/cashes/en.yml new file mode 100644 index 00000000..96da5309 --- /dev/null +++ b/config/locales/views/account/cashes/en.yml @@ -0,0 +1,8 @@ +--- +en: + account: + cashes: + index: + cash: Cash + name: Name + value: Total Balance diff --git a/config/locales/views/account/entries/en.yml b/config/locales/views/account/entries/en.yml index 8d81a30e..c4c26303 100644 --- a/config/locales/views/account/entries/en.yml +++ b/config/locales/views/account/entries/en.yml @@ -47,9 +47,11 @@ en: settings: Settings tags_label: Select one or more tags transaction: + deposit: Deposit remove_transfer: Remove transfer remove_transfer_body: This will remove the transfer from this transaction remove_transfer_confirm: Confirm + withdrawal: Withdrawal valuation: form: cancel: Cancel @@ -70,10 +72,10 @@ en: loading: Loading entries... trades: amount: Amount - new: New trade - no_trades: No trades for this account yet. - trade: trade - trades: Trades + new: New transaction + no_trades: No transactions for this account yet. + trade: transaction + trades: Transactions type: Type transactions: new: New transaction diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml index de32cef1..ef9ea9d3 100644 --- a/config/locales/views/accounts/en.yml +++ b/config/locales/views/accounts/en.yml @@ -48,6 +48,7 @@ en: title: Add an account ungrouped: "(none)" show: + cash: Cash confirm_accept: Delete "%{name}" confirm_body_html: "

By deleting this account, you will erase its value history, affecting various aspects of your overall account. This action will have a @@ -63,7 +64,7 @@ en: graphs may not reflect accurate values. sync_message_unknown_error: An error has occurred during the sync. total_value: Total Value - trades: Trades + trades: Transactions transactions: Transactions value: Value summary: diff --git a/config/routes.rb b/config/routes.rb index 8c3a73f1..e2963712 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,6 +79,7 @@ Rails.application.routes.draw do resource :logo, only: :show resources :holdings, only: %i[ index new show ] + resources :cashes, only: :index resources :entries, except: :index do collection do