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 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 diff --git a/doc/ANNOUNCE b/doc/ANNOUNCE index dec56f4016e..646a26fd0c6 100644 --- a/doc/ANNOUNCE +++ b/doc/ANNOUNCE @@ -1,14 +1,9 @@ -I'm pleased to announce hledger-1.52. Highlights include: +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. -Preserve cost basis syntax, -cost basis export examples, -faster valuation, -commodity tags, -more robust paging, -fixes. - -- 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. diff --git a/doc/CHANGES.md b/doc/CHANGES.md index eb8f98536bc..f95788effe4 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. +# 1.52.1 2026-04-28 + # 1.52 2026-03-20 Doc updates 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
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: + + diff --git a/hledger-lib/CHANGES.md b/hledger-lib/CHANGES.md index 7a13becf845..7dd40647c7d 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. +# 1.52.1 2026-04-28 + # 1.52 2026-03-20 Breaking changes 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..aeb944c0dd6 100644 --- a/hledger-lib/Hledger/Data/Balancing.hs +++ b/hledger-lib/Hledger/Data/Balancing.hs @@ -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, passed through for any balancing helpers that need it } deriving (Eq, Ord, Show) defbalancingopts :: BalancingOpts @@ -74,6 +75,7 @@ defbalancingopts = BalancingOpts , infer_balancing_costs_ = True , commodity_styles_ = Nothing , txn_balancing_ = TBPExact + , account_types_ = M.empty } -- | Check that this transaction would appear balanced to a human when displayed. @@ -193,7 +195,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 +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 (passed through; reserved for future use) -> 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." @@ -793,10 +796,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/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-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/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/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-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-ui/CHANGES.md b/hledger-ui/CHANGES.md index 3eac36a0247..8026f654492 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. +# 1.52.1 2026-04-28 + +Improvements + +- Uses hledger 1.52.1. + +- Allow vty-crossplatform 0.5. + + # 1.52 2026-03-20 Fixes 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: diff --git a/hledger-web/CHANGES.md b/hledger-web/CHANGES.md index b4a13a19a81..0fb8b96b0a9 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. +# 1.52.1 2026-04-28 + +Improvements + +- Uses hledger 1.52.1. + + # 1.52 2026-03-20 Fixes diff --git a/hledger/CHANGES.md b/hledger/CHANGES.md index 080c77c8bce..62f2cfd603b 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. +# 1.52.1 2026-04-28 + +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 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/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 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, diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index ad9146f08a3..7939bb52806 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 @@ -2307,13 +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 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: @@ -2329,7 +2337,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/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