1 {-# LANGUAGE RecordWildCards #-} 2 {-| 3 4 Generate several common kinds of report from a journal, as \"*Report\" - 5 simple intermediate data structures intended to be easily rendered as 6 text, html, json, csv etc. by hledger commands, hamlet templates, 7 javascript, or whatever. This is under Hledger.Cli since it depends 8 on the command-line options, should move to hledger-lib later. 9 10 -} 11 12 module Hledger.Reports ( 13 ReportOpts(..), 14 DisplayExp, 15 FormatStr, 16 defreportopts, 17 dateSpanFromOpts, 18 intervalFromOpts, 19 clearedValueFromOpts, 20 whichDateFromOpts, 21 journalSelectingDateFromOpts, 22 journalSelectingAmountFromOpts, 23 optsToFilterSpec, 24 -- * Entries report 25 EntriesReport, 26 EntriesReportItem, 27 entriesReport, 28 -- * Postings report 29 PostingsReport, 30 PostingsReportItem, 31 postingsReport, 32 mkpostingsReportItem, -- XXX for showPostingWithBalanceForVty in Hledger.Cli.Register 33 -- * Transactions report 34 TransactionsReport, 35 TransactionsReportItem, 36 triDate, 37 triBalance, 38 journalTransactionsReport, 39 accountTransactionsReport, 40 -- * Accounts report 41 AccountsReport, 42 AccountsReportItem, 43 accountsReport, 44 accountsReport2, 45 isInteresting, 46 -- * Tests 47 tests_Hledger_Reports 48 ) 49 where 50 51 import Control.Monad 52 import Data.List 53 import Data.Maybe 54 import Data.Ord 55 import Data.Time.Calendar 56 import Data.Tree 57 import Safe (headMay, lastMay) 58 import System.Console.CmdArgs -- for defaults support 59 import Test.HUnit 60 import Text.ParserCombinators.Parsec 61 import Text.Printf 62 63 import Hledger.Data 64 import Hledger.Utils 65 66 -- report options, used in hledger-lib and above 67 data ReportOpts = ReportOpts { 68 begin_ :: Maybe Day 69 ,end_ :: Maybe Day 70 ,period_ :: Maybe (Interval,DateSpan) 71 ,cleared_ :: Bool 72 ,uncleared_ :: Bool 73 ,cost_ :: Bool 74 ,depth_ :: Maybe Int 75 ,display_ :: Maybe DisplayExp 76 ,effective_ :: Bool 77 ,empty_ :: Bool 78 ,no_elide_ :: Bool 79 ,real_ :: Bool 80 ,flat_ :: Bool -- balance 81 ,drop_ :: Int -- balance 82 ,no_total_ :: Bool -- balance 83 ,daily_ :: Bool 84 ,weekly_ :: Bool 85 ,monthly_ :: Bool 86 ,quarterly_ :: Bool 87 ,yearly_ :: Bool 88 ,format_ :: Maybe FormatStr 89 ,patterns_ :: [String] 90 } deriving (Show) 91 92 type DisplayExp = String 93 type FormatStr = String 94 95 defreportopts = ReportOpts 96 def 97 def 98 def 99 def 100 def 101 def 102 def 103 def 104 def 105 def 106 def 107 def 108 def 109 def 110 def 111 def 112 def 113 def 114 def 115 def 116 def 117 def 118 119 instance Default ReportOpts where def = defreportopts 120 121 -- | Figure out the date span we should report on, based on any 122 -- begin/end/period options provided. A period option will cause begin and 123 -- end options to be ignored. 124 dateSpanFromOpts :: Day -> ReportOpts -> DateSpan 125 dateSpanFromOpts _ ReportOpts{..} = 126 case period_ of Just (_,span) -> span 127 Nothing -> DateSpan begin_ end_ 128 129 -- | Figure out the reporting interval, if any, specified by the options. 130 -- --period overrides --daily overrides --weekly overrides --monthly etc. 131 intervalFromOpts :: ReportOpts -> Interval 132 intervalFromOpts ReportOpts{..} = 133 case period_ of 134 Just (interval,_) -> interval 135 Nothing -> i 136 where i | daily_ = Days 1 137 | weekly_ = Weeks 1 138 | monthly_ = Months 1 139 | quarterly_ = Quarters 1 140 | yearly_ = Years 1 141 | otherwise = NoInterval 142 143 -- | Get a maybe boolean representing the last cleared/uncleared option if any. 144 clearedValueFromOpts :: ReportOpts -> Maybe Bool 145 clearedValueFromOpts ReportOpts{..} | cleared_ = Just True 146 | uncleared_ = Just False 147 | otherwise = Nothing 148 149 -- | Detect which date we will report on, based on --effective. 150 whichDateFromOpts :: ReportOpts -> WhichDate 151 whichDateFromOpts ReportOpts{..} = if effective_ then EffectiveDate else ActualDate 152 153 -- | Convert this journal's transactions' primary date to either the 154 -- actual or effective date, as per options. 155 journalSelectingDateFromOpts :: ReportOpts -> Journal -> Journal 156 journalSelectingDateFromOpts opts = journalSelectingDate (whichDateFromOpts opts) 157 158 -- | Convert this journal's postings' amounts to the cost basis amounts if 159 -- specified by options. 160 journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal 161 journalSelectingAmountFromOpts opts 162 | cost_ opts = journalConvertAmountsToCost 163 | otherwise = id 164 165 -- | Convert application options to the library's generic filter specification. 166 optsToFilterSpec :: ReportOpts -> Day -> FilterSpec 167 optsToFilterSpec opts@ReportOpts{..} d = FilterSpec { 168 datespan=dateSpanFromOpts d opts 169 ,cleared= clearedValueFromOpts opts 170 ,real=real_ 171 ,empty=empty_ 172 ,acctpats=apats 173 ,descpats=dpats 174 ,depth = depth_ 175 } 176 where (apats,dpats) = parsePatternArgs patterns_ 177 178 -- | Gather filter pattern arguments into a list of account patterns and a 179 -- list of description patterns. We interpret pattern arguments as 180 -- follows: those prefixed with "desc:" are description patterns, all 181 -- others are account patterns; also patterns prefixed with "not:" are 182 -- negated. not: should come after desc: if both are used. 183 parsePatternArgs :: [String] -> ([String],[String]) 184 parsePatternArgs args = (as, ds') 185 where 186 descprefix = "desc:" 187 (ds, as) = partition (descprefix `isPrefixOf`) args 188 ds' = map (drop (length descprefix)) ds 189 190 ------------------------------------------------------------------------------- 191 192 -- | A journal entries report is a list of whole transactions as 193 -- originally entered in the journal (mostly). Used by eg hledger's print 194 -- command and hledger-web's journal entries view. 195 type EntriesReport = [EntriesReportItem] 196 type EntriesReportItem = Transaction 197 198 -- | Select transactions for an entries report. 199 entriesReport :: ReportOpts -> FilterSpec -> Journal -> EntriesReport 200 entriesReport opts fspec j = sortBy (comparing tdate) $ jtxns $ filterJournalTransactions fspec j' 201 where 202 j' = journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j 203 204 ------------------------------------------------------------------------------- 205 206 -- | A postings report is a list of postings with a running total, a label 207 -- for the total field, and a little extra transaction info to help with rendering. 208 type PostingsReport = (String -- label for the running balance column XXX remove 209 ,[PostingsReportItem] -- line items, one per posting 210 ) 211 type PostingsReportItem = (Maybe (Day, String) -- transaction date and description if this is the first posting 212 ,Posting -- the posting 213 ,MixedAmount -- the running total after this posting 214 ) 215 216 -- | Select postings from the journal and add running balance and other 217 -- information to make a postings report. Used by eg hledger's register command. 218 postingsReport :: ReportOpts -> FilterSpec -> Journal -> PostingsReport 219 postingsReport opts fspec j = (totallabel, postingsReportItems ps nullposting startbal (+)) 220 where 221 ps | interval == NoInterval = displayableps 222 | otherwise = summarisePostingsByInterval interval depth empty filterspan displayableps 223 (precedingps, displayableps, _) = postingsMatchingDisplayExpr (display_ opts) 224 $ depthClipPostings depth 225 $ journalPostings 226 $ filterJournalPostings fspec{depth=Nothing} 227 $ journalSelectingDateFromOpts opts 228 $ journalSelectingAmountFromOpts opts 229 j 230 startbal = sumPostings precedingps 231 filterspan = datespan fspec 232 (interval, depth, empty) = (intervalFromOpts opts, depth_ opts, empty_ opts) 233 234 totallabel = "Total" 235 balancelabel = "Balance" 236 237 -- | Generate postings report line items. 238 postingsReportItems :: [Posting] -> Posting -> MixedAmount -> (MixedAmount -> MixedAmount -> MixedAmount) -> [PostingsReportItem] 239 postingsReportItems [] _ _ _ = [] 240 postingsReportItems (p:ps) pprev b sumfn = i:(postingsReportItems ps p b' sumfn) 241 where 242 i = mkpostingsReportItem isfirst p b' 243 isfirst = ptransaction p /= ptransaction pprev 244 b' = b `sumfn` pamount p 245 246 -- | Generate one postings report line item, from a flag indicating 247 -- whether to include transaction info, a posting, and the current running 248 -- balance. 249 mkpostingsReportItem :: Bool -> Posting -> MixedAmount -> PostingsReportItem 250 mkpostingsReportItem False p b = (Nothing, p, b) 251 mkpostingsReportItem True p b = (ds, p, b) 252 where ds = case ptransaction p of Just (Transaction{tdate=da,tdescription=de}) -> Just (da,de) 253 Nothing -> Just (nulldate,"") 254 255 -- | Date-sort and split a list of postings into three spans - postings matched 256 -- by the given display expression, and the preceding and following postings. 257 postingsMatchingDisplayExpr :: Maybe String -> [Posting] -> ([Posting],[Posting],[Posting]) 258 postingsMatchingDisplayExpr d ps = (before, matched, after) 259 where 260 sorted = sortBy (comparing postingDate) ps 261 (before, rest) = break (displayExprMatches d) sorted 262 (matched, after) = span (displayExprMatches d) rest 263 264 -- | Does this display expression allow this posting to be displayed ? 265 -- Raises an error if the display expression can't be parsed. 266 displayExprMatches :: Maybe String -> Posting -> Bool 267 displayExprMatches Nothing _ = True 268 displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p 269 270 -- | Parse a hledger display expression, which is a simple date test like 271 -- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate. 272 datedisplayexpr :: GenParser Char st (Posting -> Bool) 273 datedisplayexpr = do 274 char 'd' 275 op <- compareop 276 char '[' 277 (y,m,d) <- smartdate 278 char ']' 279 let date = parsedate $ printf "%04s/%02s/%02s" y m d 280 test op = return $ (`op` date) . postingDate 281 case op of 282 "<" -> test (<) 283 "<=" -> test (<=) 284 "=" -> test (==) 285 "==" -> test (==) 286 ">=" -> test (>=) 287 ">" -> test (>) 288 _ -> mzero 289 where 290 compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"] 291 292 -- | Clip the account names to the specified depth in a list of postings. 293 depthClipPostings :: Maybe Int -> [Posting] -> [Posting] 294 depthClipPostings depth = map (depthClipPosting depth) 295 296 -- | Clip a posting's account name to the specified depth. 297 depthClipPosting :: Maybe Int -> Posting -> Posting 298 depthClipPosting Nothing p = p 299 depthClipPosting (Just d) p@Posting{paccount=a} = p{paccount=clipAccountName d a} 300 301 -- XXX confusing, refactor 302 303 -- | Convert a list of postings into summary postings. Summary postings 304 -- are one per account per interval and aggregated to the specified depth 305 -- if any. 306 summarisePostingsByInterval :: Interval -> Maybe Int -> Bool -> DateSpan -> [Posting] -> [Posting] 307 summarisePostingsByInterval interval depth empty filterspan ps = concatMap summarisespan $ splitSpan interval reportspan 308 where 309 summarisespan s = summarisePostingsInDateSpan s depth empty (postingsinspan s) 310 postingsinspan s = filter (isPostingInDateSpan s) ps 311 dataspan = postingsDateSpan ps 312 reportspan | empty = filterspan `orDatesFrom` dataspan 313 | otherwise = dataspan 314 315 -- | Given a date span (representing a reporting interval) and a list of 316 -- postings within it: aggregate the postings so there is only one per 317 -- account, and adjust their date/description so that they will render 318 -- as a summary for this interval. 319 -- 320 -- As usual with date spans the end date is exclusive, but for display 321 -- purposes we show the previous day as end date, like ledger. 322 -- 323 -- When a depth argument is present, postings to accounts of greater 324 -- depth are aggregated where possible. 325 -- 326 -- The showempty flag includes spans with no postings and also postings 327 -- with 0 amount. 328 summarisePostingsInDateSpan :: DateSpan -> Maybe Int -> Bool -> [Posting] -> [Posting] 329 summarisePostingsInDateSpan (DateSpan b e) depth showempty ps 330 | null ps && (isNothing b || isNothing e) = [] 331 | null ps && showempty = [summaryp] 332 | otherwise = summaryps' 333 where 334 summaryp = summaryPosting b' ("- "++ showDate (addDays (-1) e')) 335 b' = fromMaybe (maybe nulldate postingDate $ headMay ps) b 336 e' = fromMaybe (maybe (addDays 1 nulldate) postingDate $ lastMay ps) e 337 summaryPosting date desc = nullposting{ptransaction=Just nulltransaction{tdate=date,tdescription=desc}} 338 339 summaryps' = (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps 340 summaryps = [summaryp{paccount=a,pamount=balancetoshowfor a} | a <- clippedanames] 341 anames = sort $ nub $ map paccount ps 342 -- aggregate balances by account, like journalToLedger, then do depth-clipping 343 (_,_,exclbalof,inclbalof) = groupPostings ps 344 clippedanames = nub $ map (clipAccountName d) anames 345 isclipped a = accountNameLevel a >= d 346 d = fromMaybe 99999 $ depth 347 balancetoshowfor a = 348 (if isclipped a then inclbalof else exclbalof) (if null a then "top" else a) 349 350 ------------------------------------------------------------------------------- 351 352 -- | A transactions report includes a list of transactions 353 -- (posting-filtered and unfiltered variants), a running balance, and some 354 -- other information helpful for rendering a register view (a flag 355 -- indicating multiple other accounts and a display string describing 356 -- them) with or without a notion of current account(s). 357 type TransactionsReport = (String -- label for the balance column, eg "balance" or "total" 358 ,[TransactionsReportItem] -- line items, one per transaction 359 ) 360 type TransactionsReportItem = (Transaction -- the corresponding transaction 361 ,Transaction -- the transaction with postings to the current account(s) removed 362 ,Bool -- is this a split, ie more than one other account posting 363 ,String -- a display string describing the other account(s), if any 364 ,MixedAmount -- the amount posted to the current account(s) (or total amount posted) 365 ,MixedAmount -- the running balance for the current account(s) after this transaction 366 ) 367 368 triDate (t,_,_,_,_,_) = tdate t 369 triBalance (_,_,_,_,_,Mixed a) = case a of [] -> "0" 370 (Amount{quantity=q}):_ -> show q 371 372 -- | Select transactions from the whole journal for a transactions report, 373 -- with no \"current\" account. The end result is similar to 374 -- "postingsReport" except it uses matchers and transaction-based report 375 -- items and the items are most recent first. Used by eg hledger-web's 376 -- journal view. 377 journalTransactionsReport :: ReportOpts -> Journal -> Matcher -> TransactionsReport 378 journalTransactionsReport _ Journal{jtxns=ts} m = (totallabel, items) 379 where 380 ts' = sortBy (comparing tdate) $ filter (not . null . tpostings) $ map (filterTransactionPostings m) ts 381 items = reverse $ accountTransactionsReportItems m Nothing nullmixedamt id ts' 382 -- XXX items' first element should be the full transaction with all postings 383 384 ------------------------------------------------------------------------------- 385 386 -- | Select transactions within one or more \"current\" accounts, and make a 387 -- transactions report relative to those account(s). This means: 388 -- 389 -- 1. it shows transactions from the point of view of the current account(s). 390 -- The transaction amount is the amount posted to the current account(s). 391 -- The other accounts' names are provided. 392 -- 393 -- 2. With no transaction filtering in effect other than a start date, it 394 -- shows the accurate historical running balance for the current account(s). 395 -- Otherwise it shows a running total starting at 0. 396 -- 397 -- Currently, reporting intervals are not supported, and report items are 398 -- most recent first. Used by eg hledger-web's account register view. 399 -- 400 accountTransactionsReport :: ReportOpts -> Journal -> Matcher -> Matcher -> TransactionsReport 401 accountTransactionsReport opts j m thisacctmatcher = (label, items) 402 where 403 -- transactions affecting this account, in date order 404 ts = sortBy (comparing tdate) $ filter (matchesTransaction thisacctmatcher) $ jtxns $ 405 journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j 406 -- starting balance: if we are filtering by a start date and nothing else, 407 -- the sum of postings to this account before that date; otherwise zero. 408 (startbal,label) | matcherIsNull m = (nullmixedamt, balancelabel) 409 | matcherIsStartDateOnly (effective_ opts) m = (sumPostings priorps, balancelabel) 410 | otherwise = (nullmixedamt, totallabel) 411 where 412 priorps = -- ltrace "priorps" $ 413 filter (matchesPosting 414 (-- ltrace "priormatcher" $ 415 MatchAnd [thisacctmatcher, tostartdatematcher])) 416 $ transactionsPostings ts 417 tostartdatematcher = MatchDate (DateSpan Nothing startdate) 418 startdate = matcherStartDate (effective_ opts) m 419 items = reverse $ accountTransactionsReportItems m (Just thisacctmatcher) startbal negate ts 420 421 -- | Generate transactions report items from a list of transactions, 422 -- using the provided query and current account matchers, starting balance, 423 -- sign-setting function and balance-summing function. 424 accountTransactionsReportItems :: Matcher -> Maybe Matcher -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [TransactionsReportItem] 425 accountTransactionsReportItems _ _ _ _ [] = [] 426 accountTransactionsReportItems matcher thisacctmatcher bal signfn (t:ts) = 427 -- This is used for both accountTransactionsReport and journalTransactionsReport, 428 -- which makes it a bit overcomplicated 429 case i of Just i' -> i':is 430 Nothing -> is 431 where 432 tmatched@Transaction{tpostings=psmatched} = filterTransactionPostings matcher t 433 (psthisacct,psotheracct) = case thisacctmatcher of Just m -> partition (matchesPosting m) psmatched 434 Nothing -> ([],psmatched) 435 numotheraccts = length $ nub $ map paccount psotheracct 436 amt = sum $ map pamount psotheracct 437 acct | isNothing thisacctmatcher = summarisePostings psmatched -- journal register 438 | numotheraccts == 0 = "transfer between " ++ summarisePostingAccounts psthisacct 439 | otherwise = prefix ++ summarisePostingAccounts psotheracct 440 where prefix = maybe "" (\b -> if b then "from " else "to ") $ isNegativeMixedAmount amt 441 (i,bal') = case psmatched of 442 [] -> (Nothing,bal) 443 _ -> (Just (t, tmatched, numotheraccts > 1, acct, a, b), b) 444 where 445 a = signfn amt 446 b = bal + a 447 is = accountTransactionsReportItems matcher thisacctmatcher bal' signfn ts 448 449 -- | Generate a short readable summary of some postings, like 450 -- "from (negatives) to (positives)". 451 summarisePostings :: [Posting] -> String 452 summarisePostings ps = 453 case (summarisePostingAccounts froms, summarisePostingAccounts tos) of 454 ("",t) -> "to "++t 455 (f,"") -> "from "++f 456 (f,t) -> "from "++f++" to "++t 457 where 458 (froms,tos) = partition (fromMaybe False . isNegativeMixedAmount . pamount) ps 459 460 -- | Generate a simplified summary of some postings' accounts. 461 summarisePostingAccounts :: [Posting] -> String 462 summarisePostingAccounts = intercalate ", " . map accountLeafName . nub . map paccount 463 464 filterTransactionPostings :: Matcher -> Transaction -> Transaction 465 filterTransactionPostings m t@Transaction{tpostings=ps} = t{tpostings=filter (m `matchesPosting`) ps} 466 467 ------------------------------------------------------------------------------- 468 469 -- | An accounts report is a list of account names (full and short 470 -- variants) with their balances, appropriate indentation for rendering as 471 -- a hierarchy, and grand total. 472 type AccountsReport = ([AccountsReportItem] -- line items, one per account 473 ,MixedAmount -- total balance of all accounts 474 ) 475 type AccountsReportItem = (AccountName -- full account name 476 ,AccountName -- short account name for display (the leaf name, prefixed by any boring parents immediately above) 477 ,Int -- how many steps to indent this account (0-based account depth excluding boring parents) 478 ,MixedAmount) -- account balance, includes subs unless --flat is present 479 480 -- | Select accounts, and get their balances at the end of the selected 481 -- period, and misc. display information, for an accounts report. Used by 482 -- eg hledger's balance command. 483 accountsReport :: ReportOpts -> FilterSpec -> Journal -> AccountsReport 484 accountsReport opts filterspec j = accountsReport' opts j (journalToLedger filterspec) 485 486 -- | Select accounts, and get their balances at the end of the selected 487 -- period, and misc. display information, for an accounts report. Like 488 -- "accountsReport" but uses the new matchers. Used by eg hledger-web's 489 -- accounts sidebar. 490 accountsReport2 :: ReportOpts -> Matcher -> Journal -> AccountsReport 491 accountsReport2 opts matcher j = accountsReport' opts j (journalToLedger2 matcher) 492 493 -- Accounts report helper. 494 accountsReport' :: ReportOpts -> Journal -> (Journal -> Ledger) -> AccountsReport 495 accountsReport' opts j jtol = (items, total) 496 where 497 items = map mkitem interestingaccts 498 interestingaccts | no_elide_ opts = acctnames 499 | otherwise = filter (isInteresting opts l) acctnames 500 acctnames = sort $ tail $ flatten $ treemap aname accttree 501 accttree = ledgerAccountTree (fromMaybe 99999 $ depth_ opts) l 502 total = sum $ map abalance $ ledgerTopAccounts l 503 l = jtol $ journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j 504 505 -- | Get data for one balance report line item. 506 mkitem :: AccountName -> AccountsReportItem 507 mkitem a = (a, adisplay, indent, abal) 508 where 509 adisplay | flat_ opts = a 510 | otherwise = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a] 511 where ps = takeWhile boring parents where boring = not . (`elem` interestingparents) 512 indent | flat_ opts = 0 513 | otherwise = length interestingparents 514 interestingparents = filter (`elem` interestingaccts) parents 515 parents = parentAccountNames a 516 abal | flat_ opts = exclusiveBalance acct 517 | otherwise = abalance acct 518 where acct = ledgerAccount l a 519 520 exclusiveBalance :: Account -> MixedAmount 521 exclusiveBalance = sumPostings . apostings 522 523 -- | Is the named account considered interesting for this ledger's accounts report, 524 -- following the eliding style of ledger's balance command ? 525 isInteresting :: ReportOpts -> Ledger -> AccountName -> Bool 526 isInteresting opts l a | flat_ opts = isInterestingFlat opts l a 527 | otherwise = isInterestingIndented opts l a 528 529 isInterestingFlat :: ReportOpts -> Ledger -> AccountName -> Bool 530 isInterestingFlat opts l a = notempty || emptyflag 531 where 532 acct = ledgerAccount l a 533 notempty = not $ isZeroMixedAmount $ exclusiveBalance acct 534 emptyflag = empty_ opts 535 536 isInterestingIndented :: ReportOpts -> Ledger -> AccountName -> Bool 537 isInterestingIndented opts l a 538 | numinterestingsubs==1 && not atmaxdepth = notlikesub 539 | otherwise = notzero || emptyflag 540 where 541 atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depth_ opts 542 emptyflag = empty_ opts 543 acct = ledgerAccount l a 544 notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct 545 notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumPostings $ apostings acct 546 numinterestingsubs = length $ filter isInterestingTree subtrees 547 where 548 isInterestingTree = treeany (isInteresting opts l . aname) 549 subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a 550 551 ------------------------------------------------------------------------------- 552 553 tests_Hledger_Reports :: Test 554 tests_Hledger_Reports = TestList 555 [ 556 557 "summarisePostingsByInterval" ~: do 558 summarisePostingsByInterval (Quarters 1) Nothing False (DateSpan Nothing Nothing) [] ~?= [] 559 560 -- ,"summarisePostingsInDateSpan" ~: do 561 -- let gives (b,e,depth,showempty,ps) = 562 -- (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`) 563 -- let ps = 564 -- [ 565 -- nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]} 566 -- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 2]} 567 -- ,nullposting{lpdescription="desc",lpaccount="expenses:food", lpamount=Mixed [dollars 4]} 568 -- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 8]} 569 -- ] 570 -- ("2008/01/01","2009/01/01",0,9999,False,[]) `gives` 571 -- [] 572 -- ("2008/01/01","2009/01/01",0,9999,True,[]) `gives` 573 -- [ 574 -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31"} 575 -- ] 576 -- ("2008/01/01","2009/01/01",0,9999,False,ts) `gives` 577 -- [ 578 -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food", lpamount=Mixed [dollars 4]} 579 -- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 10]} 580 -- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]} 581 -- ] 582 -- ("2008/01/01","2009/01/01",0,2,False,ts) `gives` 583 -- [ 584 -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [dollars 15]} 585 -- ] 586 -- ("2008/01/01","2009/01/01",0,1,False,ts) `gives` 587 -- [ 588 -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [dollars 15]} 589 -- ] 590 -- ("2008/01/01","2009/01/01",0,0,False,ts) `gives` 591 -- [ 592 -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [dollars 15]} 593 -- ] 594 595 ]