diff --git a/hledger-web/.hledger/web/static/hledger.js b/hledger-web/.hledger/web/static/hledger.js index 02986c104..f0723bf8a 100644 --- a/hledger-web/.hledger/web/static/hledger.js +++ b/hledger-web/.hledger/web/static/hledger.js @@ -14,6 +14,7 @@ $(document).ready(function() { /* set up various show/hide toggles */ $('#search-help-link').click(function() { $('#search-help').slideToggle('fast'); }); $('#accounts-toggle-link').click(function() { $('#accounts').slideToggle('fast'); }); + $('.postings-toggle-link').click(function() { $(this).parent().parent().nextUntil(':not(.posting)').toggle(); event.preventDefault(); }); }); diff --git a/hledger-web/.hledger/web/static/style.css b/hledger-web/.hledger/web/static/style.css index b5050cc67..3231401ad 100644 --- a/hledger-web/.hledger/web/static/style.css +++ b/hledger-web/.hledger/web/static/style.css @@ -112,6 +112,9 @@ table.registerreport { border-spacing:0; } table.registerreport tr { vertical-align:top; } table.registerreport td { padding-bottom:0.2em; } table.registerreport .date { white-space:nowrap; } +table.registerreport tr.posting { display:none; font-size:smaller; } +table.registerreport tr.posting .account { padding-left:1.5em; } +table.registerreport tr.posting .amount { padding-right:0.5em; } tr.firstposting td { } tr.newday td { border-top: 1px solid black; } tr.newmonth td { border-top: 2px solid black; } diff --git a/hledger-web/.hledger/web/templates/accountregisterreportitem.hamlet b/hledger-web/.hledger/web/templates/accountregisterreportitem.hamlet index 866ccf28e..7a0a7631f 100644 --- a/hledger-web/.hledger/web/templates/accountregisterreportitem.hamlet +++ b/hledger-web/.hledger/web/templates/accountregisterreportitem.hamlet @@ -1,6 +1,24 @@ #{date} - #{elideRight 30 desc} - #{elideRight 40 acct} - #{mixedAmountAsHtml amt} + #{elideRight 30 desc} + + $if split + #{elideRight 40 acct} + + $if showamt + #{mixedAmountAsHtml amt} #{mixedAmountAsHtml bal} +$if split + $forall p <- tpostings t' +  #{elideRight 40 $ paccount p} + #{mixedAmountAsHtml $ pamount p} + diff --git a/hledger-web/.hledger/web/templates/balancereportitem.hamlet b/hledger-web/.hledger/web/templates/balancereportitem.hamlet index a656ea0cb..e705d41c6 100644 --- a/hledger-web/.hledger/web/templates/balancereportitem.hamlet +++ b/hledger-web/.hledger/web/templates/balancereportitem.hamlet @@ -1,12 +1,13 @@ #{adisplay} + #{adisplay} +subs -   - -others + +subs + #{mixedAmountAsHtml abal} (#{numpostingsinacct acct}) diff --git a/hledger-web/.hledger/web/templates/postingregisterreportitem.hamlet b/hledger-web/.hledger/web/templates/postingregisterreportitem.hamlet index cb87271a8..95c89ba04 100644 --- a/hledger-web/.hledger/web/templates/postingregisterreportitem.hamlet +++ b/hledger-web/.hledger/web/templates/postingregisterreportitem.hamlet @@ -1,6 +1,6 @@ #{date} #{elideRight 30 desc} - #{elideRight 40 acct} + #{elideRight 40 acct} #{mixedAmountAsHtml $ pamount posting} #{mixedAmountAsHtml b} diff --git a/hledger-web/Handlers.hs b/hledger-web/Handlers.hs index 012e4c0ef..d3d6b3de8 100644 --- a/hledger-web/Handlers.hs +++ b/hledger-web/Handlers.hs @@ -65,7 +65,7 @@ getRegisterR = do let sidecontent = balanceReportAsHtml opts vd{q=""} $ balanceReport opts nullfilterspec j maincontent = case inAccountMatcher qopts of Just m' -> accountRegisterReportAsHtml opts vd $ accountRegisterReport opts j m m' - Nothing -> postingRegisterReportAsHtml opts vd $ postingRegisterReport opts nullfilterspec $ filterJournalPostings2 m j + Nothing -> accountRegisterReportAsHtml opts vd $ journalRegisterReport opts j m editform' = editform vd defaultLayout $ do setTitle "hledger-web register" @@ -93,7 +93,7 @@ getRegisterOnlyR = do setTitle "hledger-web register only" addHamlet $ case inAccountMatcher qopts of Just m' -> accountRegisterReportAsHtml opts vd $ accountRegisterReport opts j m m' - Nothing -> postingRegisterReportAsHtml opts vd $ postingRegisterReport opts nullfilterspec $ filterJournalPostings2 m j + Nothing -> accountRegisterReportAsHtml opts vd $ journalRegisterReport opts j m postRegisterOnlyR :: Handler RepPlain postRegisterOnlyR = handlePost @@ -119,34 +119,37 @@ getAccountsJsonR = do -- helpers -accountUrl :: String -> String -accountUrl a = "inacct:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a) +accountQuery :: AccountName -> String +accountQuery a = "inacct:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a) -accountsUrl :: String -> String -accountsUrl a = "inaccts:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a) +accountsQuery :: AccountName -> String +accountsQuery a = "inaccts:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a) -accountsOnlyUrl :: String -> String -accountsOnlyUrl a = "inacctsonly:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a) +accountsOnlyQuery :: AccountName -> String +accountsOnlyQuery a = "inacctsonly:" ++ quoteIfSpaced a -- (accountNameToAccountRegex a) + +-- accountUrl :: AppRoute -> AccountName -> (AppRoute,[(String,ByteString)]) +accountUrl r a = (r, [("q",pack $ accountQuery a)]) -- | Render a balance report as HTML. balanceReportAsHtml :: [Opt] -> ViewData -> BalanceReport -> Hamlet AppRoute balanceReportAsHtml _ vd@VD{here=here,q=q,m=m,qopts=qopts,j=j} (items,total) = $(Settings.hamletFile "balancereport") where l = journalToLedger nullfilterspec j - numpostingsinacct = length . apostings . ledgerAccount l inacctmatcher = inAccountMatcher qopts allaccts = isNothing inacctmatcher itemAsHtml :: ViewData -> BalanceReportItem -> Hamlet AppRoute itemAsHtml VD{here=here,q=q} (acct, adisplay, aindent, abal) = $(Settings.hamletFile "balancereportitem") where + numpostings = length $ apostings $ ledgerAccount l acct depthclass = "depth"++show aindent inacctclass = case inacctmatcher of Just m -> if m `matchesAccount` acct then "inacct" else "notinacct" Nothing -> "" :: String indent = preEscapedString $ concat $ replicate (2 * aindent) " " - accturl = (here, [("q", pack $ accountUrl acct)]) - acctsurl = (here, [("q", pack $ accountsUrl acct)]) - acctsonlyurl = (here, [("q", pack $ accountsOnlyUrl acct)]) + acctquery = (here, [("q", pack $ accountQuery acct)]) + acctsquery = (here, [("q", pack $ accountsQuery acct)]) + acctsonlyquery = (here, [("q", pack $ accountsOnlyQuery acct)]) -- | Render a journal report as HTML. journalReportAsHtml :: [Opt] -> ViewData -> JournalReport -> Hamlet AppRoute @@ -158,54 +161,23 @@ journalReportAsHtml _ vd items = $(Settings.hamletFile "journalreport") evenodd = if even n then "even" else "odd" :: String txn = trimnl $ showTransaction t where trimnl = reverse . dropWhile (=='\n') . reverse --- | Render a register report as HTML. --- Journal-wide postings register, when no account has focus. -postingRegisterReportAsHtml :: [Opt] -> ViewData -> PostingRegisterReport -> Hamlet AppRoute -postingRegisterReportAsHtml _ vd (balancelabel,items) = $(Settings.hamletFile "postingregisterreport") - where - itemAsHtml :: ViewData -> (Int, Bool, Bool, Bool, PostingRegisterReportItem) -> Hamlet AppRoute - itemAsHtml VD{here=here} (n, newd, newm, newy, (ds, posting, b)) = $(Settings.hamletFile "postingregisterreportitem") - where - evenodd = if even n then "even" else "odd" :: String - datetransition | newm = "newmonth" - | newd = "newday" - | otherwise = "" :: String - (firstposting, date, desc) = case ds of Just (da, de) -> ("firstposting", show da, de) - Nothing -> ("", "", "") :: (String,String,String) - acct = paccount posting - accturl = (here, [("q", pack $ accountUrl acct)]) - --- Add incrementing transaction numbers to a list of register report items --- starting at 1. Also add three flags that are true if the date, month, --- and year is different from the previous item's. -numberPostingRegisterReportItems :: [PostingRegisterReportItem] -> [(Int,Bool,Bool,Bool,PostingRegisterReportItem)] -numberPostingRegisterReportItems [] = [] -numberPostingRegisterReportItems is = number 0 nulldate is - where - number :: Int -> Day -> [PostingRegisterReportItem] -> [(Int,Bool,Bool,Bool,PostingRegisterReportItem)] - number _ _ [] = [] - number n prevd (i@(Nothing, _, _) :is) = (n,False,False,False,i) :(number n prevd is) - number n prevd (i@(Just (d,_), _, _):is) = (n+1,newday,newmonth,newyear,i):(number (n+1) d is) - where - newday = d/=prevd - newmonth = dm/=prevdm || dy/=prevdy - newyear = dy/=prevdy - (dy,dm,_) = toGregorian d - (prevdy,prevdm,_) = toGregorian prevd - -- Account-specific transaction register, when an account is focussed. accountRegisterReportAsHtml :: [Opt] -> ViewData -> AccountRegisterReport -> Hamlet AppRoute accountRegisterReportAsHtml _ vd (balancelabel,items) = $(Settings.hamletFile "accountregisterreport") where itemAsHtml :: ViewData -> (Int, Bool, Bool, Bool, AccountRegisterReportItem) -> Hamlet AppRoute - itemAsHtml VD{here=here} (n, newd, newm, newy, (t, acct, amt, bal)) = $(Settings.hamletFile "accountregisterreportitem") + itemAsHtml VD{here=here} (n, newd, newm, newy, (t, t', split, acct, amt, bal)) = $(Settings.hamletFile "accountregisterreportitem") where evenodd = if even n then "even" else "odd" :: String datetransition | newm = "newmonth" | newd = "newday" | otherwise = "" :: String (firstposting, date, desc) = (False, show $ tdate t, tdescription t) - accturl = (here, [("q", pack $ accountUrl acct)]) + acctquery = (here, [("q", pack $ accountQuery acct)]) + showamt = not split || not (isZeroMixedAmount amt) + +stringIfLongerThan :: Int -> String -> String +stringIfLongerThan n s = if length s > n then s else "" numberAccountRegisterReportItems :: [AccountRegisterReportItem] -> [(Int,Bool,Bool,Bool,AccountRegisterReportItem)] numberAccountRegisterReportItems [] = [] @@ -213,7 +185,7 @@ numberAccountRegisterReportItems is = number 0 nulldate is where number :: Int -> Day -> [AccountRegisterReportItem] -> [(Int,Bool,Bool,Bool,AccountRegisterReportItem)] number _ _ [] = [] - number n prevd (i@(Transaction{tdate=d},_,_,_):is) = (n+1,newday,newmonth,newyear,i):(number (n+1) d is) + number n prevd (i@(Transaction{tdate=d},_,_,_,_,_):is) = (n+1,newday,newmonth,newyear,i):(number (n+1) d is) where newday = d/=prevd newmonth = dm/=prevdm || dy/=prevdy diff --git a/hledger/Hledger/Cli/Register.hs b/hledger/Hledger/Cli/Register.hs index da17d9f5c..40a0105c0 100644 --- a/hledger/Hledger/Cli/Register.hs +++ b/hledger/Hledger/Cli/Register.hs @@ -13,6 +13,7 @@ module Hledger.Cli.Register ( ,register ,postingRegisterReport ,accountRegisterReport + ,journalRegisterReport ,postingRegisterReportAsText ,showPostingWithBalanceForVty ,tests_Hledger_Cli_Register @@ -58,6 +59,8 @@ type AccountRegisterReport = (String -- label for the balan -- | 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 @@ -103,7 +106,7 @@ 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 (+)) +postingRegisterReport opts fspec j = (totallabel, postingRegisterItems ps nullposting startbal (+)) where ps | interval == NoInterval = displayableps | otherwise = summarisePostingsByInterval interval depth empty filterspan displayableps @@ -173,10 +176,19 @@ datedisplayexpr = do where compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"] --- | Get a quicken/gnucash-style 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: +-- | 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 opts j@Journal{jtxns=ts} m = (totallabel, items) + where + ts' = sortBy (comparing tdate) $ filter (not . null . tpostings) $ map (filterTransactionPostings m) ts + items = reverse $ accountRegisterReportItems m MatchAny nullmixedamt (+) ts' + +-- | 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, @@ -195,7 +207,6 @@ 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, sumfn) | matcherIsNull m = (nullmixedamt,balancelabel,(-)) @@ -210,35 +221,45 @@ accountRegisterReport opts j m thisacctmatcher = (label, items) tostartdatematcher = MatchDate True (DateSpan Nothing startdate) startdate = matcherStartDate effective m effective = Effective `elem` opts - - displaymatcher = -- ltrace "displaymatcher" $ - MatchAnd [negateMatcher thisacctmatcher, m] - - items = reverse $ accountRegisterReportItems ts displaymatcher nulltransaction startbal sumfn + items = reverse $ accountRegisterReportItems m thisacctmatcher startbal sumfn ts -- | Generate account register line items from a list of transactions, --- using the provided matcher (postings not matching this will not affect --- the displayed item), starting transaction, starting balance, and --- balance summing function. -accountRegisterReportItems :: [Transaction] -> Matcher -> Transaction -> MixedAmount -> (MixedAmount -> MixedAmount -> MixedAmount) -> [AccountRegisterReportItem] -accountRegisterReportItems [] _ _ _ _ = [] -accountRegisterReportItems (t@Transaction{tpostings=ps}:ts) displaymatcher _ bal sumfn = +-- using the provided query and "this account" matchers, starting balance, +-- and balance summing function. +accountRegisterReportItems :: Matcher -> Matcher -> MixedAmount -> (MixedAmount -> MixedAmount -> MixedAmount) -> [Transaction] -> [AccountRegisterReportItem] +accountRegisterReportItems _ _ _ _ [] = [] +accountRegisterReportItems matcher thisacctmatcher bal sumfn (t@Transaction{tpostings=ps}:ts) = case i of Just i' -> i':is Nothing -> is where - (i,bal'') = case filter (displaymatcher `matchesPosting`) ps of + thisacctps = tpostings $ filterTransactionPostings thisacctmatcher t + numthisacctsposted = length $ nub $ map paccount thisacctps + displaymatcher | numthisacctsposted > 1 = matcher + | otherwise = MatchAnd [negateMatcher thisacctmatcher, matcher] + t'@Transaction{tpostings=ps'} = filterTransactionPostings displaymatcher t + (i,bal'') = case ps' of [] -> (Nothing,bal) -- maybe a virtual transaction, or transfer to self - [p] -> (Just (t, acct, amt, bal'), bal') + [p] -> (Just (t, t', False, acct, amt, bal'), bal') where acct = paccount p amt = pamount p bal' = bal `sumfn` amt - ps' -> (Just (t,acct,amt,bal'), bal') + ps' -> (Just (t, t', True, acct, amt, bal'), bal') where - acct = "SPLIT ("++intercalate ", " (map (accountLeafName . paccount) ps')++")" + -- describe split as from ..., to ... (not always right) + acct = case (simplify tos, simplify froms) of + ([],ts) -> "to "++commafy ts + (fs,[]) -> "from "++commafy fs + (fs,ts) -> "to "++commafy ts++" from "++commafy fs + where (tos,froms) = partition (fromMaybe False . isNegativeMixedAmount . pamount) ps' + simplify = nub . map (accountLeafName . paccount) + commafy = intercalate ", " amt = sum $ map pamount ps' bal' = bal `sumfn` amt - is = (accountRegisterReportItems ts displaymatcher t bal'' sumfn) + is = accountRegisterReportItems matcher thisacctmatcher bal'' sumfn ts + +filterTransactionPostings :: Matcher -> Transaction -> Transaction +filterTransactionPostings m t@Transaction{tpostings=ps} = t{tpostings=filter (m `matchesPosting`) ps} -- XXX confusing, refactor