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  ]