From 015988f0dcfbe540414f11c84e5d1ee47a605938 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 31 Mar 2026 22:31:44 -1000 Subject: [PATCH 01/15] fix:add: date-weight similar transactions by absolute distance from today Similar transactions were being date-weighted by their distance from the journal's latest transaction date. This meant a future-dated transaction, perhaps caused by a typo, could be greatly prioritised, causing an unexpected choice of transaction to use for defaults. Now, we date-weight by absolute distance (past or future) from today's date. --- hledger-lib/Hledger/Data/Journal.hs | 49 ++++++++++++++--------------- hledger/Hledger/Cli/Commands/Add.md | 2 +- hledger/Hledger/Cli/Utils.hs | 6 ++-- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/hledger-lib/Hledger/Data/Journal.hs b/hledger-lib/Hledger/Data/Journal.hs index 1c0f5b45fb3..8cef888db9a 100644 --- a/hledger-lib/Hledger/Data/Journal.hs +++ b/hledger-lib/Hledger/Data/Journal.hs @@ -128,20 +128,23 @@ import Control.Monad.State.Strict (StateT) import Data.Char (toUpper, isDigit) import Data.Default (Default(..)) import Data.Foldable (toList) -import Data.List ((\\), find, sortBy, union, intercalate) +import Data.Function ((&)) +import Data.List (find, intercalate, sortBy, union, (\\)) #if !MIN_VERSION_base(4,20,0) import Data.List (foldl') #endif import Data.List.Extra (nubSort) import Data.Map.Strict qualified as M import Data.Maybe (catMaybes, fromMaybe, mapMaybe, maybeToList) +import Data.Ord (comparing) import Data.Set qualified as S import Data.Text (Text) import Data.Text qualified as T -import Safe (headMay, headDef, maximumMay, minimumMay, lastDef) -import Data.Time.Calendar (Day, addDays, fromGregorian, diffDays) +import Data.Time.Calendar (Day, addDays, diffDays, fromGregorian) import Data.Time.Clock.POSIX (POSIXTime) -import Data.Tree (Tree(..), flatten) +import Data.Tree (Tree (..), flatten) +import Safe (headMay, headDef, maximumMay, minimumMay) +import System.FilePath (takeFileName) import Text.Printf (printf) import Text.Megaparsec (ParsecT) @@ -154,12 +157,6 @@ import Hledger.Data.Transaction import Hledger.Data.TransactionModifier import Hledger.Data.Valuation import Hledger.Query -import System.FilePath (takeFileName) -import Data.Ord (comparing) -import Hledger.Data.Dates (nulldate) -import Data.List (sort) -import Data.Function ((&)) --- import Data.Function ((&)) -- | A parser of text that runs in some monad, keeping a Journal as state. @@ -530,36 +527,38 @@ journalInheritedAccountTags j a = type DateWeightedSimilarityScore = Double type SimilarityScore = Double -type Age = Integer +type TimeDistance = Integer --- | Find up to N most similar and most recent transactions matching +-- | Find up to N most similar and nearest-dated transactions matching -- the given transaction description and query and exceeding the given -- description similarity score (0 to 1, see compareDescriptions). +-- The provided Day (today) is used as the reference date; +-- transactions are penalised by their absolute distance from it in days +-- (both past and future). -- Returns transactions along with --- their age in days compared to the latest transaction date, +-- their distance in days from today, -- their description similarity score, --- and a heuristically date-weighted variant of this that favours more recent transactions. -journalTransactionsSimilarTo :: Journal -> Text -> Query -> SimilarityScore -> Int - -> [(DateWeightedSimilarityScore, Age, SimilarityScore, Transaction)] -journalTransactionsSimilarTo Journal{jtxns} desc q similaritythreshold n = +-- and a heuristically date-weighted variant of this that favours nearby transactions. +journalTransactionsSimilarTo :: Journal -> Day -> Text -> Query -> SimilarityScore -> Int + -> [(DateWeightedSimilarityScore, TimeDistance, SimilarityScore, Transaction)] +journalTransactionsSimilarTo Journal{jtxns} today desc q similaritythreshold n = take n $ dbg1With ( - unlines . - ("up to 30 transactions above description similarity threshold "<>show similaritythreshold<>" ordered by recency-weighted similarity:":) . + unlines . + ("up to 30 transactions above description similarity threshold "<>show similaritythreshold<>" ordered by proximity-weighted similarity:":) . take 30 . - map ( \(w,a,s,Transaction{..}) -> printf "weighted:%8.3f age:%4d similarity:%5.3f %s %s" w a s (show tdate) tdescription )) $ + map ( \(w,d,s,Transaction{..}) -> printf "weighted:%8.3f distance:%4d similarity:%5.3f %s %s" w d s (show tdate) tdescription )) $ sortBy (comparing (negate.first4)) $ - map (\(s,t) -> (weightedScore (s,t), age t, s, t)) $ + map (\(s,t) -> (weightedScore (s,t), timedistance t, s, t)) $ filter ((> similaritythreshold).fst) [(compareDescriptions desc $ tdescription t, t) | t <- jtxns, q `matchesTransaction` t] where - latest = lastDef nulldate $ sort $ map tdate jtxns - age = diffDays latest . tdate - -- Combine similarity and recency heuristically. This gave decent results + timedistance t = abs $ diffDays today (tdate t) + -- Combine similarity and time distance heuristically. This gave decent results -- in my "find most recent invoice" use case in 2023-03, -- but will probably need more attention. weightedScore :: (Double, Transaction) -> Double - weightedScore (s, t) = 100 * s - fromIntegral (age t) / 4 + weightedScore (s, t) = 100 * s - fromIntegral (timedistance t) / 4 -- | Return a similarity score from 0 to 1.5 for two transaction descriptions. -- This is based on compareStrings, with the following modifications: diff --git a/hledger/Hledger/Cli/Commands/Add.md b/hledger/Hledger/Cli/Commands/Add.md index d3de10f18a1..350ed2d3a72 100644 --- a/hledger/Hledger/Cli/Commands/Add.md +++ b/hledger/Hledger/Cli/Commands/Add.md @@ -21,7 +21,7 @@ enter `.` or press control-d or control-c to exit. Features: - add tries to provide useful defaults, using the most similar (by description) - recent transaction (filtered by the query, if any) as a template. + and nearest-dated transaction (filtered by the query, if any) as a template. - You can also set the initial defaults with command line arguments. - [Readline-style edit keys](http://tiswww.case.edu/php/chet/readline/rluserman.html#SEC3) can be used during data entry. diff --git a/hledger/Hledger/Cli/Utils.hs b/hledger/Hledger/Cli/Utils.hs index cfb81ac2214..4f9d36d8f34 100644 --- a/hledger/Hledger/Cli/Utils.hs +++ b/hledger/Hledger/Cli/Utils.hs @@ -270,12 +270,14 @@ backupNumber f g = case g =~ ("^" ++ f ++ "\\.([0-9]+)$") of (_::FilePath, _::FilePath, _::FilePath, [ext::FilePath]) -> readMay ext _ -> Nothing --- Identify the closest recent match for this description in past transactions. +-- Identify the closest match for this description in past/future transactions, +-- considering both similarity and proximity to today's date. -- If the options specify a query, only matched transactions are considered. journalSimilarTransaction :: CliOpts -> Journal -> T.Text -> Maybe Transaction journalSimilarTransaction cliopts j desc = - fmap fourth4 $ headMay $ journalTransactionsSimilarTo j desc q 0 1 + fmap fourth4 $ headMay $ journalTransactionsSimilarTo j today desc q 0 1 where + today = cliopts ^. rsDay q = queryFromFlags $ _rsReportOpts $ reportspec_ cliopts -- | Render a 'PostingsReport' or 'AccountTransactionsReport' as Text, From 8d5663e4ced6fdd8343b3b09e8098c1e1771e974 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 31 Mar 2026 19:34:33 -1000 Subject: [PATCH 02/15] imp:journal: exclude Gain postings from txn balancing; and don't infer it Port three Gain-related changes from hledger2 to improve compatibility between hledger 1 and 2: 1. Don't infer Gain (G) type from account names; it must be declared explicitly with a type: tag. 2. Exclude Gain postings from transaction balancing. 3. Because Gain postings can be left amountless, always exclude amountless postings from reports. --- hledger-lib/Hledger/Data/AccountName.hs | 16 +++++----- hledger-lib/Hledger/Data/Balancing.hs | 31 +++++++++++++------ hledger-lib/Hledger/Read/Common.hs | 2 +- .../Reports/AccountTransactionsReport.hs | 2 +- .../Hledger/Reports/MultiBalanceReport.hs | 1 + hledger-lib/Hledger/Reports/PostingsReport.hs | 3 +- hledger/hledger.m4.md | 12 ++++++- .../test/journal/transaction-balancing.test | 12 +++++++ hledger/test/query-type.test | 12 ------- 9 files changed, 58 insertions(+), 33 deletions(-) create mode 100644 hledger/test/journal/transaction-balancing.test diff --git a/hledger-lib/Hledger/Data/AccountName.hs b/hledger-lib/Hledger/Data/AccountName.hs index 237c86e82a5..f4bd4ccde75 100644 --- a/hledger-lib/Hledger/Data/AccountName.hs +++ b/hledger-lib/Hledger/Data/AccountName.hs @@ -112,7 +112,6 @@ accountNameInferType a | regexMatchText liabilityAccountRegex a = Just Liability | regexMatchText conversionAccountRegex a = Just Conversion | regexMatchText equityAccountRegex a = Just Equity - | regexMatchText gainAccountRegex a = Just Gain | regexMatchText revenueAccountRegex a = Just Revenue | regexMatchText expenseAccountRegex a = Just Expense | otherwise = Nothing @@ -450,13 +449,14 @@ tests_AccountName = testGroup "AccountName" [ accountNameInferType "revenues" @?= Just Revenue accountNameInferType "revenue" @?= Just Revenue accountNameInferType "income" @?= Just Revenue - accountNameInferType "income:gains" @?= Just Gain - accountNameInferType "revenue:gain" @?= Just Gain - accountNameInferType "revenues:capital-gains" @?= Just Gain - accountNameInferType "income:capitalgain" @?= Just Gain - accountNameInferType "income:losses" @?= Just Gain - accountNameInferType "revenue:capital-loss" @?= Just Gain - accountNameInferType "income:gains:realized" @?= Just Gain + -- Gain type is no longer inferred from names; these are now Revenue + accountNameInferType "income:gains" @?= Just Revenue + accountNameInferType "revenue:gain" @?= Just Revenue + accountNameInferType "revenues:capital-gains" @?= Just Revenue + accountNameInferType "income:capitalgain" @?= Just Revenue + accountNameInferType "income:losses" @?= Just Revenue + accountNameInferType "revenue:capital-loss" @?= Just Revenue + accountNameInferType "income:gains:realized" @?= Just Revenue ,testCase "joinAccountNames" $ do joinAccountNames "assets" "cash" @?= "assets:cash" joinAccountNames "assets:cash" "a" @?= "assets:cash:a" diff --git a/hledger-lib/Hledger/Data/Balancing.hs b/hledger-lib/Hledger/Data/Balancing.hs index 85d6c33e35a..717925163e0 100644 --- a/hledger-lib/Hledger/Data/Balancing.hs +++ b/hledger-lib/Hledger/Data/Balancing.hs @@ -51,7 +51,7 @@ import Safe (headErr) import Text.Printf (printf) import Hledger.Data.Types -import Hledger.Data.AccountName (isAccountNamePrefixOf) +import Hledger.Data.AccountName (accountNameType, isAccountNamePrefixOf) import Hledger.Data.Amount import Hledger.Data.Journal import Hledger.Data.Posting @@ -66,6 +66,7 @@ data BalancingOpts = BalancingOpts -- Distinct from InputOpts{infer_costs_}. , commodity_styles_ :: Maybe (M.Map CommoditySymbol AmountStyle) -- ^ commodity display styles , txn_balancing_ :: TransactionBalancingPrecision + , account_types_ :: M.Map AccountName AccountType -- ^ account type map, used to exclude Gain postings from balancing } deriving (Eq, Ord, Show) defbalancingopts :: BalancingOpts @@ -74,8 +75,15 @@ defbalancingopts = BalancingOpts , infer_balancing_costs_ = True , commodity_styles_ = Nothing , txn_balancing_ = TBPExact + , account_types_ = M.empty } +-- | Is this posting to a Gain-type account ? +-- Used to exclude gain/loss postings from normal transaction balancing, +-- since gain postings balance at cost basis, not at selling price. +isGainPosting :: M.Map AccountName AccountType -> Posting -> Bool +isGainPosting atypes p = accountNameType atypes (paccount p) == Just Gain + -- | Check that this transaction would appear balanced to a human when displayed. -- On success, returns the empty list, otherwise one or more error messages. -- @@ -94,10 +102,13 @@ defbalancingopts = BalancingOpts -- (using the given display styles if provided) -- transactionCheckBalanced :: BalancingOpts -> Transaction -> [String] -transactionCheckBalanced BalancingOpts{commodity_styles_=_mglobalstyles, txn_balancing_} t = errs +transactionCheckBalanced BalancingOpts{commodity_styles_=_mglobalstyles, txn_balancing_, account_types_} t = errs where + -- In disposal transactions, gain postings are excluded from normal balancing + isGain = isGainPosting account_types_ + -- get real and balanced virtual postings, to be checked separately - (rps, bvps) = foldr partitionPosting ([], []) $ tpostings t + (rps, bvps) = foldr partitionPosting ([], []) $ filter (not . isGain) $ tpostings t where partitionPosting p ~(l, r) = case ptype p of RegularPosting -> (p:l, r) @@ -193,7 +204,7 @@ balanceTransactionHelper bopts t = do (t', inferredamtsandaccts) <- t & (if infer_balancing_costs_ bopts then transactionInferBalancingCosts else id) & dbg9With (lbl "amounts after balancing-cost-inferring".show.map showMixedAmountOneLine.transactionAmounts) - & transactionInferBalancingAmount (fromMaybe M.empty $ commodity_styles_ bopts) + & transactionInferBalancingAmount (fromMaybe M.empty $ commodity_styles_ bopts) (account_types_ bopts) <&> dbg9With (lbl "balancing amounts inferred".show.map (second showMixedAmountOneLine).snd) case transactionCheckBalanced bopts t' of [] -> Right (txnTieKnot t', inferredamtsandaccts) @@ -242,9 +253,10 @@ transactionBalanceError t errs = printf "%s:\n%s\n\nThis %stransaction is unbala -- have the same price(s), and will be converted to the price commodity. transactionInferBalancingAmount :: M.Map CommoditySymbol AmountStyle -- ^ commodity display styles + -> M.Map AccountName AccountType -- ^ account type map (for excluding Gain postings) -> Transaction -> Either String (Transaction, [(AccountName, MixedAmount)]) -transactionInferBalancingAmount styles t@Transaction{tpostings=ps} +transactionInferBalancingAmount styles atypes t@Transaction{tpostings=ps} | length amountlessrealps > 1 = Left $ transactionBalanceError t ["There can't be more than one real posting with no amount." @@ -263,7 +275,8 @@ transactionInferBalancingAmount styles t@Transaction{tpostings=ps} ) where lbl = lbl_ "transactionInferBalancingAmount" - (amountfulrealps, amountlessrealps) = partition hasAmount (realPostings t) + isGain = isGainPosting atypes + (amountfulrealps, amountlessrealps) = partition hasAmount (filter (not . isGain) $ realPostings t) realsum = sumPostings amountfulrealps -- & dbg9With (lbl "real balancing amount".showMixedAmountOneLine) (amountfulbvps, amountlessbvps) = partition hasAmount (balancedVirtualPostings t) @@ -793,10 +806,10 @@ tests_Balancing = testGroup "Balancing" [ testCase "transactionInferBalancingAmount" $ do - (fst <$> transactionInferBalancingAmount M.empty nulltransaction) @?= Right nulltransaction - (fst <$> transactionInferBalancingAmount M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` missingamt]}) @?= + (fst <$> transactionInferBalancingAmount M.empty M.empty nulltransaction) @?= Right nulltransaction + (fst <$> transactionInferBalancingAmount M.empty M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` missingamt]}) @?= Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` usd 5]} - (fst <$> transactionInferBalancingAmount M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` missingamt]}) @?= + (fst <$> transactionInferBalancingAmount M.empty M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` missingamt]}) @?= Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` usd 1]} , testGroup "balanceSingleTransaction" [ diff --git a/hledger-lib/Hledger/Read/Common.hs b/hledger-lib/Hledger/Read/Common.hs index d068979f389..ca02e4b9afc 100644 --- a/hledger-lib/Hledger/Read/Common.hs +++ b/hledger-lib/Hledger/Read/Common.hs @@ -404,7 +404,7 @@ journalFinalise iopts@InputOpts{auto_,balancingopts_,infer_costs_,infer_equity_, -- >>= \j -> deepseq (concatMap (T.unpack.showTransaction).jtxns $ j) (return j) <&> dbg9With (lbl "amounts after styling, forecasting, auto-posting".showJournalPostingAmountsDebug) >>= (\j -> if checkordereddates then journalCheckOrdereddates j $> j else Right j) -- check ordereddates before assertions. The outer parentheses are needed. - >>= journalBalanceTransactions balancingopts_{ignore_assertions_=not checkassertions} -- infer balance assignments and missing amounts, and maybe check balance assertions. + >>= (\j -> journalBalanceTransactions balancingopts_{ignore_assertions_=not checkassertions, account_types_ = jaccounttypes j} j) -- infer balance assignments and missing amounts, and maybe check balance assertions. <&> dbg9With (lbl "amounts after transaction-balancing".showJournalPostingAmountsDebug) -- <&> dbg9With (("journalFinalise amounts after styling, forecasting, auto postings, transaction balancing"<>).showJournalPostingAmountsDebug) >>= journalInferCommodityStyles -- infer commodity styles once more now that all posting amounts are present diff --git a/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs b/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs index d91aeb16b6d..5c02434798f 100644 --- a/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs +++ b/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs @@ -146,7 +146,7 @@ accountTransactionsReport rspec@ReportSpec{_rsReportOpts=ropts} j thisacctq = it | balanceaccum_ ropts == Historical = sumPostings priorps | otherwise = nullmixedamt where - priorps = dbg5 "priorps" . journalPostings $ filterJournalPostings priorq acctJournal + priorps = dbg5 "priorps" . filter hasAmount . journalPostings $ filterJournalPostings priorq acctJournal priorq = dbg5 "priorq" $ And [thisacctq, tostartdateq, datelessreportq] tostartdateq = case mstartdate of diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 595ccf3f26f..831f744d8f8 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -183,6 +183,7 @@ makeReportQuery rspec reportspan getPostings :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> [Posting] getPostings rspec@ReportSpec{_rsQuery=query, _rsReportOpts=ropts} j priceoracle reportspan = setPostingsCount + . filter hasAmount -- omit postings with no definite amount . journalPostings $ journalValueAndFilterPostingsWith rspec' j priceoracle where diff --git a/hledger-lib/Hledger/Reports/PostingsReport.hs b/hledger-lib/Hledger/Reports/PostingsReport.hs index 129a9c1fbaa..4f0f488f113 100644 --- a/hledger-lib/Hledger/Reports/PostingsReport.hs +++ b/hledger-lib/Hledger/Reports/PostingsReport.hs @@ -156,9 +156,10 @@ matchedPostingsBeforeAndDuring rspec@ReportSpec{_rsReportOpts=ropts,_rsQuery=q} dbg5 "beforeps, duringps" $ span (beforestartq `matchesPosting`) beforeandduringps where beforestartq = dbg3 "beforestartq" $ dateqtype $ DateSpan Nothing (Exact <$> spanStart reportspan) - beforeandduringps = + beforeandduringps = sortOn (postingDateOrDate2 (whichDate ropts)) -- sort postings by date or date2 . (if invert_ ropts then map postingNegateMainAmount else id) -- with --invert, invert amounts + . filter hasAmount -- omit postings with no definite amount . journalPostings -- With most calls we will not require transaction prices past this point, and can get a big -- speed improvement by stripping them early. In some cases, such as in hledger-ui, we still diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index ad9146f08a3..a31004404d6 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -1968,6 +1968,15 @@ If you hit this problem, it's easy to fix: 2. or make non-cost amounts less precise (remove unnecessary decimal digits that are raising the precision) 3. or add a posting to absorb the imbalance (eg "expenses:rounding". Remember that one posting may [omit the amount](#postings); that's convenient here.) +### Gain postings + +The `Gain` [account type](#account-types) has a special behaviour: +postings to Gain-type accounts are ignored by transaction balancing. + +This is useful in hledger 2, as part of its automated lots/gains tracking. +Gain postings are usually not used in hledger 1; but they are supported for better compatibility with hledger 2. +This makes it easier to keep a journal compatible with both hledger versions. + ## Tags @@ -2311,6 +2320,7 @@ hledger also uses a few subtypes: As a convenience, hledger will detect most of these types automatically from english account names. +(All except Gain. For more about Gain, see [Gain postings](#gain-postings).) But it's better to declare them explicitly by adding a `type:` [tag](#tags) in the account directives. The tag's value can be any of the types or one-letter abbreviations above. @@ -2329,7 +2339,7 @@ account assets:cash ; type: C account equity:conversion ; type: V -account revenues:capital ; type: G +account revenues:gains ; type: G ``` This enables the easy [balancesheet], [balancesheetequity], [cashflow] and [incomestatement] reports, and querying by [type:](#queries). diff --git a/hledger/test/journal/transaction-balancing.test b/hledger/test/journal/transaction-balancing.test new file mode 100644 index 00000000000..d13a2e25f45 --- /dev/null +++ b/hledger/test/journal/transaction-balancing.test @@ -0,0 +1,12 @@ +# * Transaction balancing + +# ** 1. Gain-type postings are excluded from transaction balancing. +< +account revenue:gains ; type:G + +2026-01-01 test + assets $100 + expenses -$100 + revenue:gains -$50 + +$ hledger -f- check balanced diff --git a/hledger/test/query-type.test b/hledger/test/query-type.test index cfd6203ec3d..bdf9e035aaf 100644 --- a/hledger/test/query-type.test +++ b/hledger/test/query-type.test @@ -158,15 +158,3 @@ account gains ; type:G $ hledger -f- accounts type:r revenue gains - -# ** 18. type:g matches auto-detected gain accounts -< -2022-02-02 Test - (income:gains) 1 - (revenue:capital-gains) 1 - (income:losses) 1 - -$ hledger -f- accounts type:g -income:gains -income:losses -revenue:capital-gains From a7496918264bb201cca54f6d7f10e5ffe30b4153 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Fri, 3 Apr 2026 12:39:27 -1000 Subject: [PATCH 03/15] ;doc: Cost basis / lot syntax -> Cost basis; updates, mention hledger 2 --- hledger/hledger.m4.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index a31004404d6..262979dca57 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -1724,7 +1724,7 @@ as long as the journal entry is well formed such that the equity postings / cost So in principle you could enable both `--infer-equity` and `--infer-costs` in your config file, and your reports would have the advantages of both. -## Cost basis / lot syntax +## Cost basis If you are buying some commodity to hold as an investment, it may be important to keep track of @@ -1733,22 +1733,26 @@ If you are buying some commodity to hold as an investment, it may be important t 3. and a sequence number or label, if needed, to disambiguate multiple acquisitions on the same day, or to serve as a mnemonic for easy reference. In hledger we call these three the "cost basis"; and if an amount being acquired has a cost basis, we call it a "lot". -Tax authorities often require that lots are tracked carefully and disposed of (sold) in a certain order. +Tax authorities sometimes require that lots are tracked in detail and disposed of (sold) in a certain order. Note, though "cost basis" sounds similar to the "cost" (transacted price) discussed above, they are distinct concepts. In some transactions the transacted price and basis cost are the same, but in others they are not. +So cost basis is recorded with its own syntax, called "cost basis annotations". -So cost basis has its own syntax, also called "lot syntax". -hledger's lot syntax is like Ledger's: one or more of the following annotations, following the main amount: +hledger 1.x supports Ledger-style cost basis syntax: one or more of the following annotations, following the main amount: - `{LOTUNITCOST}` or `{{{{LOTTOTALCOST}}}}` (see [lot price][ledger: buying and selling stock]) - `[LOTDATE]` (see [lot date][ledger: lot dates]) - `(LOTLABEL)` (see [lot note][ledger: lot notes]) -hledger does not yet do anything with this lot syntax, except to preserve it and show it in `print`'s `txt`, `beancount`, and `json` output. -This means you can use this syntax in your hledger journals (plus an amountless extra posting to help transactions balance, if needed), -then use the `print` command to export to Ledger or Beancount or rustledger, to use their lots/gains reports -(see [Export Lots workflow](workflows.md#more-advanced-workflows)). +hledger 1 does not provide automated lot/gains tracking, and ignores these cost basis annotations. +But it will preserve and show them in `print`'s `txt`, `beancount`, and `json` output. +This means you can use this syntax in your journal (perhaps with an amountless extra posting to help transactions balance, if needed), +then export to Ledger or Beancount or rustledger to do lots/gains reporting. +See [Export Lots workflow](workflows.md#more-advanced-workflows). + +(hledger 2.x has lots/gains reporting built in. +It also supports a more convenient Beancount-like `{DATE, "LABEL", COST}` syntax.) ## Balance assertions From 5404cfc50a13a30cede3253e435d9d9c730423e2 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Mon, 20 Apr 2026 12:36:42 -1000 Subject: [PATCH 04/15] fix: drop the transaction balancing exception for Gain postings This was backported recently from the hledger2 branch to better match hledger 2's behaviour and allow easier interop on the same journal files. Since then hledger 2 has switched to a better strategy. So we can revert and pretend this never happened. The Gain account type itself remains; and also some defensive changes making reports ignore postings which have had no amount inferred for them. This shouldn't happen any more with the Gain balancing exception gone, but I have left those changes in, on the assumption that if it arises again it's better to omit such postings than show a report with broken layout. --- hledger-lib/Hledger/Data/Balancing.hs | 24 ++++++------------- hledger/hledger.m4.md | 11 +-------- .../test/journal/transaction-balancing.test | 12 ---------- 3 files changed, 8 insertions(+), 39 deletions(-) delete mode 100644 hledger/test/journal/transaction-balancing.test diff --git a/hledger-lib/Hledger/Data/Balancing.hs b/hledger-lib/Hledger/Data/Balancing.hs index 717925163e0..aeb944c0dd6 100644 --- a/hledger-lib/Hledger/Data/Balancing.hs +++ b/hledger-lib/Hledger/Data/Balancing.hs @@ -51,7 +51,7 @@ import Safe (headErr) import Text.Printf (printf) import Hledger.Data.Types -import Hledger.Data.AccountName (accountNameType, isAccountNamePrefixOf) +import Hledger.Data.AccountName (isAccountNamePrefixOf) import Hledger.Data.Amount import Hledger.Data.Journal import Hledger.Data.Posting @@ -66,7 +66,7 @@ data BalancingOpts = BalancingOpts -- Distinct from InputOpts{infer_costs_}. , commodity_styles_ :: Maybe (M.Map CommoditySymbol AmountStyle) -- ^ commodity display styles , txn_balancing_ :: TransactionBalancingPrecision - , account_types_ :: M.Map AccountName AccountType -- ^ account type map, used to exclude Gain postings from balancing + , account_types_ :: M.Map AccountName AccountType -- ^ account type map, passed through for any balancing helpers that need it } deriving (Eq, Ord, Show) defbalancingopts :: BalancingOpts @@ -78,12 +78,6 @@ defbalancingopts = BalancingOpts , account_types_ = M.empty } --- | Is this posting to a Gain-type account ? --- Used to exclude gain/loss postings from normal transaction balancing, --- since gain postings balance at cost basis, not at selling price. -isGainPosting :: M.Map AccountName AccountType -> Posting -> Bool -isGainPosting atypes p = accountNameType atypes (paccount p) == Just Gain - -- | Check that this transaction would appear balanced to a human when displayed. -- On success, returns the empty list, otherwise one or more error messages. -- @@ -102,13 +96,10 @@ isGainPosting atypes p = accountNameType atypes (paccount p) == Just Gain -- (using the given display styles if provided) -- transactionCheckBalanced :: BalancingOpts -> Transaction -> [String] -transactionCheckBalanced BalancingOpts{commodity_styles_=_mglobalstyles, txn_balancing_, account_types_} t = errs +transactionCheckBalanced BalancingOpts{commodity_styles_=_mglobalstyles, txn_balancing_} t = errs where - -- In disposal transactions, gain postings are excluded from normal balancing - isGain = isGainPosting account_types_ - -- get real and balanced virtual postings, to be checked separately - (rps, bvps) = foldr partitionPosting ([], []) $ filter (not . isGain) $ tpostings t + (rps, bvps) = foldr partitionPosting ([], []) $ tpostings t where partitionPosting p ~(l, r) = case ptype p of RegularPosting -> (p:l, r) @@ -253,10 +244,10 @@ transactionBalanceError t errs = printf "%s:\n%s\n\nThis %stransaction is unbala -- have the same price(s), and will be converted to the price commodity. transactionInferBalancingAmount :: M.Map CommoditySymbol AmountStyle -- ^ commodity display styles - -> M.Map AccountName AccountType -- ^ account type map (for excluding Gain postings) + -> M.Map AccountName AccountType -- ^ account type map (passed through; reserved for future use) -> Transaction -> Either String (Transaction, [(AccountName, MixedAmount)]) -transactionInferBalancingAmount styles atypes t@Transaction{tpostings=ps} +transactionInferBalancingAmount styles _atypes t@Transaction{tpostings=ps} | length amountlessrealps > 1 = Left $ transactionBalanceError t ["There can't be more than one real posting with no amount." @@ -275,8 +266,7 @@ transactionInferBalancingAmount styles atypes t@Transaction{tpostings=ps} ) where lbl = lbl_ "transactionInferBalancingAmount" - isGain = isGainPosting atypes - (amountfulrealps, amountlessrealps) = partition hasAmount (filter (not . isGain) $ realPostings t) + (amountfulrealps, amountlessrealps) = partition hasAmount (realPostings t) realsum = sumPostings amountfulrealps -- & dbg9With (lbl "real balancing amount".showMixedAmountOneLine) (amountfulbvps, amountlessbvps) = partition hasAmount (balancedVirtualPostings t) diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 262979dca57..89269181ece 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -1972,15 +1972,6 @@ If you hit this problem, it's easy to fix: 2. or make non-cost amounts less precise (remove unnecessary decimal digits that are raising the precision) 3. or add a posting to absorb the imbalance (eg "expenses:rounding". Remember that one posting may [omit the amount](#postings); that's convenient here.) -### Gain postings - -The `Gain` [account type](#account-types) has a special behaviour: -postings to Gain-type accounts are ignored by transaction balancing. - -This is useful in hledger 2, as part of its automated lots/gains tracking. -Gain postings are usually not used in hledger 1; but they are supported for better compatibility with hledger 2. -This makes it easier to keep a journal compatible with both hledger versions. - ## Tags @@ -2324,7 +2315,7 @@ hledger also uses a few subtypes: As a convenience, hledger will detect most of these types automatically from english account names. -(All except Gain. For more about Gain, see [Gain postings](#gain-postings).) +(All except Gain, which must be declared explicitly.) But it's better to declare them explicitly by adding a `type:` [tag](#tags) in the account directives. The tag's value can be any of the types or one-letter abbreviations above. diff --git a/hledger/test/journal/transaction-balancing.test b/hledger/test/journal/transaction-balancing.test deleted file mode 100644 index d13a2e25f45..00000000000 --- a/hledger/test/journal/transaction-balancing.test +++ /dev/null @@ -1,12 +0,0 @@ -# * Transaction balancing - -# ** 1. Gain-type postings are excluded from transaction balancing. -< -account revenue:gains ; type:G - -2026-01-01 test - assets $100 - expenses -$100 - revenue:gains -$50 - -$ hledger -f- check balanced From eb14b70cc4abde39f0c6525543cd7cf0ed135433 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Mon, 20 Apr 2026 13:08:39 -1000 Subject: [PATCH 05/15] imp: add the U/UnrealisedGain account type from hledger 2, also U/UnrealisedGain is a subtype of Equity. Like G/Gain, it's added here just to allow hledger 1.x to read hledger 2's journal files. For this one, the type: tag will accept any of these spellings: U, UnrealisedGain, UnrealizedGain. --- hledger-lib/Hledger/Data/Types.hs | 53 +++++++++++++---------- hledger-lib/Hledger/Query.hs | 41 ++++++++++-------- hledger-lib/Hledger/Read/JournalReader.hs | 38 ++++++++-------- hledger/hledger.m4.md | 5 ++- 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/hledger-lib/Hledger/Data/Types.hs b/hledger-lib/Hledger/Data/Types.hs index 6557a983f1b..44945a5308f 100644 --- a/hledger-lib/Hledger/Data/Types.hs +++ b/hledger-lib/Hledger/Data/Types.hs @@ -187,20 +187,22 @@ data AccountType = | Equity | Revenue | Expense - | Cash -- ^ a subtype of Asset - liquid assets to show in cashflow report - | Conversion -- ^ a subtype of Equity - account with which to balance commodity conversions - | Gain -- ^ a subtype of Revenue - capital gains/losses + | Cash -- ^ a subtype of Asset - liquid assets to show in cashflow report + | Conversion -- ^ a subtype of Equity - account with which to balance commodity conversions + | Gain -- ^ a subtype of Revenue - realised capital gains/losses + | UnrealisedGain -- ^ a subtype of Equity - accumulated unrealised capital gains/losses (used by hledger 2) deriving (Eq,Ord,Generic) instance Show AccountType where - show Asset = "A" - show Liability = "L" - show Equity = "E" - show Revenue = "R" - show Expense = "X" - show Cash = "C" - show Conversion = "V" - show Gain = "G" + show Asset = "A" + show Liability = "L" + show Equity = "E" + show Revenue = "R" + show Expense = "X" + show Cash = "C" + show Conversion = "V" + show Gain = "G" + show UnrealisedGain = "U" isBalanceSheetAccountType :: AccountType -> Bool isBalanceSheetAccountType t = t `elem` [ @@ -208,7 +210,8 @@ isBalanceSheetAccountType t = t `elem` [ Liability, Equity, Cash, - Conversion + Conversion, + UnrealisedGain ] isIncomeStatementAccountType :: AccountType -> Bool @@ -221,18 +224,20 @@ isIncomeStatementAccountType t = t `elem` [ -- | Check whether the first argument is a subtype of the second: either equal -- or one of the defined subtypes. isAccountSubtypeOf :: AccountType -> AccountType -> Bool -isAccountSubtypeOf Asset Asset = True -isAccountSubtypeOf Liability Liability = True -isAccountSubtypeOf Equity Equity = True -isAccountSubtypeOf Revenue Revenue = True -isAccountSubtypeOf Expense Expense = True -isAccountSubtypeOf Cash Cash = True -isAccountSubtypeOf Cash Asset = True -isAccountSubtypeOf Conversion Conversion = True -isAccountSubtypeOf Conversion Equity = True -isAccountSubtypeOf Gain Gain = True -isAccountSubtypeOf Gain Revenue = True -isAccountSubtypeOf _ _ = False +isAccountSubtypeOf Asset Asset = True +isAccountSubtypeOf Liability Liability = True +isAccountSubtypeOf Equity Equity = True +isAccountSubtypeOf Revenue Revenue = True +isAccountSubtypeOf Expense Expense = True +isAccountSubtypeOf Cash Cash = True +isAccountSubtypeOf Cash Asset = True +isAccountSubtypeOf Conversion Conversion = True +isAccountSubtypeOf Conversion Equity = True +isAccountSubtypeOf Gain Gain = True +isAccountSubtypeOf Gain Revenue = True +isAccountSubtypeOf UnrealisedGain UnrealisedGain = True +isAccountSubtypeOf UnrealisedGain Equity = True +isAccountSubtypeOf _ _ = False -- not worth the trouble, letters defined in accountdirectivep for now --instance Read AccountType diff --git a/hledger-lib/Hledger/Query.hs b/hledger-lib/Hledger/Query.hs index dbef1f3bea6..8fcf07b660b 100644 --- a/hledger-lib/Hledger/Query.hs +++ b/hledger-lib/Hledger/Query.hs @@ -519,8 +519,8 @@ accountTypeChoices :: Bool -> String accountTypeChoices allowlongform = intercalate ", " -- keep synced with parseAccountType - $ ["A","L","E","R","X","C","V","G"] - ++ if allowlongform then ["Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain"] else [] + $ ["A","L","E","R","X","C","V","G","U"] + ++ if allowlongform then ["Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain","UnrealisedGain"] else [] -- | Case-insensitively parse one single-letter code, or one long-form word if permitted, to an account type. -- On failure, returns the unparseable text. @@ -528,23 +528,26 @@ parseAccountType :: Bool -> Text -> Either String AccountType parseAccountType allowlongform s = case T.toLower s of -- keep synced with accountTypeChoices - "a" -> Right Asset - "l" -> Right Liability - "e" -> Right Equity - "r" -> Right Revenue - "x" -> Right Expense - "c" -> Right Cash - "v" -> Right Conversion - "g" -> Right Gain - "asset" | allowlongform -> Right Asset - "liability" | allowlongform -> Right Liability - "equity" | allowlongform -> Right Equity - "revenue" | allowlongform -> Right Revenue - "expense" | allowlongform -> Right Expense - "cash" | allowlongform -> Right Cash - "conversion" | allowlongform -> Right Conversion - "gains" | allowlongform -> Right Gain - _ -> Left $ T.unpack s + "a" -> Right Asset + "l" -> Right Liability + "e" -> Right Equity + "r" -> Right Revenue + "x" -> Right Expense + "c" -> Right Cash + "v" -> Right Conversion + "g" -> Right Gain + "u" -> Right UnrealisedGain + "asset" | allowlongform -> Right Asset + "liability" | allowlongform -> Right Liability + "equity" | allowlongform -> Right Equity + "revenue" | allowlongform -> Right Revenue + "expense" | allowlongform -> Right Expense + "cash" | allowlongform -> Right Cash + "conversion" | allowlongform -> Right Conversion + "gain" | allowlongform -> Right Gain + "unrealisedgain" | allowlongform -> Right UnrealisedGain + "unrealizedgain" | allowlongform -> Right UnrealisedGain + _ -> Left $ T.unpack s -- | Parse the value part of a "status:" query, or return an error. parseStatus :: T.Text -> Either String Status diff --git a/hledger-lib/Hledger/Read/JournalReader.hs b/hledger-lib/Hledger/Read/JournalReader.hs index a95dc9e4831..1fcb9167775 100644 --- a/hledger-lib/Hledger/Read/JournalReader.hs +++ b/hledger-lib/Hledger/Read/JournalReader.hs @@ -549,26 +549,28 @@ accountTypeTagName = "type" parseAccountTypeCode :: Text -> Either String AccountType parseAccountTypeCode s = case T.toLower s of - "asset" -> Right Asset - "a" -> Right Asset - "liability" -> Right Liability - "l" -> Right Liability - "equity" -> Right Equity - "e" -> Right Equity - "revenue" -> Right Revenue - "r" -> Right Revenue - "expense" -> Right Expense - "x" -> Right Expense - "cash" -> Right Cash - "c" -> Right Cash - "conversion" -> Right Conversion - "v" -> Right Conversion - "gains" -> Right Gain - "g" -> Right Gain - _ -> Left err + "asset" -> Right Asset + "a" -> Right Asset + "liability" -> Right Liability + "l" -> Right Liability + "equity" -> Right Equity + "e" -> Right Equity + "revenue" -> Right Revenue + "r" -> Right Revenue + "expense" -> Right Expense + "x" -> Right Expense + "cash" -> Right Cash + "c" -> Right Cash + "conversion" -> Right Conversion + "v" -> Right Conversion + "gain" -> Right Gain + "g" -> Right Gain + "unrealisedgain" -> Right UnrealisedGain + "u" -> Right UnrealisedGain + _ -> Left err where err = T.unpack $ "invalid account type code "<>s<>", should be one of " <> - T.intercalate ", " ["A","L","E","R","X","C","V","G","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain"] + T.intercalate ", " ["A","L","E","R","X","C","V","G","U","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain","UnrealisedGain"] -- Add an account declaration to the journal, auto-numbering it. addAccountDeclaration :: (AccountName,Text,[Tag],SourcePos) -> JournalParser m () diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 89269181ece..7939bb52806 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -2311,14 +2311,17 @@ hledger also uses a few subtypes: | `Cash` | `C` | liquid assets (subtype of Asset) | | `Conversion` | `V` | commodity conversions equity (subtype of Equity) | | `Gain` | `G` | capital gains/losses (subtype of Revenue) | +| `UnrealisedGain` | `U` | accumulated unrealised capital gains/losses (subtype of Equity) | As a convenience, hledger will detect most of these types automatically from english account names. -(All except Gain, which must be declared explicitly.) +(All except Gain and UnrealisedGain, which must be declared explicitly.) But it's better to declare them explicitly by adding a `type:` [tag](#tags) in the account directives. The tag's value can be any of the types or one-letter abbreviations above. +hledger 1 does nothing special with `Gain` and `UnrealisedGain` accounts; they are supported mainly so that journals written for hledger 2 (which uses them for lot/gain tracking) can be read without errors. + Here is a typical set of account type declarations. Subaccounts will inherit their parent's type, or can override it: From 2ee53a404eb1990ac3b3e1c8b1a0b61e5f08bad7 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 28 Apr 2026 10:04:44 -1000 Subject: [PATCH 06/15] ;doc: changelogs --- doc/CHANGES.md | 2 ++ hledger-lib/CHANGES.md | 2 ++ hledger-ui/CHANGES.md | 9 +++++++++ hledger-web/CHANGES.md | 7 +++++++ hledger/CHANGES.md | 16 ++++++++++++++++ 5 files changed, 36 insertions(+) diff --git a/doc/CHANGES.md b/doc/CHANGES.md index eb8f98536bc..4bd3d61dc3d 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -18,6 +18,8 @@ General changes in the hledger project. For package-specific changes, see the hledger package changelogs. +# b2fe1201 + # 1.52 2026-03-20 Doc updates diff --git a/hledger-lib/CHANGES.md b/hledger-lib/CHANGES.md index 7a13becf845..2034fdc8aaa 100644 --- a/hledger-lib/CHANGES.md +++ b/hledger-lib/CHANGES.md @@ -17,6 +17,8 @@ API/developer-ish changes in hledger-lib. For user-visible changes, see the hledger package changelog. +# b2fe1201 + # 1.52 2026-03-20 Breaking changes diff --git a/hledger-ui/CHANGES.md b/hledger-ui/CHANGES.md index 3eac36a0247..2d9a906b1ca 100644 --- a/hledger-ui/CHANGES.md +++ b/hledger-ui/CHANGES.md @@ -23,6 +23,15 @@ User-visible changes in hledger-ui. See also the hledger changelog. +# b2fe1201 + +Improvements + +- Uses hledger 1.52.1. + +- Allow vty-crossplatform 0.5. + + # 1.52 2026-03-20 Fixes diff --git a/hledger-web/CHANGES.md b/hledger-web/CHANGES.md index b4a13a19a81..52be3d6e606 100644 --- a/hledger-web/CHANGES.md +++ b/hledger-web/CHANGES.md @@ -23,6 +23,13 @@ User-visible changes in hledger-web. See also the hledger changelog. +# b2fe1201 + +Improvements + +- Uses hledger 1.52.1. + + # 1.52 2026-03-20 Fixes diff --git a/hledger/CHANGES.md b/hledger/CHANGES.md index 080c77c8bce..b77fd256008 100644 --- a/hledger/CHANGES.md +++ b/hledger/CHANGES.md @@ -26,6 +26,22 @@ API User-visible changes in the hledger command line tool and library. +# b2fe1201 + +Breaking changes + +- The `Gain` (`G`) account type is no longer auto-detected from account names like + `revenue:gains` or `income:capital gains`; it must now be declared explicitly with `; type: G`. + Also, the `UnrealisedGain` (`U`) account type, a subtype of Equity, has been added + (spellings `U`, `UnrealisedGain`, and `UnrealizedGain` are all accepted). + These improve compatibility between hledger 1 and 2. + +Docs + +- The "Cost basis / lot syntax" section has been renamed to "Cost basis", + and updated to mention hledger 2. + + # 1.52 2026-03-20 Features From 403e87e1514f20329819e8e1a00faa6f3508e9be Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 28 Apr 2026 11:20:57 -1000 Subject: [PATCH 07/15] ;doc: finalise changelogs for 1.52.1 on 2026-04-28 --- doc/CHANGES.md | 2 +- hledger-lib/CHANGES.md | 2 +- hledger-ui/CHANGES.md | 2 +- hledger-web/CHANGES.md | 2 +- hledger/CHANGES.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 4bd3d61dc3d..f95788effe4 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -18,7 +18,7 @@ General changes in the hledger project. For package-specific changes, see the hledger package changelogs. -# b2fe1201 +# 1.52.1 2026-04-28 # 1.52 2026-03-20 diff --git a/hledger-lib/CHANGES.md b/hledger-lib/CHANGES.md index 2034fdc8aaa..7dd40647c7d 100644 --- a/hledger-lib/CHANGES.md +++ b/hledger-lib/CHANGES.md @@ -17,7 +17,7 @@ API/developer-ish changes in hledger-lib. For user-visible changes, see the hledger package changelog. -# b2fe1201 +# 1.52.1 2026-04-28 # 1.52 2026-03-20 diff --git a/hledger-ui/CHANGES.md b/hledger-ui/CHANGES.md index 2d9a906b1ca..8026f654492 100644 --- a/hledger-ui/CHANGES.md +++ b/hledger-ui/CHANGES.md @@ -23,7 +23,7 @@ User-visible changes in hledger-ui. See also the hledger changelog. -# b2fe1201 +# 1.52.1 2026-04-28 Improvements diff --git a/hledger-web/CHANGES.md b/hledger-web/CHANGES.md index 52be3d6e606..0fb8b96b0a9 100644 --- a/hledger-web/CHANGES.md +++ b/hledger-web/CHANGES.md @@ -23,7 +23,7 @@ User-visible changes in hledger-web. See also the hledger changelog. -# b2fe1201 +# 1.52.1 2026-04-28 Improvements diff --git a/hledger/CHANGES.md b/hledger/CHANGES.md index b77fd256008..62f2cfd603b 100644 --- a/hledger/CHANGES.md +++ b/hledger/CHANGES.md @@ -26,7 +26,7 @@ API User-visible changes in the hledger command line tool and library. -# b2fe1201 +# 1.52.1 2026-04-28 Breaking changes From caa667a221cdaca428f4db434a9b7a23c8f0c985 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 28 Apr 2026 11:31:41 -1000 Subject: [PATCH 08/15] ;doc: relnotes --- doc/relnotes.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/doc/relnotes.md b/doc/relnotes.md index 8ad44b07dd7..421a0afd88a 100644 --- a/doc/relnotes.md +++ b/doc/relnotes.md @@ -63,9 +63,53 @@ Major releases and user-visible changes. + +## 2026-04-28 hledger-1.52.1 + +### hledger 1.52.1 + + +Breaking changes + +- The `Gain` (`G`) account type is no longer auto-detected from account names like + `revenue:gains` or `income:capital gains`; it must now be declared explicitly with `; type: G`. + Also, the `UnrealisedGain` (`U`) account type, a subtype of Equity, has been added + (spellings `U`, `UnrealisedGain`, and `UnrealizedGain` are all accepted). + These improve compatibility between hledger 1 and 2. + +Docs + +- The "Cost basis / lot syntax" section has been renamed to "Cost basis", + and updated to mention hledger 2. + + +### hledger-ui 1.52.1 + + +Improvements + +- Uses hledger 1.52.1. + +- Allow vty-crossplatform 0.5. + + +### hledger-web 1.52.1 + + +Improvements + +- Uses hledger 1.52.1. + + +### credits 1.52.1 + + +Simon Michael. + + + ## 2026-03-20 hledger-1.52 -**Preserve cost basis syntax, cost basis export examples, faster valuation, commodity tags, more robust paging, fixes.** ### hledger 1.52 @@ -289,6 +333,7 @@ n0vdd. + ## 2026-01-08 hledger-1.51.2 ### hledger 1.51.2 @@ -11046,3 +11091,5 @@ Release stats: + + From 7d936484be1072e7d0fbaf50e6c913ff0cbad64c Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Fri, 27 Mar 2026 21:43:01 -1000 Subject: [PATCH 09/15] ;doc:ANNOUNCE:edits --- doc/ANNOUNCE | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/doc/ANNOUNCE b/doc/ANNOUNCE index dec56f4016e..3369f90eda6 100644 --- a/doc/ANNOUNCE +++ b/doc/ANNOUNCE @@ -1,11 +1,9 @@ -I'm pleased to announce hledger-1.52. Highlights include: +I'm pleased to announce hledger-1.52. Highlights: -Preserve cost basis syntax, -cost basis export examples, -faster valuation, -commodity tags, more robust paging, -fixes. +faster valuation, +commodity tags; +cost basis annotations are preserved and can be exported (examples provided). - https://github.com/simonmichael/hledger/releases/1.52 - https://hledger.org/relnotes.html#2026-03-20-hledger-152 From 55b9cf277710c90ac2e47469784a16e72de3c47d Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 28 Apr 2026 11:33:12 -1000 Subject: [PATCH 10/15] ;doc: announce --- doc/ANNOUNCE | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/doc/ANNOUNCE b/doc/ANNOUNCE index 3369f90eda6..646a26fd0c6 100644 --- a/doc/ANNOUNCE +++ b/doc/ANNOUNCE @@ -1,12 +1,9 @@ -I'm pleased to announce hledger-1.52. Highlights: +I'm pleased to announce hledger-1.52.1. +This is a small release adjusting the G account type and adding the U account type, +for better compatibility between hledger 1 and 2. -more robust paging, -faster valuation, -commodity tags; -cost basis annotations are preserved and can be exported (examples provided). - -- https://github.com/simonmichael/hledger/releases/1.52 -- https://hledger.org/relnotes.html#2026-03-20-hledger-152 +- https://github.com/simonmichael/hledger/releases/1.52.1 +- https://hledger.org/relnotes.html#2026-04-28-hledger-1521 - https://hledger.org/install #hledger is free, robust, friendly, fast, cross-platform, double-entry, multi-currency, multi-interface, GNU GPL-licensed, #plaintextaccounting software. From 4da8b3af09bb03dd7dee662a55b351ce709701c1 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 28 Apr 2026 11:34:45 -1000 Subject: [PATCH 11/15] ;doc: gh notes version bumps --- doc/ghrelnotes | 2 +- doc/ghtestbinnotes.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ghrelnotes b/doc/ghrelnotes index 658fb105cf8..f7effa1904e 100755 --- a/doc/ghrelnotes +++ b/doc/ghrelnotes @@ -121,7 +121,7 @@ In a powershell window (press `WINDOWS-R`, `powershell`, `ENTER`): ``` cd ~ -curl https://github.com/simonmichael/hledger/releases/download/1.52/hledger-windows-x64.zip -OutFile hledger-windows-x64.zip +curl https://github.com/simonmichael/hledger/releases/download/1.52.1/hledger-windows-x64.zip -OutFile hledger-windows-x64.zip Expand-Archive hledger-windows-x64.zip -Force -DestinationPath AppData\Roaming\local\bin hledger --version; hledger-ui --version; hledger-web --version # should show $REL ``` diff --git a/doc/ghtestbinnotes.md b/doc/ghtestbinnotes.md index e69a41346fe..a98bc30e949 100644 --- a/doc/ghtestbinnotes.md +++ b/doc/ghtestbinnotes.md @@ -1,6 +1,6 @@ Here you can find unreleased test binaries, which are updated occasionally. By using these and providing [feedback](https://hledger.org/support.html), you can help to make the next release better. -These binaries have the following changes since the 1.52 release: https://github.com/simonmichael/hledger/compare/1.52...testlatest +These binaries have the following changes since the 1.52.1 release: https://github.com/simonmichael/hledger/compare/1.52.1...testlatest
From 141cc2d12c2ed17df3ec47fec0a0e8e34d8b5cb8 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Tue, 28 Apr 2026 12:56:24 -1000 Subject: [PATCH 12/15] ;tools: just devver: update --- Justfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Justfile b/Justfile index 24f3325f475..3963fee2334 100644 --- a/Justfile +++ b/Justfile @@ -1066,9 +1066,9 @@ ghrel-upload: @devver: #!/usr/bin/env bash set -euo pipefail - RELVER=$(just relver) + RELVER=$(just majorver) DEVVER=$RELVER.99 - just _on-master-branch + just _on-trunk echo "Setting versions to $DEVVER.." ./Shake setversion "$DEVVER" -c @@ -1473,13 +1473,13 @@ _on-release-branch: exit 1 fi -# Check that we're on the master branch. -_on-master-branch: +# Check that we're on the hledger1 or main branch. +_on-trunk: #!/usr/bin/env bash set -euo pipefail BRANCH=$(git branch --show-current) - if [[ ! $BRANCH =~ master ]]; then - echo "You are currently on $BRANCH branch. Please switch to the master branch." + if [[ ! $BRANCH =~ hledger1 && ! $BRANCH =~ main ]]; then + echo "You are currently on $BRANCH branch. Please switch to the hledger1 or main branch." exit 1 fi From d04ce04a563ed56cde7f248327b972ebade2cdee Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Mon, 27 Apr 2026 22:42:41 -1000 Subject: [PATCH 13/15] ;pkg:cabal: avoid ram package and yesod-static#1917 --- cabal.project | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cabal.project b/cabal.project index c9da98760f9..825241fb71e 100644 --- a/cabal.project +++ b/cabal.project @@ -23,3 +23,6 @@ write-ghc-environment-files: always -- package * -- -- Speeds up a bit -- library-vanilla: False + +-- avoid https://github.com/yesodweb/yesod/issues/1917 +constraints: ram < 0 From 8308b24324c529a1908547e16db069b23601df54 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Fri, 17 Apr 2026 18:20:26 -1000 Subject: [PATCH 14/15] ;pkg:ui: allow brick <3 --- hledger-ui/hledger-ui.cabal | 2 +- hledger-ui/package.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hledger-ui/hledger-ui.cabal b/hledger-ui/hledger-ui.cabal index d2d37a10bbd..7cfbc70eb2c 100644 --- a/hledger-ui/hledger-ui.cabal +++ b/hledger-ui/hledger-ui.cabal @@ -74,7 +74,7 @@ library ansi-terminal >=0.9 , async , base >=4.18 && <4.23 - , brick >=2.1.1 && <2.3.2 || >2.3.2 && <2.11 + , brick >=2.1.1 && <2.3.2 || >2.3.2 && <3 , cmdargs >=0.8 , containers >=0.5.9 , data-default diff --git a/hledger-ui/package.yaml b/hledger-ui/package.yaml index 257bde42f5f..90966efb724 100644 --- a/hledger-ui/package.yaml +++ b/hledger-ui/package.yaml @@ -94,7 +94,7 @@ library: - time >=1.5 - transformers - vector - - brick >=2.1.1 && <2.3.2 || >2.3.2 && < 2.11 + - brick >=2.1.1 && <2.3.2 || >2.3.2 && <3 - vty >=6.1 && <6.6 - vty-crossplatform >= 0.4.0.0 && < 0.5.0.0 when: From 7e4eb8518caf2cf2f9f4b9c2b7ac0e97d2013b96 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sat, 24 Jan 2026 00:45:06 +0300 Subject: [PATCH 15/15] feat: areg: Add --matching flag Works like the inverse of the --related flag on register. --- hledger/Hledger/Cli/Commands/Aregister.hs | 65 +++++++++++++---------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Aregister.hs b/hledger/Hledger/Cli/Commands/Aregister.hs index a5cc5b31d5e..0f1be897a7e 100644 --- a/hledger/Hledger/Cli/Commands/Aregister.hs +++ b/hledger/Hledger/Cli/Commands/Aregister.hs @@ -56,6 +56,7 @@ aregistermode = hledgerCommandMode -- ,flagNone ["average","A"] (setboolopt "average") -- "show running average of posting amounts instead of total (implies --empty)" -- ,flagNone ["related","r"] (setboolopt "related") "show postings' siblings instead" + ,flagNone ["matching"] (setboolopt "matching") "show the matched account instead of related accounts" ,flagNone ["invert"] (setboolopt "invert") "display all amounts with reversed sign" ,flagReq ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "omit N leading account name parts" ,flagReq ["heading"] (\s opts -> Right $ setopt "heading" s opts) "YN" @@ -119,15 +120,16 @@ aregister opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j = do styleAmounts (journalCommodityStylesWith HardRounding j) $ (if empty_ ropts' then id else filter (not . mixedAmountLooksZero . fifth6)) $ reverse items + matching = boolopt "matching" rawopts -- select renderer - render | fmt=="txt" = accountTransactionsReportAsText opts (_rsQuery rspec') thisacctq - | fmt=="html" = accountTransactionsReportAsHTML opts (_rsQuery rspec') thisacctq - | fmt=="csv" = printCSV . accountTransactionsReportAsCsv opts hd wd (_rsQuery rspec') thisacctq - | fmt=="tsv" = printTSV . accountTransactionsReportAsCsv opts hd wd (_rsQuery rspec') thisacctq - | fmt=="fods" = - printFods IO.localeEncoding . Map.singleton "Aregister" . - (,) (1,0) . - accountTransactionsReportAsSpreadsheet opts oneLineNoCostFmt hd wd (_rsQuery rspec') thisacctq + render | fmt=="txt" = accountTransactionsReportAsText opts (_rsQuery rspec') thisacctq matching + | fmt=="html" = accountTransactionsReportAsHTML opts (_rsQuery rspec') thisacctq matching + | fmt=="csv" = printCSV . accountTransactionsReportAsCsv opts hd wd (_rsQuery rspec') thisacctq matching + | fmt=="tsv" = printTSV . accountTransactionsReportAsCsv opts hd wd (_rsQuery rspec') thisacctq matching + | fmt=="fods" = + printFods IO.localeEncoding . Map.singleton "Aregister" . + (,) (1,0) . + accountTransactionsReportAsSpreadsheet opts oneLineNoCostFmt hd wd (_rsQuery rspec') thisacctq matching | fmt=="json" = toJsonText | otherwise = error' $ unsupportedOutputFormatError fmt -- PARTIAL: where @@ -137,39 +139,40 @@ aregister opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j = do writeOutputLazyText opts $ render items' accountTransactionsReportAsCsv :: - CliOpts -> Bool -> WhichDate -> Query -> Query -> AccountTransactionsReport -> CSV -accountTransactionsReportAsCsv opts hd wd reportq thisacctq = + CliOpts -> Bool -> WhichDate -> Query -> Query -> Bool -> AccountTransactionsReport -> CSV +accountTransactionsReportAsCsv opts hd wd reportq thisacctq matching = Spr.rawTableContent . - accountTransactionsReportAsSpreadsheet opts machineFmt hd wd reportq thisacctq + accountTransactionsReportAsSpreadsheet opts machineFmt hd wd reportq thisacctq matching accountTransactionsReportAsSpreadsheet :: CliOpts -> AmountFormat -> Bool -> - WhichDate -> Query -> Query -> AccountTransactionsReport -> + WhichDate -> Query -> Query -> Bool -> AccountTransactionsReport -> [[Spr.Cell Spr.NumLines Text]] -accountTransactionsReportAsSpreadsheet opts fmt hd wd reportq thisacctq is = +accountTransactionsReportAsSpreadsheet opts fmt hd wd reportq thisacctq matching is = optional hd [Spr.addHeaderBorders $ map Spr.headerCell $ ["txnidx","date","code","description","otheraccounts","change","balance"]] ++ - map (accountTransactionsReportItemAsRecord opts fmt True wd reportq thisacctq) is + map (accountTransactionsReportItemAsRecord opts fmt True wd reportq thisacctq matching) is accountTransactionsReportItemAsRecord :: CliOpts -> AmountFormat -> Bool -> - WhichDate -> Query -> Query -> AccountTransactionsReportItem -> + WhichDate -> Query -> Query -> Bool -> AccountTransactionsReportItem -> [Spr.Cell Spr.NumLines Text] accountTransactionsReportItemAsRecord - opts fmt internals wd reportq thisacctq - (t@Transaction{tindex,tcode,tdescription}, _, _issplit, otheraccts, change, balance) + opts fmt internals wd reportq thisacctq matching + (t@Transaction{tindex,tcode,tdescription,tpostings}, _, _issplit, otheraccts, change, balance) = (optional internals [Spr.integerCell tindex]) ++ date : (optional internals [cell tcode]) ++ [cell tdescription, - cell $ T.intercalate ", " $ map dropAcct $ nub otheraccts, + cell $ if matching then matchedAcct else T.intercalate ", " $ map dropAcct $ nub otheraccts, amountCell change, amountCell balance] where dropAcct = accountNameDrop (fromMaybe 0 $ readMay =<< maybestringopt "drop" (rawopts_ opts)) cell = Spr.defaultCell + matchedAcct = T.intercalate ", " . nub $ map paccount $ filter (matchesPosting thisacctq) tpostings date = (Spr.defaultCell $ showDate $ transactionRegisterDate wd reportq thisacctq t) @@ -178,8 +181,8 @@ accountTransactionsReportItemAsRecord wbToText <$> Spr.cellFromMixedAmount fmt (Spr.Class "amount", amt) -- | Render a register report as a HTML snippet. -accountTransactionsReportAsHTML :: CliOpts -> Query -> Query -> AccountTransactionsReport -> TL.Text -accountTransactionsReportAsHTML copts reportq thisacctq items = +accountTransactionsReportAsHTML :: CliOpts -> Query -> Query -> Bool -> AccountTransactionsReport -> TL.Text +accountTransactionsReportAsHTML copts reportq thisacctq matching items = htmlAsLazyText $ do L.link_ [L.rel_ "stylesheet", L.href_ "hledger.css"] L.table_ $ do @@ -194,17 +197,17 @@ accountTransactionsReportAsHTML copts reportq thisacctq items = accountTransactionsReportItemAsRecord copts oneLineNoCostFmt False (whichDate $ _rsReportOpts $ reportspec_ copts) - reportq thisacctq + reportq thisacctq matching -- | Render a register report as plain text suitable for console output. -accountTransactionsReportAsText :: CliOpts -> Query -> Query -> AccountTransactionsReport -> TL.Text -accountTransactionsReportAsText copts reportq thisacctq items = TB.toLazyText $ +accountTransactionsReportAsText :: CliOpts -> Query -> Query -> Bool -> AccountTransactionsReport -> TL.Text +accountTransactionsReportAsText copts reportq thisacctq matching items = TB.toLazyText $ (optional (headingopt copts) $ title <> TB.singleton '\n') <> - postingsOrTransactionsReportAsText alignAll copts itemAsText itemamt itembal items + postingsOrTransactionsReportAsText alignAll copts (itemAsText matching) itemamt itembal items where alignAll = boolopt "align-all" $ rawopts_ copts - itemAsText = accountTransactionsReportItemAsText copts reportq thisacctq + itemAsText m = accountTransactionsReportItemAsText copts reportq thisacctq m itemamt (_,_,_,_,a,_) = a itembal (_,_,_,_,_,a) = a @@ -241,13 +244,13 @@ optional b x = if b then x else mempty -- Returns a string which can be multi-line, eg if the running balance -- has multiple commodities. -- -accountTransactionsReportItemAsText :: CliOpts -> Query -> Query -> Int -> Int +accountTransactionsReportItemAsText :: CliOpts -> Query -> Query -> Bool -> Int -> Int -> (AccountTransactionsReportItem, [WideBuilder], [WideBuilder]) -> TB.Builder accountTransactionsReportItemAsText copts@CliOpts{reportspec_=ReportSpec{_rsReportOpts=ropts}} - reportq thisacctq preferredamtwidth preferredbalwidth - ((t@Transaction{tdescription}, _, _issplit, otheraccts, _, _), amt, bal) = + reportq thisacctq matching preferredamtwidth preferredbalwidth + ((t@Transaction{tdescription,tpostings}, _, _issplit, otheraccts, _, _), amt, bal) = -- Transaction -- the transaction, unmodified -- Transaction -- the transaction, as seen from the current account -- Bool -- is this a split (more than one posting to other accounts) ? @@ -291,7 +294,11 @@ accountTransactionsReportItemAsText (descwidth, acctwidth) = (w, remaining - 2 - w) where w = fromMaybe ((remaining - 2) `div` 2) mdescwidth - accts = T.intercalate ", " . map (dropAcct . accountSummarisedName) $ nub otheraccts + accts = if matching + then T.intercalate ", " . map (dropAcct . accountSummarisedName) $ nub matchedaccts + else T.intercalate ", " . map (dropAcct . accountSummarisedName) $ nub otheraccts + where + matchedaccts = map paccount $ filter (matchesPosting thisacctq) tpostings -- tests