Packages
beancount_ex
0.6.0
An idiomatic Elixir interface to Beancount that serves as the long-term behavioral oracle for a future native Elixir General Ledger.
Current section
Files
Jump to
Current section
Files
lib/beancount/engine/elixir/posting_amount.ex
defmodule Beancount.Engine.Elixir.PostingAmount do
@moduledoc false
alias Beancount.CostSpec
alias Beancount.Directives.Posting
@spec balance_contribution(Posting.t()) :: {String.t(), Decimal.t()} | nil
def balance_contribution(%Posting{} = posting) do
cond do
contribution = cost_contribution(posting) ->
contribution
contribution = price_contribution(posting) ->
contribution
true ->
amount_currency_contribution(posting)
end
end
@spec expand_postings([Posting.t()]) :: [Posting.t()]
def expand_postings(postings) do
case infer_elided_posting(postings) do
nil ->
postings
{index, amount, currency} ->
List.update_at(postings, index, &%{&1 | amount: amount, currency: currency})
end
end
@spec transaction_totals([Posting.t()]) :: %{String.t() => Decimal.t()}
def transaction_totals(postings) do
postings
|> expand_postings()
|> Enum.reduce(%{}, fn posting, totals ->
case balance_contribution(posting) do
{currency, amount} ->
Map.update(totals, currency, amount, &Decimal.add(&1, amount))
nil ->
totals
end
end)
end
defp infer_elided_posting(postings) do
{totals, elided} = fold_posting_totals(postings)
case {elided, single_nonzero_currency(totals)} do
{[{index, _posting}], {currency, total}} ->
{index, Decimal.negate(total), currency}
_ ->
nil
end
end
defp fold_posting_totals(postings) do
Enum.reduce(Enum.with_index(postings), {%{}, []}, &fold_posting_total/2)
end
defp fold_posting_total({posting, index}, {totals, elided}) do
case balance_contribution(posting) do
{currency, amount} ->
{Map.update(totals, currency, amount, &Decimal.add(&1, amount)), elided}
nil when is_nil(posting.amount) ->
{totals, [{index, posting} | elided]}
nil ->
{totals, elided}
end
end
defp single_nonzero_currency(totals) do
case Enum.reject(totals, fn {_, total} -> Decimal.equal?(total, 0) end) do
[{currency, total}] -> {currency, total}
_ -> nil
end
end
defp amount_currency_contribution(%Posting{amount: %Decimal{} = amount, currency: currency})
when is_binary(currency),
do: {currency, amount}
defp amount_currency_contribution(_), do: nil
defp cost_contribution(%Posting{cost: %CostSpec{} = cost} = posting) do
case cost_basis_from_spec(posting.amount, cost) do
{currency, amount} -> {currency, amount}
nil -> nil
end
end
defp cost_contribution(_), do: nil
defp cost_basis_from_spec(%Decimal{} = units, %CostSpec{
per_amount: %Decimal{} = per_amount,
per_currency: per_currency
})
when is_binary(per_currency) do
{per_currency, Decimal.mult(units, per_amount)}
end
defp cost_basis_from_spec(_units, %CostSpec{
total_amount: %Decimal{} = total_amount,
total_currency: total_currency
})
when is_binary(total_currency) do
{total_currency, total_amount}
end
defp cost_basis_from_spec(_units, _cost), do: nil
defp price_contribution(%Posting{
amount: %Decimal{} = amount,
price: %{amount: price_amount, currency: price_currency, type: :unit}
})
when is_binary(price_currency) do
{price_currency, Decimal.mult(amount, price_amount)}
end
defp price_contribution(%Posting{
price: %{amount: total_amount, currency: price_currency, type: :total}
})
when is_binary(price_currency) do
{price_currency, total_amount}
end
defp price_contribution(_), do: nil
end