mirror of
https://github.com/simonmichael/hledger.git
synced 2024-12-27 12:24:43 +03:00
refactor: move basic report generation to hledger-lib
This commit is contained in:
parent
b27c90aea5
commit
df7dc1464e
@ -1,6 +1,7 @@
|
|||||||
module Hledger (
|
module Hledger (
|
||||||
module Hledger.Data
|
module Hledger.Data
|
||||||
,module Hledger.Read
|
,module Hledger.Read
|
||||||
|
,module Hledger.Report
|
||||||
,module Hledger.Utils
|
,module Hledger.Utils
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
448
hledger-lib/Hledger/Report.hs
Normal file
448
hledger-lib/Hledger/Report.hs
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
{-|
|
||||||
|
|
||||||
|
Generate various kinds of report from a journal/ledger.
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Hledger.Report (
|
||||||
|
tests_Hledger_Report
|
||||||
|
,JournalReport
|
||||||
|
,JournalReportItem
|
||||||
|
,PostingRegisterReport
|
||||||
|
,PostingRegisterReportItem
|
||||||
|
,AccountRegisterReport
|
||||||
|
,AccountRegisterReportItem
|
||||||
|
,BalanceReport
|
||||||
|
,BalanceReportItem
|
||||||
|
,ariDate
|
||||||
|
,ariBalance
|
||||||
|
,journalReport
|
||||||
|
,postingRegisterReport
|
||||||
|
,accountRegisterReport
|
||||||
|
,journalRegisterReport
|
||||||
|
,mkpostingRegisterItem
|
||||||
|
,balanceReport
|
||||||
|
,balanceReport2
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
|
import Data.Ord
|
||||||
|
import Data.Time.Calendar
|
||||||
|
import Data.Tree
|
||||||
|
import Safe (headMay, lastMay)
|
||||||
|
import Test.HUnit
|
||||||
|
import Text.ParserCombinators.Parsec
|
||||||
|
import Text.Printf
|
||||||
|
|
||||||
|
import Hledger.Cli.Options
|
||||||
|
import Hledger.Cli.Utils
|
||||||
|
import Hledger.Data
|
||||||
|
import Hledger.Utils
|
||||||
|
|
||||||
|
|
||||||
|
-- | A "journal report" is just a list of transactions.
|
||||||
|
type JournalReport = [JournalReportItem]
|
||||||
|
|
||||||
|
type JournalReportItem = Transaction
|
||||||
|
|
||||||
|
-- | A posting register report lists postings to one or more accounts,
|
||||||
|
-- with a running total. Postings may be actual postings, or aggregate
|
||||||
|
-- postings corresponding to a reporting interval.
|
||||||
|
type PostingRegisterReport = (String -- label for the running balance column XXX remove
|
||||||
|
,[PostingRegisterReportItem] -- line items, one per posting
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostingRegisterReportItem = (Maybe (Day, String) -- transaction date and description if this is the first posting
|
||||||
|
,Posting -- the posting
|
||||||
|
,MixedAmount -- the running total after this posting
|
||||||
|
)
|
||||||
|
|
||||||
|
-- | An account register report lists transactions to a single account (or
|
||||||
|
-- possibly subs as well), with the accurate running account balance when
|
||||||
|
-- possible (otherwise, a running total.)
|
||||||
|
type AccountRegisterReport = (String -- label for the balance column, eg "balance" or "total"
|
||||||
|
,[AccountRegisterReportItem] -- line items, one per transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountRegisterReportItem = (Transaction -- the corresponding transaction
|
||||||
|
,Transaction -- the transaction with postings to the focussed account removed
|
||||||
|
,Bool -- is this a split (more than one other-account posting) ?
|
||||||
|
,String -- the (possibly aggregated) account info to display
|
||||||
|
,MixedAmount -- the (possibly aggregated) amount to display (sum of the other-account postings)
|
||||||
|
,MixedAmount -- the running balance for the focussed account after this transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
ariDate (t,_,_,_,_,_) = tdate t
|
||||||
|
ariBalance (_,_,_,_,_,Mixed a) = case a of [] -> "0"
|
||||||
|
(Amount{quantity=q}):_ -> show q
|
||||||
|
|
||||||
|
-- | A balance report is a chart of accounts with balances, and their grand total.
|
||||||
|
type BalanceReport = ([BalanceReportItem] -- line items, one per account
|
||||||
|
,MixedAmount -- total balance of all accounts
|
||||||
|
)
|
||||||
|
|
||||||
|
type BalanceReportItem = (AccountName -- full account name
|
||||||
|
,AccountName -- account name elided for display: the leaf name,
|
||||||
|
-- prefixed by any boring parents immediately above
|
||||||
|
,Int -- how many steps to indent this account (0-based account depth excluding boring parents)
|
||||||
|
,MixedAmount) -- account balance, includes subs unless --flat is present
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
journalReport :: [Opt] -> FilterSpec -> Journal -> JournalReport
|
||||||
|
journalReport opts fspec j = sortBy (comparing tdate) $ jtxns $ filterJournalTransactions fspec j'
|
||||||
|
where
|
||||||
|
j' = journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Get a ledger-style posting register report, with the specified options,
|
||||||
|
-- for the whole journal. See also "accountRegisterReport".
|
||||||
|
postingRegisterReport :: [Opt] -> FilterSpec -> Journal -> PostingRegisterReport
|
||||||
|
postingRegisterReport opts fspec j = (totallabel, postingRegisterItems ps nullposting startbal (+))
|
||||||
|
where
|
||||||
|
ps | interval == NoInterval = displayableps
|
||||||
|
| otherwise = summarisePostingsByInterval interval depth empty filterspan displayableps
|
||||||
|
(precedingps, displayableps, _) = postingsMatchingDisplayExpr (displayExprFromOpts opts)
|
||||||
|
$ depthClipPostings depth
|
||||||
|
$ journalPostings
|
||||||
|
$ filterJournalPostings fspec{depth=Nothing}
|
||||||
|
$ journalSelectingDateFromOpts opts
|
||||||
|
$ journalSelectingAmountFromOpts opts
|
||||||
|
j
|
||||||
|
startbal = sumPostings precedingps
|
||||||
|
filterspan = datespan fspec
|
||||||
|
(interval, depth, empty) = (intervalFromOpts opts, depthFromOpts opts, Empty `elem` opts)
|
||||||
|
|
||||||
|
totallabel = "Total"
|
||||||
|
balancelabel = "Balance"
|
||||||
|
|
||||||
|
-- | Generate posting register report line items.
|
||||||
|
postingRegisterItems :: [Posting] -> Posting -> MixedAmount -> (MixedAmount -> MixedAmount -> MixedAmount) -> [PostingRegisterReportItem]
|
||||||
|
postingRegisterItems [] _ _ _ = []
|
||||||
|
postingRegisterItems (p:ps) pprev b sumfn = i:(postingRegisterItems ps p b' sumfn)
|
||||||
|
where
|
||||||
|
i = mkpostingRegisterItem isfirst p b'
|
||||||
|
isfirst = ptransaction p /= ptransaction pprev
|
||||||
|
b' = b `sumfn` pamount p
|
||||||
|
|
||||||
|
-- | Generate one register report line item, from a flag indicating
|
||||||
|
-- whether to include transaction info, a posting, and the current running
|
||||||
|
-- balance.
|
||||||
|
mkpostingRegisterItem :: Bool -> Posting -> MixedAmount -> PostingRegisterReportItem
|
||||||
|
mkpostingRegisterItem False p b = (Nothing, p, b)
|
||||||
|
mkpostingRegisterItem True p b = (ds, p, b)
|
||||||
|
where ds = case ptransaction p of Just (Transaction{tdate=da,tdescription=de}) -> Just (da,de)
|
||||||
|
Nothing -> Just (nulldate,"")
|
||||||
|
|
||||||
|
-- | Date-sort and split a list of postings into three spans - postings matched
|
||||||
|
-- by the given display expression, and the preceding and following postings.
|
||||||
|
postingsMatchingDisplayExpr :: Maybe String -> [Posting] -> ([Posting],[Posting],[Posting])
|
||||||
|
postingsMatchingDisplayExpr d ps = (before, matched, after)
|
||||||
|
where
|
||||||
|
sorted = sortBy (comparing postingDate) ps
|
||||||
|
(before, rest) = break (displayExprMatches d) sorted
|
||||||
|
(matched, after) = span (displayExprMatches d) rest
|
||||||
|
|
||||||
|
-- | Does this display expression allow this posting to be displayed ?
|
||||||
|
-- Raises an error if the display expression can't be parsed.
|
||||||
|
displayExprMatches :: Maybe String -> Posting -> Bool
|
||||||
|
displayExprMatches Nothing _ = True
|
||||||
|
displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p
|
||||||
|
|
||||||
|
-- | Parse a hledger display expression, which is a simple date test like
|
||||||
|
-- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.
|
||||||
|
datedisplayexpr :: GenParser Char st (Posting -> Bool)
|
||||||
|
datedisplayexpr = do
|
||||||
|
char 'd'
|
||||||
|
op <- compareop
|
||||||
|
char '['
|
||||||
|
(y,m,d) <- smartdate
|
||||||
|
char ']'
|
||||||
|
let date = parsedate $ printf "%04s/%02s/%02s" y m d
|
||||||
|
test op = return $ (`op` date) . postingDate
|
||||||
|
case op of
|
||||||
|
"<" -> test (<)
|
||||||
|
"<=" -> test (<=)
|
||||||
|
"=" -> test (==)
|
||||||
|
"==" -> test (==)
|
||||||
|
">=" -> test (>=)
|
||||||
|
">" -> test (>)
|
||||||
|
_ -> mzero
|
||||||
|
where
|
||||||
|
compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]
|
||||||
|
|
||||||
|
-- | Clip the account names to the specified depth in a list of postings.
|
||||||
|
depthClipPostings :: Maybe Int -> [Posting] -> [Posting]
|
||||||
|
depthClipPostings depth = map (depthClipPosting depth)
|
||||||
|
|
||||||
|
-- | Clip a posting's account name to the specified depth.
|
||||||
|
depthClipPosting :: Maybe Int -> Posting -> Posting
|
||||||
|
depthClipPosting Nothing p = p
|
||||||
|
depthClipPosting (Just d) p@Posting{paccount=a} = p{paccount=clipAccountName d a}
|
||||||
|
|
||||||
|
-- XXX confusing, refactor
|
||||||
|
|
||||||
|
-- | Convert a list of postings into summary postings. Summary postings
|
||||||
|
-- are one per account per interval and aggregated to the specified depth
|
||||||
|
-- if any.
|
||||||
|
summarisePostingsByInterval :: Interval -> Maybe Int -> Bool -> DateSpan -> [Posting] -> [Posting]
|
||||||
|
summarisePostingsByInterval interval depth empty filterspan ps = concatMap summarisespan $ splitSpan interval reportspan
|
||||||
|
where
|
||||||
|
summarisespan s = summarisePostingsInDateSpan s depth empty (postingsinspan s)
|
||||||
|
postingsinspan s = filter (isPostingInDateSpan s) ps
|
||||||
|
dataspan = postingsDateSpan ps
|
||||||
|
reportspan | empty = filterspan `orDatesFrom` dataspan
|
||||||
|
| otherwise = dataspan
|
||||||
|
|
||||||
|
-- | Given a date span (representing a reporting interval) and a list of
|
||||||
|
-- postings within it: aggregate the postings so there is only one per
|
||||||
|
-- account, and adjust their date/description so that they will render
|
||||||
|
-- as a summary for this interval.
|
||||||
|
--
|
||||||
|
-- As usual with date spans the end date is exclusive, but for display
|
||||||
|
-- purposes we show the previous day as end date, like ledger.
|
||||||
|
--
|
||||||
|
-- When a depth argument is present, postings to accounts of greater
|
||||||
|
-- depth are aggregated where possible.
|
||||||
|
--
|
||||||
|
-- The showempty flag includes spans with no postings and also postings
|
||||||
|
-- with 0 amount.
|
||||||
|
summarisePostingsInDateSpan :: DateSpan -> Maybe Int -> Bool -> [Posting] -> [Posting]
|
||||||
|
summarisePostingsInDateSpan (DateSpan b e) depth showempty ps
|
||||||
|
| null ps && (isNothing b || isNothing e) = []
|
||||||
|
| null ps && showempty = [summaryp]
|
||||||
|
| otherwise = summaryps'
|
||||||
|
where
|
||||||
|
summaryp = summaryPosting b' ("- "++ showDate (addDays (-1) e'))
|
||||||
|
b' = fromMaybe (maybe nulldate postingDate $ headMay ps) b
|
||||||
|
e' = fromMaybe (maybe (addDays 1 nulldate) postingDate $ lastMay ps) e
|
||||||
|
summaryPosting date desc = nullposting{ptransaction=Just nulltransaction{tdate=date,tdescription=desc}}
|
||||||
|
|
||||||
|
summaryps' = (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps
|
||||||
|
summaryps = [summaryp{paccount=a,pamount=balancetoshowfor a} | a <- clippedanames]
|
||||||
|
anames = sort $ nub $ map paccount ps
|
||||||
|
-- aggregate balances by account, like journalToLedger, then do depth-clipping
|
||||||
|
(_,_,exclbalof,inclbalof) = groupPostings ps
|
||||||
|
clippedanames = nub $ map (clipAccountName d) anames
|
||||||
|
isclipped a = accountNameLevel a >= d
|
||||||
|
d = fromMaybe 99999 $ depth
|
||||||
|
balancetoshowfor a =
|
||||||
|
(if isclipped a then inclbalof else exclbalof) (if null a then "top" else a)
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Get a ledger-style register report showing all matched transactions and postings.
|
||||||
|
-- Similar to "postingRegisterReport" except it uses matchers and
|
||||||
|
-- per-transaction report items like "accountRegisterReport".
|
||||||
|
journalRegisterReport :: [Opt] -> Journal -> Matcher -> AccountRegisterReport
|
||||||
|
journalRegisterReport _ Journal{jtxns=ts} m = (totallabel, items)
|
||||||
|
where
|
||||||
|
ts' = sortBy (comparing tdate) $ filter (not . null . tpostings) $ map (filterTransactionPostings m) ts
|
||||||
|
items = reverse $ accountRegisterReportItems m Nothing nullmixedamt id ts'
|
||||||
|
-- XXX items' first element should be the full transaction with all postings
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Get a conventional account register report, with the specified
|
||||||
|
-- options, for the currently focussed account (or possibly the focussed
|
||||||
|
-- account plus sub-accounts.) This differs from "postingRegisterReport"
|
||||||
|
-- in several ways:
|
||||||
|
--
|
||||||
|
-- 1. it shows transactions, from the point of view of the focussed
|
||||||
|
-- account. The other account's name and posted amount is displayed,
|
||||||
|
-- aggregated if there is more than one other account posting.
|
||||||
|
--
|
||||||
|
-- 2. With no transaction filtering in effect other than a start date, it
|
||||||
|
-- shows the accurate historical running balance for this
|
||||||
|
-- account. Otherwise it shows a running total starting at 0 like the posting register report.
|
||||||
|
--
|
||||||
|
-- 3. Currently this report does not handle reporting intervals.
|
||||||
|
--
|
||||||
|
-- 4. Report items will be most recent first.
|
||||||
|
--
|
||||||
|
accountRegisterReport :: [Opt] -> Journal -> Matcher -> Matcher -> AccountRegisterReport
|
||||||
|
accountRegisterReport opts j m thisacctmatcher = (label, items)
|
||||||
|
where
|
||||||
|
-- transactions affecting this account, in date order
|
||||||
|
ts = sortBy (comparing tdate) $ filter (matchesTransaction thisacctmatcher) $ jtxns j
|
||||||
|
-- starting balance: if we are filtering by a start date and nothing else,
|
||||||
|
-- the sum of postings to this account before that date; otherwise zero.
|
||||||
|
(startbal,label) | matcherIsNull m = (nullmixedamt, balancelabel)
|
||||||
|
| matcherIsStartDateOnly effective m = (sumPostings priorps, balancelabel)
|
||||||
|
| otherwise = (nullmixedamt, totallabel)
|
||||||
|
where
|
||||||
|
priorps = -- ltrace "priorps" $
|
||||||
|
filter (matchesPosting
|
||||||
|
(-- ltrace "priormatcher" $
|
||||||
|
MatchAnd [thisacctmatcher, tostartdatematcher]))
|
||||||
|
$ transactionsPostings ts
|
||||||
|
tostartdatematcher = MatchDate True (DateSpan Nothing startdate)
|
||||||
|
startdate = matcherStartDate effective m
|
||||||
|
effective = Effective `elem` opts
|
||||||
|
items = reverse $ accountRegisterReportItems m (Just thisacctmatcher) startbal negate ts
|
||||||
|
|
||||||
|
-- | Generate account register line items from a list of transactions,
|
||||||
|
-- using the provided query and "this account" matchers, starting balance,
|
||||||
|
-- sign-setting function and balance-summing function.
|
||||||
|
|
||||||
|
-- This is used for both accountRegisterReport and journalRegisterReport,
|
||||||
|
-- which makes it a bit overcomplicated.
|
||||||
|
accountRegisterReportItems :: Matcher -> Maybe Matcher -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [AccountRegisterReportItem]
|
||||||
|
accountRegisterReportItems _ _ _ _ [] = []
|
||||||
|
accountRegisterReportItems matcher thisacctmatcher bal signfn (t:ts) =
|
||||||
|
case i of Just i' -> i':is
|
||||||
|
Nothing -> is
|
||||||
|
where
|
||||||
|
tmatched@Transaction{tpostings=psmatched} = filterTransactionPostings matcher t
|
||||||
|
(psthisacct,psotheracct) = case thisacctmatcher of Just m -> partition (matchesPosting m) psmatched
|
||||||
|
Nothing -> ([],psmatched)
|
||||||
|
numotheraccts = length $ nub $ map paccount psotheracct
|
||||||
|
amt = sum $ map pamount psotheracct
|
||||||
|
acct | isNothing thisacctmatcher = summarisePostings psmatched -- journal register
|
||||||
|
| numotheraccts == 0 = "transfer between " ++ summarisePostingAccounts psthisacct
|
||||||
|
| otherwise = prefix ++ summarisePostingAccounts psotheracct
|
||||||
|
where prefix = maybe "" (\b -> if b then "from " else "to ") $ isNegativeMixedAmount amt
|
||||||
|
(i,bal') = case psmatched of
|
||||||
|
[] -> (Nothing,bal)
|
||||||
|
_ -> (Just (t, tmatched, numotheraccts > 1, acct, a, b), b)
|
||||||
|
where
|
||||||
|
a = signfn amt
|
||||||
|
b = bal + a
|
||||||
|
is = accountRegisterReportItems matcher thisacctmatcher bal' signfn ts
|
||||||
|
|
||||||
|
-- | Generate a short readable summary of some postings, like
|
||||||
|
-- "from (negatives) to (positives)".
|
||||||
|
summarisePostings :: [Posting] -> String
|
||||||
|
summarisePostings ps =
|
||||||
|
case (summarisePostingAccounts froms, summarisePostingAccounts tos) of
|
||||||
|
("",t) -> "to "++t
|
||||||
|
(f,"") -> "from "++f
|
||||||
|
(f,t) -> "from "++f++" to "++t
|
||||||
|
where
|
||||||
|
(froms,tos) = partition (fromMaybe False . isNegativeMixedAmount . pamount) ps
|
||||||
|
|
||||||
|
-- | Generate a simplified summary of some postings' accounts.
|
||||||
|
summarisePostingAccounts :: [Posting] -> String
|
||||||
|
summarisePostingAccounts = intercalate ", " . map accountLeafName . nub . map paccount
|
||||||
|
|
||||||
|
filterTransactionPostings :: Matcher -> Transaction -> Transaction
|
||||||
|
filterTransactionPostings m t@Transaction{tpostings=ps} = t{tpostings=filter (m `matchesPosting`) ps}
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Get a balance report with the specified options for this journal.
|
||||||
|
balanceReport :: [Opt] -> FilterSpec -> Journal -> BalanceReport
|
||||||
|
balanceReport opts filterspec j = balanceReport' opts j (journalToLedger filterspec)
|
||||||
|
|
||||||
|
-- | Get a balance report with the specified options for this
|
||||||
|
-- journal. Like balanceReport but uses the new matchers.
|
||||||
|
balanceReport2 :: [Opt] -> Matcher -> Journal -> BalanceReport
|
||||||
|
balanceReport2 opts matcher j = balanceReport' opts j (journalToLedger2 matcher)
|
||||||
|
|
||||||
|
-- Balance report helper.
|
||||||
|
balanceReport' :: [Opt] -> Journal -> (Journal -> Ledger) -> BalanceReport
|
||||||
|
balanceReport' opts j jtol = (items, total)
|
||||||
|
where
|
||||||
|
items = map mkitem interestingaccts
|
||||||
|
interestingaccts | NoElide `elem` opts = acctnames
|
||||||
|
| otherwise = filter (isInteresting opts l) acctnames
|
||||||
|
acctnames = sort $ tail $ flatten $ treemap aname accttree
|
||||||
|
accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l
|
||||||
|
total = sum $ map abalance $ ledgerTopAccounts l
|
||||||
|
l = jtol $ journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j
|
||||||
|
|
||||||
|
-- | Get data for one balance report line item.
|
||||||
|
mkitem :: AccountName -> BalanceReportItem
|
||||||
|
mkitem a = (a, adisplay, indent, abal)
|
||||||
|
where
|
||||||
|
adisplay | Flat `elem` opts = a
|
||||||
|
| otherwise = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a]
|
||||||
|
where ps = takeWhile boring parents where boring = not . (`elem` interestingparents)
|
||||||
|
indent | Flat `elem` opts = 0
|
||||||
|
| otherwise = length interestingparents
|
||||||
|
interestingparents = filter (`elem` interestingaccts) parents
|
||||||
|
parents = parentAccountNames a
|
||||||
|
abal | Flat `elem` opts = exclusiveBalance acct
|
||||||
|
| otherwise = abalance acct
|
||||||
|
where acct = ledgerAccount l a
|
||||||
|
|
||||||
|
exclusiveBalance :: Account -> MixedAmount
|
||||||
|
exclusiveBalance = sumPostings . apostings
|
||||||
|
|
||||||
|
-- | Is the named account considered interesting for this ledger's balance report ?
|
||||||
|
-- We follow the style of ledger's balance command.
|
||||||
|
isInteresting :: [Opt] -> Ledger -> AccountName -> Bool
|
||||||
|
isInteresting opts l a | Flat `elem` opts = isInterestingFlat opts l a
|
||||||
|
| otherwise = isInterestingIndented opts l a
|
||||||
|
|
||||||
|
isInterestingFlat :: [Opt] -> Ledger -> AccountName -> Bool
|
||||||
|
isInterestingFlat opts l a = notempty || emptyflag
|
||||||
|
where
|
||||||
|
acct = ledgerAccount l a
|
||||||
|
notempty = not $ isZeroMixedAmount $ exclusiveBalance acct
|
||||||
|
emptyflag = Empty `elem` opts
|
||||||
|
|
||||||
|
isInterestingIndented :: [Opt] -> Ledger -> AccountName -> Bool
|
||||||
|
isInterestingIndented opts l a
|
||||||
|
| numinterestingsubs==1 && not atmaxdepth = notlikesub
|
||||||
|
| otherwise = notzero || emptyflag
|
||||||
|
where
|
||||||
|
atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depthFromOpts opts
|
||||||
|
emptyflag = Empty `elem` opts
|
||||||
|
acct = ledgerAccount l a
|
||||||
|
notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct
|
||||||
|
notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumPostings $ apostings acct
|
||||||
|
numinterestingsubs = length $ filter isInterestingTree subtrees
|
||||||
|
where
|
||||||
|
isInterestingTree = treeany (isInteresting opts l . aname)
|
||||||
|
subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
tests_Hledger_Report :: Test
|
||||||
|
tests_Hledger_Report = TestList
|
||||||
|
[
|
||||||
|
|
||||||
|
"summarisePostingsByInterval" ~: do
|
||||||
|
summarisePostingsByInterval (Quarters 1) Nothing False (DateSpan Nothing Nothing) [] ~?= []
|
||||||
|
|
||||||
|
-- ,"summarisePostingsInDateSpan" ~: do
|
||||||
|
-- let gives (b,e,depth,showempty,ps) =
|
||||||
|
-- (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`)
|
||||||
|
-- let ps =
|
||||||
|
-- [
|
||||||
|
-- nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}
|
||||||
|
-- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 2]}
|
||||||
|
-- ,nullposting{lpdescription="desc",lpaccount="expenses:food", lpamount=Mixed [dollars 4]}
|
||||||
|
-- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 8]}
|
||||||
|
-- ]
|
||||||
|
-- ("2008/01/01","2009/01/01",0,9999,False,[]) `gives`
|
||||||
|
-- []
|
||||||
|
-- ("2008/01/01","2009/01/01",0,9999,True,[]) `gives`
|
||||||
|
-- [
|
||||||
|
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31"}
|
||||||
|
-- ]
|
||||||
|
-- ("2008/01/01","2009/01/01",0,9999,False,ts) `gives`
|
||||||
|
-- [
|
||||||
|
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food", lpamount=Mixed [dollars 4]}
|
||||||
|
-- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 10]}
|
||||||
|
-- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}
|
||||||
|
-- ]
|
||||||
|
-- ("2008/01/01","2009/01/01",0,2,False,ts) `gives`
|
||||||
|
-- [
|
||||||
|
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [dollars 15]}
|
||||||
|
-- ]
|
||||||
|
-- ("2008/01/01","2009/01/01",0,1,False,ts) `gives`
|
||||||
|
-- [
|
||||||
|
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [dollars 15]}
|
||||||
|
-- ]
|
||||||
|
-- ("2008/01/01","2009/01/01",0,0,False,ts) `gives`
|
||||||
|
-- [
|
||||||
|
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [dollars 15]}
|
||||||
|
-- ]
|
||||||
|
|
||||||
|
]
|
@ -96,18 +96,13 @@ balance report:
|
|||||||
-}
|
-}
|
||||||
|
|
||||||
module Hledger.Cli.Balance (
|
module Hledger.Cli.Balance (
|
||||||
BalanceReport
|
balance
|
||||||
,BalanceReportItem
|
|
||||||
,balance
|
|
||||||
,balanceReport
|
|
||||||
,balanceReport2
|
|
||||||
,balanceReportAsText
|
,balanceReportAsText
|
||||||
,tests_Hledger_Cli_Balance
|
,tests_Hledger_Cli_Balance
|
||||||
-- ,tests_Balance
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Tree
|
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
|
|
||||||
import Hledger.Cli.Format
|
import Hledger.Cli.Format
|
||||||
@ -120,18 +115,6 @@ import Prelude hiding (putStr)
|
|||||||
import Hledger.Utils.UTF8 (putStr)
|
import Hledger.Utils.UTF8 (putStr)
|
||||||
|
|
||||||
|
|
||||||
-- | A balance report is a chart of accounts with balances, and their grand total.
|
|
||||||
type BalanceReport = ([BalanceReportItem] -- line items, one per account
|
|
||||||
,MixedAmount -- total balance of all accounts
|
|
||||||
)
|
|
||||||
|
|
||||||
-- | The data for a single balance report line item, representing one account.
|
|
||||||
type BalanceReportItem = (AccountName -- full account name
|
|
||||||
,AccountName -- account name elided for display: the leaf name,
|
|
||||||
-- prefixed by any boring parents immediately above
|
|
||||||
,Int -- how many steps to indent this account (0-based account depth excluding boring parents)
|
|
||||||
,MixedAmount) -- account balance, includes subs unless --flat is present
|
|
||||||
|
|
||||||
-- | Print a balance report.
|
-- | Print a balance report.
|
||||||
balance :: [Opt] -> [String] -> Journal -> IO ()
|
balance :: [Opt] -> [String] -> Journal -> IO ()
|
||||||
balance opts args j = do
|
balance opts args j = do
|
||||||
@ -196,73 +179,6 @@ formatAccount opts accountName depth balance leftJustified min max field = case
|
|||||||
where
|
where
|
||||||
a = maybe "" (accountNameDrop (dropFromOpts opts)) accountName
|
a = maybe "" (accountNameDrop (dropFromOpts opts)) accountName
|
||||||
|
|
||||||
-- | Get a balance report with the specified options for this journal.
|
|
||||||
balanceReport :: [Opt] -> FilterSpec -> Journal -> BalanceReport
|
|
||||||
balanceReport opts filterspec j = balanceReport' opts j (journalToLedger filterspec)
|
|
||||||
|
|
||||||
-- | Get a balance report with the specified options for this
|
|
||||||
-- journal. Like balanceReport but uses the new matchers.
|
|
||||||
balanceReport2 :: [Opt] -> Matcher -> Journal -> BalanceReport
|
|
||||||
balanceReport2 opts matcher j = balanceReport' opts j (journalToLedger2 matcher)
|
|
||||||
|
|
||||||
-- Balance report helper.
|
|
||||||
balanceReport' :: [Opt] -> Journal -> (Journal -> Ledger) -> BalanceReport
|
|
||||||
balanceReport' opts j jtol = (items, total)
|
|
||||||
where
|
|
||||||
items = map mkitem interestingaccts
|
|
||||||
interestingaccts | NoElide `elem` opts = acctnames
|
|
||||||
| otherwise = filter (isInteresting opts l) acctnames
|
|
||||||
acctnames = sort $ tail $ flatten $ treemap aname accttree
|
|
||||||
accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l
|
|
||||||
total = sum $ map abalance $ ledgerTopAccounts l
|
|
||||||
l = jtol $ journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j
|
|
||||||
|
|
||||||
-- | Get data for one balance report line item.
|
|
||||||
mkitem :: AccountName -> BalanceReportItem
|
|
||||||
mkitem a = (a, adisplay, indent, abal)
|
|
||||||
where
|
|
||||||
adisplay | Flat `elem` opts = a
|
|
||||||
| otherwise = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a]
|
|
||||||
where ps = takeWhile boring parents where boring = not . (`elem` interestingparents)
|
|
||||||
indent | Flat `elem` opts = 0
|
|
||||||
| otherwise = length interestingparents
|
|
||||||
interestingparents = filter (`elem` interestingaccts) parents
|
|
||||||
parents = parentAccountNames a
|
|
||||||
abal | Flat `elem` opts = exclusiveBalance acct
|
|
||||||
| otherwise = abalance acct
|
|
||||||
where acct = ledgerAccount l a
|
|
||||||
|
|
||||||
exclusiveBalance :: Account -> MixedAmount
|
|
||||||
exclusiveBalance = sumPostings . apostings
|
|
||||||
|
|
||||||
-- | Is the named account considered interesting for this ledger's balance report ?
|
|
||||||
-- We follow the style of ledger's balance command.
|
|
||||||
isInteresting :: [Opt] -> Ledger -> AccountName -> Bool
|
|
||||||
isInteresting opts l a | Flat `elem` opts = isInterestingFlat opts l a
|
|
||||||
| otherwise = isInterestingIndented opts l a
|
|
||||||
|
|
||||||
isInterestingFlat :: [Opt] -> Ledger -> AccountName -> Bool
|
|
||||||
isInterestingFlat opts l a = notempty || emptyflag
|
|
||||||
where
|
|
||||||
acct = ledgerAccount l a
|
|
||||||
notempty = not $ isZeroMixedAmount $ exclusiveBalance acct
|
|
||||||
emptyflag = Empty `elem` opts
|
|
||||||
|
|
||||||
isInterestingIndented :: [Opt] -> Ledger -> AccountName -> Bool
|
|
||||||
isInterestingIndented opts l a
|
|
||||||
| numinterestingsubs==1 && not atmaxdepth = notlikesub
|
|
||||||
| otherwise = notzero || emptyflag
|
|
||||||
where
|
|
||||||
atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depthFromOpts opts
|
|
||||||
emptyflag = Empty `elem` opts
|
|
||||||
acct = ledgerAccount l a
|
|
||||||
notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct
|
|
||||||
notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumPostings $ apostings acct
|
|
||||||
numinterestingsubs = length $ filter isInterestingTree subtrees
|
|
||||||
where
|
|
||||||
isInterestingTree = treeany (isInteresting opts l . aname)
|
|
||||||
subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a
|
|
||||||
|
|
||||||
tests_Hledger_Cli_Balance = TestList
|
tests_Hledger_Cli_Balance = TestList
|
||||||
[
|
[
|
||||||
]
|
]
|
||||||
|
@ -6,27 +6,17 @@ A ledger-compatible @print@ command.
|
|||||||
-}
|
-}
|
||||||
|
|
||||||
module Hledger.Cli.Print (
|
module Hledger.Cli.Print (
|
||||||
JournalReport
|
print'
|
||||||
,JournalReportItem
|
|
||||||
,print'
|
|
||||||
,journalReport
|
|
||||||
,showTransactions
|
,showTransactions
|
||||||
) where
|
) where
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Ord
|
|
||||||
|
|
||||||
import Hledger.Cli.Options
|
import Hledger.Cli.Options
|
||||||
import Hledger.Cli.Utils
|
import Hledger.Cli.Utils
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
import Prelude hiding (putStr)
|
import Prelude hiding (putStr)
|
||||||
import Hledger.Utils.UTF8 (putStr)
|
import Hledger.Utils.UTF8 (putStr)
|
||||||
|
import Hledger.Cli.Options
|
||||||
|
|
||||||
-- | A "journal report" is just a list of transactions.
|
|
||||||
type JournalReport = [JournalReportItem]
|
|
||||||
|
|
||||||
-- | The data for a single journal report item, representing one transaction.
|
|
||||||
type JournalReportItem = Transaction
|
|
||||||
|
|
||||||
-- | Print journal transactions in standard format.
|
-- | Print journal transactions in standard format.
|
||||||
print' :: [Opt] -> [String] -> Journal -> IO ()
|
print' :: [Opt] -> [String] -> Journal -> IO ()
|
||||||
@ -41,8 +31,3 @@ journalReportAsText :: [Opt] -> FilterSpec -> JournalReport -> String
|
|||||||
journalReportAsText opts _ items = concatMap (showTransactionForPrint effective) items
|
journalReportAsText opts _ items = concatMap (showTransactionForPrint effective) items
|
||||||
where effective = Effective `elem` opts
|
where effective = Effective `elem` opts
|
||||||
|
|
||||||
journalReport :: [Opt] -> FilterSpec -> Journal -> JournalReport
|
|
||||||
journalReport opts fspec j = sortBy (comparing tdate) $ jtxns $ filterJournalTransactions fspec j'
|
|
||||||
where
|
|
||||||
j' = journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j
|
|
||||||
|
|
||||||
|
@ -6,29 +6,15 @@ A ledger-compatible @register@ command.
|
|||||||
-}
|
-}
|
||||||
|
|
||||||
module Hledger.Cli.Register (
|
module Hledger.Cli.Register (
|
||||||
PostingRegisterReport
|
register
|
||||||
,PostingRegisterReportItem
|
|
||||||
,AccountRegisterReport
|
|
||||||
,AccountRegisterReportItem
|
|
||||||
,register
|
|
||||||
,postingRegisterReport
|
|
||||||
,accountRegisterReport
|
|
||||||
,journalRegisterReport
|
|
||||||
,postingRegisterReportAsText
|
,postingRegisterReportAsText
|
||||||
,showPostingWithBalanceForVty
|
,showPostingWithBalanceForVty
|
||||||
,ariDate
|
|
||||||
,ariBalance
|
|
||||||
,tests_Hledger_Cli_Register
|
,tests_Hledger_Cli_Register
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Ord
|
|
||||||
import Data.Time.Calendar
|
|
||||||
import Safe (headMay, lastMay)
|
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
import Text.ParserCombinators.Parsec
|
|
||||||
import Text.Printf
|
import Text.Printf
|
||||||
|
|
||||||
import Hledger.Cli.Options
|
import Hledger.Cli.Options
|
||||||
@ -39,39 +25,6 @@ import Prelude hiding (putStr)
|
|||||||
import Hledger.Utils.UTF8 (putStr)
|
import Hledger.Utils.UTF8 (putStr)
|
||||||
|
|
||||||
|
|
||||||
-- | A posting register report lists postings to one or more accounts,
|
|
||||||
-- with a running total. Postings may be actual postings, or aggregate
|
|
||||||
-- postings corresponding to a reporting interval.
|
|
||||||
type PostingRegisterReport = (String -- label for the running balance column XXX remove
|
|
||||||
,[PostingRegisterReportItem] -- line items, one per posting
|
|
||||||
)
|
|
||||||
|
|
||||||
-- | A single posting register line item, representing one posting.
|
|
||||||
type PostingRegisterReportItem = (Maybe (Day, String) -- transaction date and description if this is the first posting
|
|
||||||
,Posting -- the posting
|
|
||||||
,MixedAmount -- the running total after this posting
|
|
||||||
)
|
|
||||||
|
|
||||||
-- | An account register report lists transactions to a single account (or
|
|
||||||
-- possibly subs as well), with the accurate running account balance when
|
|
||||||
-- possible (otherwise, a running total.)
|
|
||||||
type AccountRegisterReport = (String -- label for the balance column, eg "balance" or "total"
|
|
||||||
,[AccountRegisterReportItem] -- line items, one per transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
-- | A single account register line item, representing one transaction to/from the focussed account.
|
|
||||||
type AccountRegisterReportItem = (Transaction -- the corresponding transaction
|
|
||||||
,Transaction -- the transaction with postings to the focussed account removed
|
|
||||||
,Bool -- is this a split (more than one other-account posting) ?
|
|
||||||
,String -- the (possibly aggregated) account info to display
|
|
||||||
,MixedAmount -- the (possibly aggregated) amount to display (sum of the other-account postings)
|
|
||||||
,MixedAmount -- the running balance for the focussed account after this transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
ariDate (t,_,_,_,_,_) = tdate t
|
|
||||||
ariBalance (_,_,_,_,_,Mixed a) = case a of [] -> "0"
|
|
||||||
(Amount{quantity=q}):_ -> show q
|
|
||||||
|
|
||||||
-- | Print a (posting) register report.
|
-- | Print a (posting) register report.
|
||||||
register :: [Opt] -> [String] -> Journal -> IO ()
|
register :: [Opt] -> [String] -> Journal -> IO ()
|
||||||
register opts args j = do
|
register opts args j = do
|
||||||
@ -104,278 +57,11 @@ postingRegisterReportItemAsText _ (dd, p, b) = concatTopPadded [datedesc, pstr,
|
|||||||
pstr = showPostingForRegister p
|
pstr = showPostingForRegister p
|
||||||
bal = padleft 12 (showMixedAmountOrZeroWithoutPrice b)
|
bal = padleft 12 (showMixedAmountOrZeroWithoutPrice b)
|
||||||
|
|
||||||
showPostingWithBalanceForVty showtxninfo p b = postingRegisterReportItemAsText [] $ mkitem showtxninfo p b
|
-- XXX
|
||||||
|
showPostingWithBalanceForVty showtxninfo p b = postingRegisterReportItemAsText [] $ mkpostingRegisterItem showtxninfo p b
|
||||||
totallabel = "Total"
|
|
||||||
balancelabel = "Balance"
|
|
||||||
|
|
||||||
-- | Get a ledger-style posting register report, with the specified options,
|
|
||||||
-- for the whole journal. See also "accountRegisterReport".
|
|
||||||
postingRegisterReport :: [Opt] -> FilterSpec -> Journal -> PostingRegisterReport
|
|
||||||
postingRegisterReport opts fspec j = (totallabel, postingRegisterItems ps nullposting startbal (+))
|
|
||||||
where
|
|
||||||
ps | interval == NoInterval = displayableps
|
|
||||||
| otherwise = summarisePostingsByInterval interval depth empty filterspan displayableps
|
|
||||||
(precedingps, displayableps, _) = postingsMatchingDisplayExpr (displayExprFromOpts opts)
|
|
||||||
$ depthClipPostings depth
|
|
||||||
$ journalPostings
|
|
||||||
$ filterJournalPostings fspec{depth=Nothing}
|
|
||||||
$ journalSelectingDateFromOpts opts
|
|
||||||
$ journalSelectingAmountFromOpts opts
|
|
||||||
j
|
|
||||||
startbal = sumPostings precedingps
|
|
||||||
filterspan = datespan fspec
|
|
||||||
(interval, depth, empty) = (intervalFromOpts opts, depthFromOpts opts, Empty `elem` opts)
|
|
||||||
|
|
||||||
-- | Generate posting register report line items.
|
|
||||||
postingRegisterItems :: [Posting] -> Posting -> MixedAmount -> (MixedAmount -> MixedAmount -> MixedAmount) -> [PostingRegisterReportItem]
|
|
||||||
postingRegisterItems [] _ _ _ = []
|
|
||||||
postingRegisterItems (p:ps) pprev b sumfn = i:(postingRegisterItems ps p b' sumfn)
|
|
||||||
where
|
|
||||||
i = mkitem isfirst p b'
|
|
||||||
isfirst = ptransaction p /= ptransaction pprev
|
|
||||||
b' = b `sumfn` pamount p
|
|
||||||
|
|
||||||
-- | Generate one register report line item, from a flag indicating
|
|
||||||
-- whether to include transaction info, a posting, and the current running
|
|
||||||
-- balance.
|
|
||||||
mkitem :: Bool -> Posting -> MixedAmount -> PostingRegisterReportItem
|
|
||||||
mkitem False p b = (Nothing, p, b)
|
|
||||||
mkitem True p b = (ds, p, b)
|
|
||||||
where ds = case ptransaction p of Just (Transaction{tdate=da,tdescription=de}) -> Just (da,de)
|
|
||||||
Nothing -> Just (nulldate,"")
|
|
||||||
|
|
||||||
-- | Date-sort and split a list of postings into three spans - postings matched
|
|
||||||
-- by the given display expression, and the preceding and following postings.
|
|
||||||
postingsMatchingDisplayExpr :: Maybe String -> [Posting] -> ([Posting],[Posting],[Posting])
|
|
||||||
postingsMatchingDisplayExpr d ps = (before, matched, after)
|
|
||||||
where
|
|
||||||
sorted = sortBy (comparing postingDate) ps
|
|
||||||
(before, rest) = break (displayExprMatches d) sorted
|
|
||||||
(matched, after) = span (displayExprMatches d) rest
|
|
||||||
|
|
||||||
-- | Does this display expression allow this posting to be displayed ?
|
|
||||||
-- Raises an error if the display expression can't be parsed.
|
|
||||||
displayExprMatches :: Maybe String -> Posting -> Bool
|
|
||||||
displayExprMatches Nothing _ = True
|
|
||||||
displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p
|
|
||||||
|
|
||||||
-- | Parse a hledger display expression, which is a simple date test like
|
|
||||||
-- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.
|
|
||||||
datedisplayexpr :: GenParser Char st (Posting -> Bool)
|
|
||||||
datedisplayexpr = do
|
|
||||||
char 'd'
|
|
||||||
op <- compareop
|
|
||||||
char '['
|
|
||||||
(y,m,d) <- smartdate
|
|
||||||
char ']'
|
|
||||||
let date = parsedate $ printf "%04s/%02s/%02s" y m d
|
|
||||||
test op = return $ (`op` date) . postingDate
|
|
||||||
case op of
|
|
||||||
"<" -> test (<)
|
|
||||||
"<=" -> test (<=)
|
|
||||||
"=" -> test (==)
|
|
||||||
"==" -> test (==)
|
|
||||||
">=" -> test (>=)
|
|
||||||
">" -> test (>)
|
|
||||||
_ -> mzero
|
|
||||||
where
|
|
||||||
compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]
|
|
||||||
|
|
||||||
-- | Get a ledger-style register report showing all matched transactions and postings.
|
|
||||||
-- Similar to "postingRegisterReport" except it uses matchers and
|
|
||||||
-- per-transaction report items like "accountRegisterReport".
|
|
||||||
journalRegisterReport :: [Opt] -> Journal -> Matcher -> AccountRegisterReport
|
|
||||||
journalRegisterReport _ Journal{jtxns=ts} m = (totallabel, items)
|
|
||||||
where
|
|
||||||
ts' = sortBy (comparing tdate) $ filter (not . null . tpostings) $ map (filterTransactionPostings m) ts
|
|
||||||
items = reverse $ accountRegisterReportItems m Nothing nullmixedamt id ts'
|
|
||||||
-- XXX items' first element should be the full transaction with all postings
|
|
||||||
|
|
||||||
-- | Get a conventional account register report, with the specified
|
|
||||||
-- options, for the currently focussed account (or possibly the focussed
|
|
||||||
-- account plus sub-accounts.) This differs from "postingRegisterReport"
|
|
||||||
-- in several ways:
|
|
||||||
--
|
|
||||||
-- 1. it shows transactions, from the point of view of the focussed
|
|
||||||
-- account. The other account's name and posted amount is displayed,
|
|
||||||
-- aggregated if there is more than one other account posting.
|
|
||||||
--
|
|
||||||
-- 2. With no transaction filtering in effect other than a start date, it
|
|
||||||
-- shows the accurate historical running balance for this
|
|
||||||
-- account. Otherwise it shows a running total starting at 0 like the posting register report.
|
|
||||||
--
|
|
||||||
-- 3. Currently this report does not handle reporting intervals.
|
|
||||||
--
|
|
||||||
-- 4. Report items will be most recent first.
|
|
||||||
--
|
|
||||||
accountRegisterReport :: [Opt] -> Journal -> Matcher -> Matcher -> AccountRegisterReport
|
|
||||||
accountRegisterReport opts j m thisacctmatcher = (label, items)
|
|
||||||
where
|
|
||||||
-- transactions affecting this account, in date order
|
|
||||||
ts = sortBy (comparing tdate) $ filter (matchesTransaction thisacctmatcher) $ jtxns j
|
|
||||||
-- starting balance: if we are filtering by a start date and nothing else,
|
|
||||||
-- the sum of postings to this account before that date; otherwise zero.
|
|
||||||
(startbal,label) | matcherIsNull m = (nullmixedamt, balancelabel)
|
|
||||||
| matcherIsStartDateOnly effective m = (sumPostings priorps, balancelabel)
|
|
||||||
| otherwise = (nullmixedamt, totallabel)
|
|
||||||
where
|
|
||||||
priorps = -- ltrace "priorps" $
|
|
||||||
filter (matchesPosting
|
|
||||||
(-- ltrace "priormatcher" $
|
|
||||||
MatchAnd [thisacctmatcher, tostartdatematcher]))
|
|
||||||
$ transactionsPostings ts
|
|
||||||
tostartdatematcher = MatchDate True (DateSpan Nothing startdate)
|
|
||||||
startdate = matcherStartDate effective m
|
|
||||||
effective = Effective `elem` opts
|
|
||||||
items = reverse $ accountRegisterReportItems m (Just thisacctmatcher) startbal negate ts
|
|
||||||
|
|
||||||
-- | Generate account register line items from a list of transactions,
|
|
||||||
-- using the provided query and "this account" matchers, starting balance,
|
|
||||||
-- sign-setting function and balance-summing function.
|
|
||||||
|
|
||||||
-- This is used for both accountRegisterReport and journalRegisterReport,
|
|
||||||
-- which makes it a bit overcomplicated.
|
|
||||||
accountRegisterReportItems :: Matcher -> Maybe Matcher -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [AccountRegisterReportItem]
|
|
||||||
accountRegisterReportItems _ _ _ _ [] = []
|
|
||||||
accountRegisterReportItems matcher thisacctmatcher bal signfn (t:ts) =
|
|
||||||
case i of Just i' -> i':is
|
|
||||||
Nothing -> is
|
|
||||||
where
|
|
||||||
tmatched@Transaction{tpostings=psmatched} = filterTransactionPostings matcher t
|
|
||||||
(psthisacct,psotheracct) = case thisacctmatcher of Just m -> partition (matchesPosting m) psmatched
|
|
||||||
Nothing -> ([],psmatched)
|
|
||||||
numotheraccts = length $ nub $ map paccount psotheracct
|
|
||||||
amt = sum $ map pamount psotheracct
|
|
||||||
acct | isNothing thisacctmatcher = summarisePostings psmatched -- journal register
|
|
||||||
| numotheraccts == 0 = "transfer between " ++ summarisePostingAccounts psthisacct
|
|
||||||
| otherwise = prefix ++ summarisePostingAccounts psotheracct
|
|
||||||
where prefix = maybe "" (\b -> if b then "from " else "to ") $ isNegativeMixedAmount amt
|
|
||||||
(i,bal') = case psmatched of
|
|
||||||
[] -> (Nothing,bal)
|
|
||||||
_ -> (Just (t, tmatched, numotheraccts > 1, acct, a, b), b)
|
|
||||||
where
|
|
||||||
a = signfn amt
|
|
||||||
b = bal + a
|
|
||||||
is = accountRegisterReportItems matcher thisacctmatcher bal' signfn ts
|
|
||||||
|
|
||||||
-- | Generate a short readable summary of some postings, like
|
|
||||||
-- "from (negatives) to (positives)".
|
|
||||||
summarisePostings :: [Posting] -> String
|
|
||||||
summarisePostings ps =
|
|
||||||
case (summarisePostingAccounts froms, summarisePostingAccounts tos) of
|
|
||||||
("",t) -> "to "++t
|
|
||||||
(f,"") -> "from "++f
|
|
||||||
(f,t) -> "from "++f++" to "++t
|
|
||||||
where
|
|
||||||
(froms,tos) = partition (fromMaybe False . isNegativeMixedAmount . pamount) ps
|
|
||||||
|
|
||||||
-- | Generate a simplified summary of some postings' accounts.
|
|
||||||
summarisePostingAccounts :: [Posting] -> String
|
|
||||||
summarisePostingAccounts = intercalate ", " . map accountLeafName . nub . map paccount
|
|
||||||
|
|
||||||
filterTransactionPostings :: Matcher -> Transaction -> Transaction
|
|
||||||
filterTransactionPostings m t@Transaction{tpostings=ps} = t{tpostings=filter (m `matchesPosting`) ps}
|
|
||||||
|
|
||||||
-- XXX confusing, refactor
|
|
||||||
|
|
||||||
-- | Convert a list of postings into summary postings. Summary postings
|
|
||||||
-- are one per account per interval and aggregated to the specified depth
|
|
||||||
-- if any.
|
|
||||||
summarisePostingsByInterval :: Interval -> Maybe Int -> Bool -> DateSpan -> [Posting] -> [Posting]
|
|
||||||
summarisePostingsByInterval interval depth empty filterspan ps = concatMap summarisespan $ splitSpan interval reportspan
|
|
||||||
where
|
|
||||||
summarisespan s = summarisePostingsInDateSpan s depth empty (postingsinspan s)
|
|
||||||
postingsinspan s = filter (isPostingInDateSpan s) ps
|
|
||||||
dataspan = postingsDateSpan ps
|
|
||||||
reportspan | empty = filterspan `orDatesFrom` dataspan
|
|
||||||
| otherwise = dataspan
|
|
||||||
|
|
||||||
-- | Given a date span (representing a reporting interval) and a list of
|
|
||||||
-- postings within it: aggregate the postings so there is only one per
|
|
||||||
-- account, and adjust their date/description so that they will render
|
|
||||||
-- as a summary for this interval.
|
|
||||||
--
|
|
||||||
-- As usual with date spans the end date is exclusive, but for display
|
|
||||||
-- purposes we show the previous day as end date, like ledger.
|
|
||||||
--
|
|
||||||
-- When a depth argument is present, postings to accounts of greater
|
|
||||||
-- depth are aggregated where possible.
|
|
||||||
--
|
|
||||||
-- The showempty flag includes spans with no postings and also postings
|
|
||||||
-- with 0 amount.
|
|
||||||
summarisePostingsInDateSpan :: DateSpan -> Maybe Int -> Bool -> [Posting] -> [Posting]
|
|
||||||
summarisePostingsInDateSpan (DateSpan b e) depth showempty ps
|
|
||||||
| null ps && (isNothing b || isNothing e) = []
|
|
||||||
| null ps && showempty = [summaryp]
|
|
||||||
| otherwise = summaryps'
|
|
||||||
where
|
|
||||||
summaryp = summaryPosting b' ("- "++ showDate (addDays (-1) e'))
|
|
||||||
b' = fromMaybe (maybe nulldate postingDate $ headMay ps) b
|
|
||||||
e' = fromMaybe (maybe (addDays 1 nulldate) postingDate $ lastMay ps) e
|
|
||||||
summaryPosting date desc = nullposting{ptransaction=Just nulltransaction{tdate=date,tdescription=desc}}
|
|
||||||
|
|
||||||
summaryps' = (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps
|
|
||||||
summaryps = [summaryp{paccount=a,pamount=balancetoshowfor a} | a <- clippedanames]
|
|
||||||
anames = sort $ nub $ map paccount ps
|
|
||||||
-- aggregate balances by account, like journalToLedger, then do depth-clipping
|
|
||||||
(_,_,exclbalof,inclbalof) = groupPostings ps
|
|
||||||
clippedanames = nub $ map (clipAccountName d) anames
|
|
||||||
isclipped a = accountNameLevel a >= d
|
|
||||||
d = fromMaybe 99999 $ depth
|
|
||||||
balancetoshowfor a =
|
|
||||||
(if isclipped a then inclbalof else exclbalof) (if null a then "top" else a)
|
|
||||||
|
|
||||||
-- | Clip the account names to the specified depth in a list of postings.
|
|
||||||
depthClipPostings :: Maybe Int -> [Posting] -> [Posting]
|
|
||||||
depthClipPostings depth = map (depthClipPosting depth)
|
|
||||||
|
|
||||||
-- | Clip a posting's account name to the specified depth.
|
|
||||||
depthClipPosting :: Maybe Int -> Posting -> Posting
|
|
||||||
depthClipPosting Nothing p = p
|
|
||||||
depthClipPosting (Just d) p@Posting{paccount=a} = p{paccount=clipAccountName d a}
|
|
||||||
|
|
||||||
|
|
||||||
tests_Hledger_Cli_Register :: Test
|
tests_Hledger_Cli_Register :: Test
|
||||||
tests_Hledger_Cli_Register = TestList
|
tests_Hledger_Cli_Register = TestList
|
||||||
[
|
[
|
||||||
|
|
||||||
"summarisePostingsByInterval" ~: do
|
|
||||||
summarisePostingsByInterval (Quarters 1) Nothing False (DateSpan Nothing Nothing) [] ~?= []
|
|
||||||
|
|
||||||
-- ,"summarisePostingsInDateSpan" ~: do
|
|
||||||
-- let gives (b,e,depth,showempty,ps) =
|
|
||||||
-- (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`)
|
|
||||||
-- let ps =
|
|
||||||
-- [
|
|
||||||
-- nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}
|
|
||||||
-- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 2]}
|
|
||||||
-- ,nullposting{lpdescription="desc",lpaccount="expenses:food", lpamount=Mixed [dollars 4]}
|
|
||||||
-- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 8]}
|
|
||||||
-- ]
|
|
||||||
-- ("2008/01/01","2009/01/01",0,9999,False,[]) `gives`
|
|
||||||
-- []
|
|
||||||
-- ("2008/01/01","2009/01/01",0,9999,True,[]) `gives`
|
|
||||||
-- [
|
|
||||||
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31"}
|
|
||||||
-- ]
|
|
||||||
-- ("2008/01/01","2009/01/01",0,9999,False,ts) `gives`
|
|
||||||
-- [
|
|
||||||
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food", lpamount=Mixed [dollars 4]}
|
|
||||||
-- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 10]}
|
|
||||||
-- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}
|
|
||||||
-- ]
|
|
||||||
-- ("2008/01/01","2009/01/01",0,2,False,ts) `gives`
|
|
||||||
-- [
|
|
||||||
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [dollars 15]}
|
|
||||||
-- ]
|
|
||||||
-- ("2008/01/01","2009/01/01",0,1,False,ts) `gives`
|
|
||||||
-- [
|
|
||||||
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [dollars 15]}
|
|
||||||
-- ]
|
|
||||||
-- ("2008/01/01","2009/01/01",0,0,False,ts) `gives`
|
|
||||||
-- [
|
|
||||||
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [dollars 15]}
|
|
||||||
-- ]
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user