From b82fc0fc80f79b01656115bca768f63aaaf60f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 27 Sep 2026 13:21:26 +0200 Subject: [PATCH] Show negative values as parentheses on Accounts --- lib/hledger/report_builder.rb | 48 +++++++++++++-------- spec/lib/hledger/integration_spec.rb | 1 + spec/lib/hledger/report_builder_spec.rb | 3 +- test/javascripts/acceptance/hledger-test.js | 12 ++++++ 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/lib/hledger/report_builder.rb b/lib/hledger/report_builder.rb index ce9cefc..5784206 100644 --- a/lib/hledger/report_builder.rb +++ b/lib/hledger/report_builder.rb @@ -42,7 +42,11 @@ module Hledger rows, total = parse_json(result.stdout) sections = [ - { "title" => nil, "rows" => account_rows(Array(rows)), "total" => amounts(Array(total)) }, + { + "title" => nil, + "rows" => account_rows(Array(rows)), + "total" => amounts(Array(total), parenthesize: true), + }, ] envelope("accounts", begin_date, end_date, "sections" => sections, "net" => nil) @@ -176,7 +180,7 @@ module Hledger [(full || display).to_s, Array(row_amounts).map { |amount| raw_amount(amount) }] end - tree_rows(entries) + tree_rows(entries, parenthesize: true) end def compound_rows(rows) @@ -204,11 +208,13 @@ module Hledger end end - def tree_rows(entries) + def tree_rows(entries, parenthesize: false) AccountTree .expand(entries) .map do |node| - node.merge("amounts" => node["amounts"].map { |part| normalized_amount(part) }) + node.merge( + "amounts" => node["amounts"].map { |part| normalized_amount(part, parenthesize:) }, + ) end end @@ -216,8 +222,8 @@ module Hledger label.to_s.casecmp?(title.to_s) end - def amounts(list) - Array(list).map { |amount| normalized_amount(raw_amount(amount)) } + def amounts(list, parenthesize: false) + Array(list).map { |amount| normalized_amount(raw_amount(amount), parenthesize:) } end def raw_amount(amount) @@ -228,7 +234,7 @@ module Hledger } end - def normalized_amount(part) + def normalized_amount(part, parenthesize: false) commodity = part["commodity"].to_s quantity = part["quantity"] style = part["style"] || {} @@ -236,7 +242,7 @@ module Hledger { "commodity" => commodity, "quantity" => quantity.to_s("F"), - "display" => display_amount(quantity, commodity, style), + "display" => display_amount(quantity, commodity, style, parenthesize:), "side" => style["ascommodityside"] || "R", "spaced" => style["ascommodityspaced"] ? true : false, "precision" => style["asprecision"], @@ -251,25 +257,31 @@ module Hledger BigDecimal(mantissa) / (10**places) end - def display_amount(quantity, commodity, style) + def display_amount(quantity, commodity, style, parenthesize: false) precision = style["asprecision"] if precision.nil? - rendered = quantity.to_s("F") + rendered = quantity.abs.to_s("F") precision = rendered.include?(".") ? rendered.split(".").last.length : 0 end mark = style["asdecimalmark"].presence || "." - number = format("%.#{precision}f", quantity) + magnitude = parenthesize ? quantity.abs : quantity + number = format("%.#{precision}f", magnitude) number = number.tr(".", mark) if mark != "." - return number if commodity.blank? + text = + if commodity.blank? + number + else + spaced = style["ascommodityspaced"] ? " " : "" + if style["ascommodityside"] == "L" + "#{commodity}#{spaced}#{number}" + else + "#{number}#{spaced}#{commodity}" + end + end - spaced = style["ascommodityspaced"] ? " " : "" - if style["ascommodityside"] == "L" - "#{commodity}#{spaced}#{number}" - else - "#{number}#{spaced}#{commodity}" - end + parenthesize && quantity.negative? ? "(#{text})" : text end def balance_flags(begin_date, end_date) diff --git a/spec/lib/hledger/integration_spec.rb b/spec/lib/hledger/integration_spec.rb index 5294f6c..b5865c2 100644 --- a/spec/lib/hledger/integration_spec.rb +++ b/spec/lib/hledger/integration_spec.rb @@ -40,6 +40,7 @@ RSpec.describe "Hledger integration" do expect(by_account.keys.first(3)).to eq(%w[assets assets:bank assets:bank:checking]) expect(by_account["assets:bank"]["label"]).to eq("Bank") expect(by_account["assets:bank"]["amounts"].first["display"]).to eq("1029.50 EUR") + expect(by_account["equity:alice"]["amounts"].first["display"]).to eq("(600.00 EUR)") end it "renders the balance sheet and income statement as trees" do diff --git a/spec/lib/hledger/report_builder_spec.rb b/spec/lib/hledger/report_builder_spec.rb index aa572b2..de7db69 100644 --- a/spec/lib/hledger/report_builder_spec.rb +++ b/spec/lib/hledger/report_builder_spec.rb @@ -63,7 +63,8 @@ RSpec.describe Hledger::ReportBuilder do expect(assets["amounts"].first["display"]).to eq("1029.50 EUR") equity = rows.find { |row| row["account"] == "equity" } - expect(equity["amounts"].first["display"]).to eq("-1000.00 EUR") + expect(equity["amounts"].first["display"]).to eq("(1000.00 EUR)") + expect(equity["amounts"].first["quantity"]).to eq("-1000.0") checking = rows.find { |row| row["account"] == "assets:bank:checking" } expect(checking["depth"]).to eq(2) diff --git a/test/javascripts/acceptance/hledger-test.js b/test/javascripts/acceptance/hledger-test.js index f5ed398..ca663d1 100644 --- a/test/javascripts/acceptance/hledger-test.js +++ b/test/javascripts/acceptance/hledger-test.js @@ -40,6 +40,15 @@ const accountsReport = { { commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }, ], }, + { + account: "equity", + label: "Equity", + depth: 0, + root: true, + amounts: [ + { commodity: "EUR", quantity: "-1000.0", display: "(1000.00 EUR)" }, + ], + }, ], total: [{ commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }], }, @@ -279,6 +288,9 @@ acceptance("Hledger plugin", function (needs) { assert.dom(".hledger-dashboard__table").includesText("Bank"); assert.dom(".hledger-dashboard__table").includesText("Checking"); assert.dom(".hledger-dashboard__table").includesText("1000.00 EUR"); + assert + .dom(".hledger-dashboard__table") + .includesText("(1000.00 EUR)", "shows credit balances in parentheses"); assert.dom(".hledger-dashboard__account.--depth-1").hasText("Bank"); assert.dom(".hledger-dashboard__account.--depth-2").hasText("Checking"); assert