From e678e09704c3a0bf79f7eae3d51694dc90f8ab7c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 8 Jan 2026 19:12:39 -0500 Subject: [PATCH] feat: accounts: add Gain (G) account type as subtype of Revenue [#2522] Add a new account type Gain with single-letter code G as a subtype of Revenue, similar to how Cash is a subtype of Asset and Conversion is a subtype of Equity. This enables tracking capital gains/losses separately while still including them in income statements and close --retain. Usage: account revenues:capital ; type: G - type:G matches only Gain accounts - type:R matches both Revenue and Gain (subtype matching) - Auto-detection from account names matching: ^(income|revenue)s?:(capital[- ]?)?(gains?|loss(es)?)(:|$) e.g. income:gains, revenue:capital-gains, income:losses --- hledger-lib/Hledger/Data/AccountName.hs | 10 ++++ hledger-lib/Hledger/Data/Types.hs | 7 ++- hledger-lib/Hledger/Query.hs | 10 ++-- hledger-lib/Hledger/Read/JournalReader.hs | 4 +- hledger/hledger.m4.md | 16 +++--- hledger/test/close.test | 20 ++++++++ hledger/test/journal/account-types.test | 61 +++++++++++++++++++++++ hledger/test/query-type.test | 35 +++++++++++++ 8 files changed, 151 insertions(+), 12 deletions(-) diff --git a/hledger-lib/Hledger/Data/AccountName.hs b/hledger-lib/Hledger/Data/AccountName.hs index 7104d2ef8..237c86e82 100644 --- a/hledger-lib/Hledger/Data/AccountName.hs +++ b/hledger-lib/Hledger/Data/AccountName.hs @@ -30,6 +30,7 @@ module Hledger.Data.AccountName ( ,equityAccountRegex ,conversionAccountRegex ,revenueAccountRegex + ,gainAccountRegex ,expenseAccountRegex ,acctsep ,acctsepchar @@ -99,6 +100,7 @@ liabilityAccountRegex = toRegexCI' "^(debts?|liabilit(y|ies))(:|$)" equityAccountRegex = toRegexCI' "^equity(:|$)" conversionAccountRegex = toRegexCI' "^equity:(trade|trades|trading|conversion)(:|$)" revenueAccountRegex = toRegexCI' "^(income|revenue)s?(:|$)" +gainAccountRegex = toRegexCI' "^(income|revenue)s?:(capital[- ]?)?(gains?|loss(es)?)(:|$)" expenseAccountRegex = toRegexCI' "^expenses?(:|$)" -- | Try to guess an account's type from its name, @@ -110,6 +112,7 @@ 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 @@ -447,6 +450,13 @@ 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 ,testCase "joinAccountNames" $ do joinAccountNames "assets" "cash" @?= "assets:cash" joinAccountNames "assets:cash" "a" @?= "assets:cash:a" diff --git a/hledger-lib/Hledger/Data/Types.hs b/hledger-lib/Hledger/Data/Types.hs index b954d307f..cb44685e9 100644 --- a/hledger-lib/Hledger/Data/Types.hs +++ b/hledger-lib/Hledger/Data/Types.hs @@ -181,6 +181,7 @@ data AccountType = | 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 deriving (Eq,Ord,Generic) instance Show AccountType where @@ -191,6 +192,7 @@ instance Show AccountType where show Expense = "X" show Cash = "C" show Conversion = "V" + show Gain = "G" isBalanceSheetAccountType :: AccountType -> Bool isBalanceSheetAccountType t = t `elem` [ @@ -204,7 +206,8 @@ isBalanceSheetAccountType t = t `elem` [ isIncomeStatementAccountType :: AccountType -> Bool isIncomeStatementAccountType t = t `elem` [ Revenue, - Expense + Expense, + Gain ] -- | Check whether the first argument is a subtype of the second: either equal @@ -219,6 +222,8 @@ 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 -- not worth the trouble, letters defined in accountdirectivep for now diff --git a/hledger-lib/Hledger/Query.hs b/hledger-lib/Hledger/Query.hs index b9760526f..7753fa1ac 100644 --- a/hledger-lib/Hledger/Query.hs +++ b/hledger-lib/Hledger/Query.hs @@ -514,11 +514,11 @@ parseTypeCodes s = help = "type:'s argument should be one or more of " ++ accountTypeChoices False accountTypeChoices :: Bool -> String -accountTypeChoices allowlongform = - intercalate ", " +accountTypeChoices allowlongform = + intercalate ", " -- keep synced with parseAccountType - $ ["A","L","E","R","X","C","V"] - ++ if allowlongform then ["Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"] else [] + $ ["A","L","E","R","X","C","V","G"] + ++ if allowlongform then ["Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain"] 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. @@ -533,6 +533,7 @@ parseAccountType allowlongform s = "x" -> Right Expense "c" -> Right Cash "v" -> Right Conversion + "g" -> Right Gain "asset" | allowlongform -> Right Asset "liability" | allowlongform -> Right Liability "equity" | allowlongform -> Right Equity @@ -540,6 +541,7 @@ parseAccountType allowlongform s = "expense" | allowlongform -> Right Expense "cash" | allowlongform -> Right Cash "conversion" | allowlongform -> Right Conversion + "gains" | allowlongform -> Right Gain _ -> Left $ T.unpack s -- | Parse the value part of a "status:" query, or return an error. diff --git a/hledger-lib/Hledger/Read/JournalReader.hs b/hledger-lib/Hledger/Read/JournalReader.hs index 61cdd9b7f..c64c221f8 100644 --- a/hledger-lib/Hledger/Read/JournalReader.hs +++ b/hledger-lib/Hledger/Read/JournalReader.hs @@ -563,10 +563,12 @@ parseAccountTypeCode s = "c" -> Right Cash "conversion" -> Right Conversion "v" -> Right Conversion + "gains" -> Right Gain + "g" -> Right Gain _ -> Left err where err = T.unpack $ "invalid account type code "<>s<>", should be one of " <> - T.intercalate ", " ["A","L","E","R","X","C","V","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"] + T.intercalate ", " ["A","L","E","R","X","C","V","G","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion","Gain"] -- 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 b0b966262..524882857 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -2212,16 +2212,17 @@ and two more representing changes in these: | `Revenue` | `R` | inflows (also known as `Income`) | | `Expense` | `X` | outflows | -hledger also uses a couple of subtypes: +hledger also uses a few subtypes: |||| |-|-|-| -| `Cash` | `C` | liquid assets | -| `Conversion` | `V` | commodity conversions equity | +| `Cash` | `C` | liquid assets (subtype of Asset) | +| `Conversion` | `V` | commodity conversions equity (subtype of Equity) | +| `Gain` | `G` | capital gains/losses (subtype of Revenue) | -As a convenience, hledger will detect these types automatically from english account names. +As a convenience, hledger will detect most of these types automatically from english account names. 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. @@ -2239,6 +2240,8 @@ account assets:bank ; type: C account assets:cash ; type: C account equity:conversion ; type: V + +account revenues:capital ; type: G ``` This enables the easy [balancesheet], [balancesheetequity], [cashflow] and [incomestatement] reports, and querying by [type:](#queries). @@ -5682,8 +5685,9 @@ Match unmarked, pending, or cleared transactions respectively. **`type:TYPECODES`**\ Match by account type (see [Declaring accounts > Account types](#account-types)). `TYPECODES` is one or more of the single-letter account type codes -`ALERXCV`, case insensitive. -Note `type:A` and `type:E` will also match their respective subtypes `C` (Cash) and `V` (Conversion). +`ALERXCVG`, case insensitive. +Note `type:A`, `type:E`, and `type:R` will also match their respective subtypes +`C` (Cash), `V` (Conversion), and `G` (Gain). Certain kinds of account alias can disrupt account types, see [Rewriting accounts > Aliases and account types](#aliases-and-account-types). diff --git a/hledger/test/close.test b/hledger/test/close.test index d1514df4f..7b1eb3a10 100644 --- a/hledger/test/close.test +++ b/hledger/test/close.test @@ -274,3 +274,23 @@ $ hledger -f- close --clopen -e 2001 --round=hard -c '$1.0' equity:opening/closing balances >= + +# ** 20. With --retain, Gain accounts (subtype of Revenue) are also closed. +< +account revenues ; type:R +account gains ; type:G +account expenses ; type:X + +2016/1/1 + revenues $-100 + gains $-50 + expenses $150 + +$ hledger close -f- -e 2017 --retain +2016-12-31 retain earnings ; retain: + revenues $100 = $0 + gains $50 = $0 + expenses $-150 = $0 + equity:retained earnings + +>=0 diff --git a/hledger/test/journal/account-types.test b/hledger/test/journal/account-types.test index a2c12ebd8..fdb61641b 100644 --- a/hledger/test/journal/account-types.test +++ b/hledger/test/journal/account-types.test @@ -123,6 +123,7 @@ account a:aa:aaa ; type:L (a:aa) 1 (a:aa:aaa) 1 + # ** 6. bs will detect proper accounts even with an intervening parent account (#1921) $ hledger -f- bs -N Balance Sheet 2021-01-01 @@ -136,3 +137,63 @@ Balance Sheet 2021-01-01 -------------++------------ a || -1 a:aa:aaa || -1 + +# ** 7. Gains accounts appear in income statement as revenue subtype +< +account revenues ; type:R +account gains ; type:G +account expenses ; type:X + +2020-01-01 + revenues -100 + gains -50 + expenses 150 + +$ hledger -f- is +Income Statement 2020-01-01 + + || 2020-01-01 +==========++============ + Revenues || +----------++------------ + revenues || 100 + gains || 50 +----------++------------ + || 150 +==========++============ + Expenses || +----------++------------ + expenses || 150 +----------++------------ + || 150 +==========++============ + Net: || 0 + +# ** 8. Gain accounts are auto-detected from common naming patterns +< +2020-01-01 + income:gains -50 + revenue:capital-gains -30 + income:losses -20 + expenses 100 + +$ hledger -f- is +Income Statement 2020-01-01 + + || 2020-01-01 +=======================++============ + Revenues || +-----------------------++------------ + income:gains || 50 + income:losses || 20 + revenue:capital-gains || 30 +-----------------------++------------ + || 100 +=======================++============ + Expenses || +-----------------------++------------ + expenses || 100 +-----------------------++------------ + || 100 +=======================++============ + Net: || 0 diff --git a/hledger/test/query-type.test b/hledger/test/query-type.test index cdb104e1b..cfd6203ec 100644 --- a/hledger/test/query-type.test +++ b/hledger/test/query-type.test @@ -135,3 +135,38 @@ $ hledger -f- reg type:ae 2022-02-02 Test (assets:cash) 1 1 (equity:conversion) 2 3 (equity:conversion) -2 1 + +# ** 16. type:g matches gains accounts +< +account gains ; type:G + +2022-02-02 Test + (gains) 1 + +$ hledger -f- accounts type:g +gains + +# ** 17. type:r matches both revenue and gains (subtype matching) +< +account revenue ; type:R +account gains ; type:G + +2022-02-02 Test + (revenue) 1 + (gains) 1 + +$ 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