mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-07 21:15:19 +03:00
code: Strip extraneous trailing whitespace from Haskell sources
This commit is contained in:
parent
7e332fda20
commit
11d9e5eb6a
6
Shake.hs
6
Shake.hs
@ -497,7 +497,7 @@ main = do
|
||||
| pkg <- packages ]
|
||||
|
||||
phony "commandhelp" $ need commandtxts
|
||||
|
||||
|
||||
commandtxts |%> \out -> do
|
||||
let src = out -<.> "md"
|
||||
need [src]
|
||||
@ -695,7 +695,7 @@ main = do
|
||||
-- tagrelease: \
|
||||
-- $(call def-help,tagrelease, commit a release tag based on $(VERSIONFILE) for each package )
|
||||
-- for p in $(PACKAGES); do git tag -f $$p-$(VERSION); done
|
||||
|
||||
|
||||
-- MISC
|
||||
|
||||
-- Generate the web manuals based on the current checkout and save
|
||||
@ -777,7 +777,7 @@ wikiLink :: Markdown -> Markdown
|
||||
wikiLink =
|
||||
replaceBy wikilinkre wikilinkReplace .
|
||||
replaceBy labelledwikilinkre labelledwikilinkReplace
|
||||
|
||||
|
||||
-- regex stuff
|
||||
|
||||
-- couldn't figure out how to use match subgroups, so we don't
|
||||
|
@ -56,7 +56,7 @@ cmdmode = hledgerCommandMode
|
||||
[here| chart
|
||||
Generate a pie chart for the top account balances with the same sign,
|
||||
in SVG format.
|
||||
|
||||
|
||||
Based on the old hledger-chart package, this is not yet useful.
|
||||
It's supposed to show only balances of one sign, but this might be broken.
|
||||
|]
|
||||
|
@ -45,7 +45,7 @@ hledger smooth revenues:consulting | hledger -f- incomestatement -W
|
||||
|
||||
FLAGS
|
||||
|]
|
||||
[]
|
||||
[]
|
||||
[generalflagsgroup1]
|
||||
[]
|
||||
([], Just $ argsFlag "ACCT")
|
||||
@ -64,7 +64,7 @@ main = do
|
||||
q = queryFromOpts today ropts
|
||||
acct = T.pack $ headDef (error' "Please provide an account name argument") args
|
||||
pr = postingsReport ropts (And [Acct $ accountNameToAccountRegex acct, q]) j
|
||||
|
||||
|
||||
-- dates of postings to acct (in report)
|
||||
pdates = map (postingDate . fourth5) (snd pr)
|
||||
-- the specified report end date or today's date
|
||||
|
@ -76,8 +76,8 @@ accountsFromPostings ps =
|
||||
in
|
||||
acctsflattened
|
||||
|
||||
-- | Convert a list of account names to a tree of Account objects,
|
||||
-- with just the account names filled in.
|
||||
-- | Convert a list of account names to a tree of Account objects,
|
||||
-- with just the account names filled in.
|
||||
-- A single root account with the given name is added.
|
||||
accountTree :: AccountName -> [AccountName] -> Account
|
||||
accountTree rootname as = nullacct{aname=rootname, asubs=map (uncurry accountTree') $ M.assocs m }
|
||||
@ -193,7 +193,7 @@ filterAccounts p a
|
||||
| otherwise = concatMap (filterAccounts p) (asubs a)
|
||||
|
||||
-- | Sort each group of siblings in an account tree by inclusive amount,
|
||||
-- so that the accounts with largest normal balances are listed first.
|
||||
-- so that the accounts with largest normal balances are listed first.
|
||||
-- The provided normal balance sign determines whether normal balances
|
||||
-- are negative or positive, affecting the sort order. Ie,
|
||||
-- if balances are normally negative, then the most negative balances
|
||||
@ -217,10 +217,10 @@ accountSetDeclarationInfo j a@Account{..} =
|
||||
-- | Sort account names by the order in which they were declared in
|
||||
-- the journal, at each level of the account tree (ie within each
|
||||
-- group of siblings). Undeclared accounts are sorted last and
|
||||
-- alphabetically.
|
||||
-- alphabetically.
|
||||
-- This is hledger's default sort for reports organised by account.
|
||||
-- The account list is converted to a tree temporarily, adding any
|
||||
-- missing parents; these can be kept (suitable for a tree-mode report)
|
||||
-- missing parents; these can be kept (suitable for a tree-mode report)
|
||||
-- or removed (suitable for a flat-mode report).
|
||||
--
|
||||
sortAccountNamesByDeclaration :: Journal -> Bool -> [AccountName] -> [AccountName]
|
||||
@ -235,14 +235,14 @@ sortAccountNamesByDeclaration j keepparents as =
|
||||
as
|
||||
|
||||
-- | Sort each group of siblings in an account tree by declaration order, then account name.
|
||||
-- So each group will contain first the declared accounts,
|
||||
-- in the same order as their account directives were parsed,
|
||||
-- and then the undeclared accounts, sorted by account name.
|
||||
-- So each group will contain first the declared accounts,
|
||||
-- in the same order as their account directives were parsed,
|
||||
-- and then the undeclared accounts, sorted by account name.
|
||||
sortAccountTreeByDeclaration :: Account -> Account
|
||||
sortAccountTreeByDeclaration a
|
||||
| null $ asubs a = a
|
||||
| otherwise = a{asubs=
|
||||
sortOn accountDeclarationOrderAndName $
|
||||
sortOn accountDeclarationOrderAndName $
|
||||
map sortAccountTreeByDeclaration $ asubs a
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ module Hledger.Data.AccountName (
|
||||
,expandAccountName
|
||||
,expandAccountNames
|
||||
,isAccountNamePrefixOf
|
||||
-- ,isAccountRegex
|
||||
-- ,isAccountRegex
|
||||
,isSubAccountNameOf
|
||||
,parentAccountName
|
||||
,parentAccountNames
|
||||
@ -50,7 +50,7 @@ import Data.Tree
|
||||
import Text.Printf
|
||||
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
|
||||
-- $setup
|
||||
-- >>> :set -XOverloadedStrings
|
||||
@ -88,13 +88,13 @@ accountNameLevel "" = 0
|
||||
accountNameLevel a = T.length (T.filter (==acctsepchar) a) + 1
|
||||
|
||||
-- | A top-level account prefixed to some accounts in budget reports.
|
||||
-- Defined here so it can be ignored by accountNameDrop.
|
||||
-- Defined here so it can be ignored by accountNameDrop.
|
||||
unbudgetedAccountName :: T.Text
|
||||
unbudgetedAccountName = "<unbudgeted>"
|
||||
|
||||
-- | Remove some number of account name components from the front of the account name.
|
||||
-- If the special "<unbudgeted>" top-level account is present, it is preserved and
|
||||
-- dropping affects the rest of the account name.
|
||||
-- dropping affects the rest of the account name.
|
||||
accountNameDrop :: Int -> AccountName -> AccountName
|
||||
accountNameDrop n a
|
||||
| a == unbudgetedAccountName = a
|
||||
@ -103,7 +103,7 @@ accountNameDrop n a
|
||||
"" -> unbudgetedAccountName
|
||||
a' -> unbudgetedAccountAndSep <> a'
|
||||
| otherwise = accountNameFromComponents $ drop n $ accountNameComponents a
|
||||
where
|
||||
where
|
||||
unbudgetedAccountAndSep = unbudgetedAccountName <> acctsep
|
||||
|
||||
-- | Sorted unique account names implied by these account names,
|
||||
|
@ -139,7 +139,7 @@ import Text.Printf
|
||||
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.Commodity
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
|
||||
|
||||
deriving instance Show MarketPrice
|
||||
@ -148,7 +148,7 @@ deriving instance Show MarketPrice
|
||||
-------------------------------------------------------------------------------
|
||||
-- Amount styles
|
||||
|
||||
-- | Default amount style
|
||||
-- | Default amount style
|
||||
amountstyle = AmountStyle L False 0 (Just '.') Nothing
|
||||
|
||||
|
||||
@ -222,10 +222,10 @@ amountToCost styles = styleAmount styles . costOfAmount
|
||||
-- | Replace an amount's TotalPrice, if it has one, with an equivalent UnitPrice.
|
||||
-- Has no effect on amounts without one.
|
||||
-- Also increases the unit price's display precision to show one extra decimal place,
|
||||
-- to help keep transaction amounts balancing.
|
||||
-- to help keep transaction amounts balancing.
|
||||
-- Does Decimal division, might be some rounding/irrational number issues.
|
||||
amountTotalPriceToUnitPrice :: Amount -> Amount
|
||||
amountTotalPriceToUnitPrice
|
||||
amountTotalPriceToUnitPrice
|
||||
a@Amount{aquantity=q, aprice=Just (TotalPrice pa@Amount{aquantity=pq, astyle=ps@AmountStyle{asprecision=pp}})}
|
||||
= a{aprice = Just $ UnitPrice pa{aquantity=abs (pq/q), astyle=ps{asprecision=pp+1}}}
|
||||
amountTotalPriceToUnitPrice a = a
|
||||
@ -317,20 +317,20 @@ showAmountDebug Amount{..} = printf "Amount {acommodity=%s, aquantity=%s, aprice
|
||||
showAmountWithoutPrice :: Amount -> String
|
||||
showAmountWithoutPrice a = showAmount a{aprice=Nothing}
|
||||
|
||||
-- | Set an amount's internal precision, ie rounds the Decimal representing
|
||||
-- | Set an amount's internal precision, ie rounds the Decimal representing
|
||||
-- the amount's quantity to some number of decimal places.
|
||||
-- Rounding is done with Data.Decimal's default roundTo function:
|
||||
-- "If the value ends in 5 then it is rounded to the nearest even value (Banker's Rounding)".
|
||||
-- Does not change the amount's display precision.
|
||||
-- Intended only for internal use, eg when comparing amounts in tests.
|
||||
-- Intended only for internal use, eg when comparing amounts in tests.
|
||||
setAmountInternalPrecision :: Int -> Amount -> Amount
|
||||
setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{
|
||||
astyle=s{asprecision=p}
|
||||
setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{
|
||||
astyle=s{asprecision=p}
|
||||
,aquantity=roundTo (fromIntegral p) q
|
||||
}
|
||||
|
||||
-- | Set an amount's internal precision, flipped.
|
||||
-- Intended only for internal use, eg when comparing amounts in tests.
|
||||
-- Intended only for internal use, eg when comparing amounts in tests.
|
||||
withInternalPrecision :: Amount -> Int -> Amount
|
||||
withInternalPrecision = flip setAmountInternalPrecision
|
||||
|
||||
@ -366,7 +366,7 @@ styleAmount :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount
|
||||
styleAmount styles a =
|
||||
case M.lookup (acommodity a) styles of
|
||||
Just s -> a{astyle=s}
|
||||
Nothing -> a
|
||||
Nothing -> a
|
||||
|
||||
-- | Get the string representation of an amount, based on its
|
||||
-- commodity's display settings. String representations equivalent to
|
||||
@ -375,7 +375,7 @@ styleAmount styles a =
|
||||
showAmount :: Amount -> String
|
||||
showAmount = showAmountHelper False
|
||||
|
||||
-- | Colour version. For a negative amount, adds ANSI codes to change the colour,
|
||||
-- | Colour version. For a negative amount, adds ANSI codes to change the colour,
|
||||
-- currently to hard-coded red.
|
||||
cshowAmount :: Amount -> String
|
||||
cshowAmount a =
|
||||
@ -589,7 +589,7 @@ multiplyMixedAmountAndPrice n = mapMixedAmount (multiplyAmountAndPrice n)
|
||||
-- | Calculate the average of some mixed amounts.
|
||||
averageMixedAmounts :: [MixedAmount] -> MixedAmount
|
||||
averageMixedAmounts [] = 0
|
||||
averageMixedAmounts as = fromIntegral (length as) `divideMixedAmount` sum as
|
||||
averageMixedAmounts as = fromIntegral (length as) `divideMixedAmount` sum as
|
||||
|
||||
-- | Is this mixed amount negative, if it can be normalised to a single commodity ?
|
||||
isNegativeMixedAmount :: MixedAmount -> Maybe Bool
|
||||
@ -620,7 +620,7 @@ isReallyZeroMixedAmountCost = isReallyZeroMixedAmount . costOfMixedAmount
|
||||
|
||||
-- | Given a map of standard amount display styles, apply the appropriate ones to each individual amount.
|
||||
styleMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
|
||||
styleMixedAmount styles (Mixed as) = Mixed $ map (styleAmount styles) as
|
||||
styleMixedAmount styles (Mixed as) = Mixed $ map (styleAmount styles) as
|
||||
|
||||
-- | Get the string representation of a mixed amount, after
|
||||
-- normalising it to one amount per commodity. Assumes amounts have
|
||||
@ -713,7 +713,7 @@ canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> M
|
||||
canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as
|
||||
|
||||
-- | Replace each component amount's TotalPrice, if it has one, with an equivalent UnitPrice.
|
||||
-- Has no effect on amounts without one.
|
||||
-- Has no effect on amounts without one.
|
||||
-- Does Decimal division, might be some rounding/irrational number issues.
|
||||
mixedAmountTotalPriceToUnitPrice :: MixedAmount -> MixedAmount
|
||||
mixedAmountTotalPriceToUnitPrice (Mixed as) = Mixed $ map amountTotalPriceToUnitPrice as
|
||||
@ -731,17 +731,17 @@ tests_Amount = tests "Amount" [
|
||||
,costOfAmount (eur 1){aprice=Just $ TotalPrice $ usd 2} `is` usd 2
|
||||
,costOfAmount (eur (-1)){aprice=Just $ TotalPrice $ usd 2} `is` usd (-2)
|
||||
]
|
||||
|
||||
|
||||
,tests "isZeroAmount" [
|
||||
expect $ isZeroAmount amount
|
||||
,expect $ isZeroAmount $ usd 0
|
||||
]
|
||||
|
||||
|
||||
,tests "negating amounts" [
|
||||
negate (usd 1) `is` (usd 1){aquantity= -1}
|
||||
,let b = (usd 1){aprice=Just $ UnitPrice $ eur 2} in negate b `is` b{aquantity= -1}
|
||||
]
|
||||
|
||||
|
||||
,tests "adding amounts without prices" [
|
||||
(usd 1.23 + usd (-1.23)) `is` usd 0
|
||||
,(usd 1.23 + usd (-1.23)) `is` usd 0
|
||||
@ -753,7 +753,7 @@ tests_Amount = tests "Amount" [
|
||||
-- adding different commodities assumes conversion rate 1
|
||||
,expect $ isZeroAmount (usd 1.23 - eur 1.23)
|
||||
]
|
||||
|
||||
|
||||
,tests "showAmount" [
|
||||
showAmount (usd 0 + gbp 0) `is` "0"
|
||||
]
|
||||
@ -770,7 +770,7 @@ tests_Amount = tests "Amount" [
|
||||
])
|
||||
`is` Mixed [usd 0 `withPrecision` 3]
|
||||
]
|
||||
|
||||
|
||||
,tests "adding mixed amounts with total prices" [
|
||||
sum (map (Mixed . (:[]))
|
||||
[usd 1 @@ eur 1
|
||||
@ -780,7 +780,7 @@ tests_Amount = tests "Amount" [
|
||||
,usd (-2) @@ eur 1
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
,tests "showMixedAmount" [
|
||||
showMixedAmount (Mixed [usd 1]) `is` "$1.00"
|
||||
,showMixedAmount (Mixed [usd 1 `at` eur 2]) `is` "$1.00 @ €2.00"
|
||||
@ -788,27 +788,27 @@ tests_Amount = tests "Amount" [
|
||||
,showMixedAmount (Mixed []) `is` "0"
|
||||
,showMixedAmount missingmixedamt `is` ""
|
||||
]
|
||||
|
||||
|
||||
,tests "showMixedAmountWithoutPrice" $
|
||||
let a = usd 1 `at` eur 2 in
|
||||
let a = usd 1 `at` eur 2 in
|
||||
[
|
||||
showMixedAmountWithoutPrice (Mixed [a]) `is` "$1.00"
|
||||
,showMixedAmountWithoutPrice (Mixed [a, -a]) `is` "0"
|
||||
]
|
||||
|
||||
|
||||
,tests "normaliseMixedAmount" [
|
||||
test "a missing amount overrides any other amounts" $
|
||||
test "a missing amount overrides any other amounts" $
|
||||
normaliseMixedAmount (Mixed [usd 1, missingamt]) `is` missingmixedamt
|
||||
,test "unpriced same-commodity amounts are combined" $
|
||||
,test "unpriced same-commodity amounts are combined" $
|
||||
normaliseMixedAmount (Mixed [usd 0, usd 2]) `is` Mixed [usd 2]
|
||||
,test "amounts with same unit price are combined" $
|
||||
,test "amounts with same unit price are combined" $
|
||||
normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 1]) `is` Mixed [usd 2 `at` eur 1]
|
||||
,test "amounts with different unit prices are not combined" $
|
||||
,test "amounts with different unit prices are not combined" $
|
||||
normaliseMixedAmount (Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]) `is` Mixed [usd 1 `at` eur 1, usd 1 `at` eur 2]
|
||||
,test "amounts with total prices are not combined" $
|
||||
normaliseMixedAmount (Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]) `is` Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1]
|
||||
]
|
||||
|
||||
|
||||
,tests "normaliseMixedAmountSquashPricesForDisplay" [
|
||||
normaliseMixedAmountSquashPricesForDisplay (Mixed []) `is` Mixed [nullamt]
|
||||
,expect $ isZeroMixedAmount $ normaliseMixedAmountSquashPricesForDisplay
|
||||
|
@ -156,7 +156,7 @@ spansSpan spans = DateSpan (maybe Nothing spanStart $ headMay spans) (maybe Noth
|
||||
-- If no interval is specified, the original span is returned.
|
||||
-- If the original span is the null date span, ie unbounded, the null date span is returned.
|
||||
-- If the original span is empty, eg if the end date is <= the start date, no spans are returned.
|
||||
--
|
||||
--
|
||||
--
|
||||
-- ==== Examples:
|
||||
-- >>> let t i d1 d2 = splitSpan i $ mkdatespan d1 d2
|
||||
@ -531,19 +531,19 @@ startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day
|
||||
-- Examples: lets take 2017-11-22. Year-long intervals covering it that
|
||||
-- starts before Nov 22 will start in 2017. However
|
||||
-- intervals that start after Nov 23rd should start in 2016:
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- >>> nthdayofyearcontaining 11 21 wed22nd
|
||||
-- 2017-11-21
|
||||
-- 2017-11-21
|
||||
-- >>> nthdayofyearcontaining 11 22 wed22nd
|
||||
-- 2017-11-22
|
||||
-- 2017-11-22
|
||||
-- >>> nthdayofyearcontaining 11 23 wed22nd
|
||||
-- 2016-11-23
|
||||
-- 2016-11-23
|
||||
-- >>> nthdayofyearcontaining 12 02 wed22nd
|
||||
-- 2016-12-02
|
||||
-- 2016-12-02
|
||||
-- >>> nthdayofyearcontaining 12 31 wed22nd
|
||||
-- 2016-12-31
|
||||
-- 2016-12-31
|
||||
-- >>> nthdayofyearcontaining 1 1 wed22nd
|
||||
-- 2017-01-01
|
||||
-- 2017-01-01
|
||||
nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day
|
||||
nthdayofyearcontaining m md date
|
||||
| not (validMonth $ show m) = error' $ "nthdayofyearcontaining: invalid month "++show m
|
||||
@ -555,23 +555,23 @@ nthdayofyearcontaining m md date
|
||||
s = startofyear date
|
||||
|
||||
-- | For given date d find month-long interval that starts on nth day of month
|
||||
-- and covers it.
|
||||
-- and covers it.
|
||||
-- The given day of month should be basically valid (1-31), or an error is raised.
|
||||
--
|
||||
-- Examples: lets take 2017-11-22. Month-long intervals covering it that
|
||||
-- start on 1st-22nd of month will start in Nov. However
|
||||
-- intervals that start on 23rd-30th of month should start in Oct:
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- >>> nthdayofmonthcontaining 1 wed22nd
|
||||
-- 2017-11-01
|
||||
-- 2017-11-01
|
||||
-- >>> nthdayofmonthcontaining 12 wed22nd
|
||||
-- 2017-11-12
|
||||
-- 2017-11-12
|
||||
-- >>> nthdayofmonthcontaining 22 wed22nd
|
||||
-- 2017-11-22
|
||||
-- 2017-11-22
|
||||
-- >>> nthdayofmonthcontaining 23 wed22nd
|
||||
-- 2017-10-23
|
||||
-- 2017-10-23
|
||||
-- >>> nthdayofmonthcontaining 30 wed22nd
|
||||
-- 2017-10-30
|
||||
-- 2017-10-30
|
||||
nthdayofmonthcontaining :: MonthDay -> Day -> Day
|
||||
nthdayofmonthcontaining md date
|
||||
| not (validDay $ show md) = error' $ "nthdayofmonthcontaining: invalid day " ++show md
|
||||
@ -582,22 +582,22 @@ nthdayofmonthcontaining md date
|
||||
s = startofmonth date
|
||||
|
||||
-- | For given date d find week-long interval that starts on nth day of week
|
||||
-- and covers it.
|
||||
-- and covers it.
|
||||
--
|
||||
-- Examples: 2017-11-22 is Wed. Week-long intervals that cover it and
|
||||
-- start on Mon, Tue or Wed will start in the same week. However
|
||||
-- intervals that start on Thu or Fri should start in prev week:
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- intervals that start on Thu or Fri should start in prev week:
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- >>> nthdayofweekcontaining 1 wed22nd
|
||||
-- 2017-11-20
|
||||
-- 2017-11-20
|
||||
-- >>> nthdayofweekcontaining 2 wed22nd
|
||||
-- 2017-11-21
|
||||
-- >>> nthdayofweekcontaining 3 wed22nd
|
||||
-- 2017-11-22
|
||||
-- 2017-11-22
|
||||
-- >>> nthdayofweekcontaining 4 wed22nd
|
||||
-- 2017-11-16
|
||||
-- 2017-11-16
|
||||
-- >>> nthdayofweekcontaining 5 wed22nd
|
||||
-- 2017-11-17
|
||||
-- 2017-11-17
|
||||
nthdayofweekcontaining :: WeekDay -> Day -> Day
|
||||
nthdayofweekcontaining n d | nthOfSameWeek <= d = nthOfSameWeek
|
||||
| otherwise = nthOfPrevWeek
|
||||
@ -606,12 +606,12 @@ nthdayofweekcontaining n d | nthOfSameWeek <= d = nthOfSameWeek
|
||||
s = startofweek d
|
||||
|
||||
-- | For given date d find month-long interval that starts on nth weekday of month
|
||||
-- and covers it.
|
||||
-- and covers it.
|
||||
--
|
||||
-- Examples: 2017-11-22 is 3rd Wed of Nov. Month-long intervals that cover it and
|
||||
-- start on 1st-4th Wed will start in Nov. However
|
||||
-- intervals that start on 4th Thu or Fri or later should start in Oct:
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- intervals that start on 4th Thu or Fri or later should start in Oct:
|
||||
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||
-- >>> nthweekdayofmonthcontaining 1 3 wed22nd
|
||||
-- 2017-11-01
|
||||
-- >>> nthweekdayofmonthcontaining 3 2 wed22nd
|
||||
@ -630,12 +630,12 @@ nthweekdayofmonthcontaining n wd d | nthWeekdaySameMonth <= d = nthWeekdaySameM
|
||||
|
||||
-- | Advance to nth weekday wd after given start day s
|
||||
advancetonthweekday :: Int -> WeekDay -> Day -> Day
|
||||
advancetonthweekday n wd s =
|
||||
advancetonthweekday n wd s =
|
||||
maybe err (addWeeks (n-1)) $ firstMatch (>=s) $ iterate (addWeeks 1) $ firstweekday s
|
||||
where
|
||||
err = error' "advancetonthweekday: should not happen"
|
||||
addWeeks k = addDays (7 * fromIntegral k)
|
||||
firstMatch p = headMay . dropWhile (not . p)
|
||||
firstMatch p = headMay . dropWhile (not . p)
|
||||
firstweekday = addDays (fromIntegral wd-1) . startofweek
|
||||
|
||||
----------------------------------------------------------------------
|
||||
@ -834,7 +834,7 @@ md = do
|
||||
failIfInvalidDay d
|
||||
return ("",m,d)
|
||||
|
||||
-- These are compared case insensitively, and should all be kept lower case.
|
||||
-- These are compared case insensitively, and should all be kept lower case.
|
||||
months = ["january","february","march","april","may","june",
|
||||
"july","august","september","october","november","december"]
|
||||
monthabbrevs = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"]
|
||||
@ -864,8 +864,8 @@ weekday = do
|
||||
wday <- T.toLower <$> (choice . map string' $ weekdays ++ weekdayabbrevs)
|
||||
case catMaybes $ [wday `elemIndex` weekdays, wday `elemIndex` weekdayabbrevs] of
|
||||
(i:_) -> return (i+1)
|
||||
[] -> fail $ "weekday: should not happen: attempted to find " <>
|
||||
show wday <> " in " <> show (weekdays ++ weekdayabbrevs)
|
||||
[] -> fail $ "weekday: should not happen: attempted to find " <>
|
||||
show wday <> " in " <> show (weekdays ++ weekdayabbrevs)
|
||||
|
||||
today,yesterday,tomorrow :: TextParser m SmartDate
|
||||
today = string' "today" >> return ("","","today")
|
||||
@ -909,7 +909,7 @@ lastthisnextthing = do
|
||||
-- >>> p "every 2nd day"
|
||||
-- Right (DayOfMonth 2,DateSpan -)
|
||||
-- >>> p "every 2nd day 2009-"
|
||||
-- Right (DayOfMonth 2,DateSpan 2009/01/01-)
|
||||
-- Right (DayOfMonth 2,DateSpan 2009/01/01-)
|
||||
-- >>> p "every 29th Nov"
|
||||
-- Right (DayOfYear 11 29,DateSpan -)
|
||||
-- >>> p "every 29th nov -2009"
|
||||
@ -1007,9 +1007,9 @@ reportingintervalp = choice' [
|
||||
string' "of"
|
||||
skipMany spacenonewline
|
||||
string' period
|
||||
|
||||
|
||||
optOf_ period = optional $ try $ of_ period
|
||||
|
||||
|
||||
nth = do n <- some digitChar
|
||||
choice' $ map string' ["st","nd","rd","th"]
|
||||
return $ read n
|
||||
|
@ -111,7 +111,7 @@ import Data.Tree
|
||||
import System.Time (ClockTime(TOD))
|
||||
import Text.Printf
|
||||
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.AccountName
|
||||
import Hledger.Data.Amount
|
||||
@ -160,12 +160,12 @@ instance Show Journal where
|
||||
-- ]
|
||||
|
||||
-- The monoid instance for Journal is useful for two situations.
|
||||
--
|
||||
--
|
||||
-- 1. concatenating finalised journals, eg with multiple -f options:
|
||||
-- FIRST <> SECOND. The second's list fields are appended to the
|
||||
-- first's, map fields are combined, transaction counts are summed,
|
||||
-- the parse state of the second is kept.
|
||||
--
|
||||
--
|
||||
-- 2. merging a child parsed journal, eg with the include directive:
|
||||
-- CHILD <> PARENT. A parsed journal's data is in reverse order, so
|
||||
-- this gives what we want.
|
||||
@ -268,7 +268,7 @@ journalPostings = concatMap tpostings . jtxns
|
||||
journalAccountNamesUsed :: Journal -> [AccountName]
|
||||
journalAccountNamesUsed = accountNamesFromPostings . journalPostings
|
||||
|
||||
-- | Sorted unique account names implied by this journal's transactions -
|
||||
-- | Sorted unique account names implied by this journal's transactions -
|
||||
-- accounts posted to and all their implied parent accounts.
|
||||
journalAccountNamesImplied :: Journal -> [AccountName]
|
||||
journalAccountNamesImplied = expandAccountNames . journalAccountNamesUsed
|
||||
@ -289,31 +289,31 @@ journalAccountNamesDeclaredOrImplied j = nub $ sort $ journalAccountNamesDeclare
|
||||
|
||||
-- | Convenience/compatibility alias for journalAccountNamesDeclaredOrImplied.
|
||||
journalAccountNames :: Journal -> [AccountName]
|
||||
journalAccountNames = journalAccountNamesDeclaredOrImplied
|
||||
journalAccountNames = journalAccountNamesDeclaredOrImplied
|
||||
|
||||
journalAccountNameTree :: Journal -> Tree AccountName
|
||||
journalAccountNameTree = accountNameTreeFrom . journalAccountNames
|
||||
|
||||
-- queries for standard account types
|
||||
|
||||
-- | Get a query for accounts of a certain type (Asset, Liability..) in this journal.
|
||||
-- The query will match all accounts which were declared as that type by account directives,
|
||||
-- plus all their subaccounts which have not been declared as a different type.
|
||||
-- If no accounts were declared as this type, the query will instead match accounts
|
||||
-- | Get a query for accounts of a certain type (Asset, Liability..) in this journal.
|
||||
-- The query will match all accounts which were declared as that type by account directives,
|
||||
-- plus all their subaccounts which have not been declared as a different type.
|
||||
-- If no accounts were declared as this type, the query will instead match accounts
|
||||
-- with names matched by the provided case-insensitive regular expression.
|
||||
journalAccountTypeQuery :: AccountType -> Regexp -> Journal -> Query
|
||||
journalAccountTypeQuery atype fallbackregex j =
|
||||
case M.lookup atype (jdeclaredaccounttypes j) of
|
||||
Nothing -> Acct fallbackregex
|
||||
Just as ->
|
||||
-- XXX Query isn't able to match account type since that requires extra info from the journal.
|
||||
-- XXX Query isn't able to match account type since that requires extra info from the journal.
|
||||
-- So we do a hacky search by name instead.
|
||||
And [
|
||||
And [
|
||||
Or $ map (Acct . accountNameToAccountRegex) as
|
||||
,Not $ Or $ map (Acct . accountNameToAccountRegex) differentlytypedsubs
|
||||
]
|
||||
where
|
||||
differentlytypedsubs = concat
|
||||
differentlytypedsubs = concat
|
||||
[subs | (t,bs) <- M.toList (jdeclaredaccounttypes j)
|
||||
, t /= atype
|
||||
, let subs = [b | b <- bs, any (`isAccountNamePrefixOf` b) as]
|
||||
@ -321,35 +321,35 @@ journalAccountTypeQuery atype fallbackregex j =
|
||||
|
||||
-- | A query for accounts in this journal which have been
|
||||
-- declared as Asset by account directives, or otherwise for
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- @^assets?(:|$)@.
|
||||
journalAssetAccountQuery :: Journal -> Query
|
||||
journalAssetAccountQuery = journalAccountTypeQuery Asset "^assets?(:|$)"
|
||||
|
||||
-- | A query for accounts in this journal which have been
|
||||
-- declared as Liability by account directives, or otherwise for
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- @^(debts?|liabilit(y|ies))(:|$)@.
|
||||
journalLiabilityAccountQuery :: Journal -> Query
|
||||
journalLiabilityAccountQuery = journalAccountTypeQuery Liability "^(debts?|liabilit(y|ies))(:|$)"
|
||||
|
||||
-- | A query for accounts in this journal which have been
|
||||
-- declared as Equity by account directives, or otherwise for
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- @^equity(:|$)@.
|
||||
journalEquityAccountQuery :: Journal -> Query
|
||||
journalEquityAccountQuery = journalAccountTypeQuery Equity "^equity(:|$)"
|
||||
|
||||
-- | A query for accounts in this journal which have been
|
||||
-- declared as Revenue by account directives, or otherwise for
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- @^(income|revenue)s?(:|$)@.
|
||||
journalRevenueAccountQuery :: Journal -> Query
|
||||
journalRevenueAccountQuery = journalAccountTypeQuery Revenue "^(income|revenue)s?(:|$)"
|
||||
|
||||
-- | A query for accounts in this journal which have been
|
||||
-- declared as Expense by account directives, or otherwise for
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- accounts with names matched by the case-insensitive regular expression
|
||||
-- @^(income|revenue)s?(:|$)@.
|
||||
journalExpenseAccountQuery :: Journal -> Query
|
||||
journalExpenseAccountQuery = journalAccountTypeQuery Expense "^expenses?(:|$)"
|
||||
@ -371,7 +371,7 @@ journalProfitAndLossAccountQuery j = Or [journalRevenueAccountQuery j
|
||||
|
||||
-- | A query for Cash (-equivalent) accounts in this journal (ie,
|
||||
-- accounts which appear on the cashflow statement.) This is currently
|
||||
-- hard-coded to be all the Asset accounts except for those with names
|
||||
-- hard-coded to be all the Asset accounts except for those with names
|
||||
-- containing the case-insensitive regular expression @(receivable|:A/R|:fixed)@.
|
||||
journalCashAccountQuery :: Journal -> Query
|
||||
journalCashAccountQuery j = And [journalAssetAccountQuery j, Not $ Acct "(receivable|:A/R|:fixed)"]
|
||||
@ -579,7 +579,7 @@ journalTieTransactions j@Journal{jtxns=ts} = j{jtxns=map txnTieKnot ts}
|
||||
journalUntieTransactions :: Transaction -> Transaction
|
||||
journalUntieTransactions t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}
|
||||
|
||||
-- | Apply any transaction modifier rules in the journal
|
||||
-- | Apply any transaction modifier rules in the journal
|
||||
-- (adding automated postings to transactions, eg).
|
||||
journalModifyTransactions :: Journal -> Journal
|
||||
journalModifyTransactions j = j{ jtxns = modifyTransactions (jtxnmodifiers j) (jtxns j) }
|
||||
@ -591,7 +591,7 @@ journalCheckBalanceAssertions = either Just (const Nothing) . journalBalanceTran
|
||||
|
||||
-- "Transaction balancing" - inferring missing amounts and checking transaction balancedness and balance assertions
|
||||
|
||||
-- | Monad used for statefully balancing/amount-inferring/assertion-checking
|
||||
-- | Monad used for statefully balancing/amount-inferring/assertion-checking
|
||||
-- a sequence of transactions.
|
||||
-- Perhaps can be simplified, or would a different ordering of layers make sense ?
|
||||
-- If you see a way, let us know.
|
||||
@ -613,9 +613,9 @@ data BalancingState s = BalancingState {
|
||||
withB :: (BalancingState s -> ST s a) -> Balancing s a
|
||||
withB f = ask >>= lift . lift . f
|
||||
|
||||
-- | Get an account's running balance so far.
|
||||
-- | Get an account's running balance so far.
|
||||
getAmountB :: AccountName -> Balancing s MixedAmount
|
||||
getAmountB acc = withB $ \BalancingState{bsBalances} -> do
|
||||
getAmountB acc = withB $ \BalancingState{bsBalances} -> do
|
||||
fromMaybe 0 <$> H.lookup bsBalances acc
|
||||
|
||||
-- | Add an amount to an account's running balance, and return the new running balance.
|
||||
@ -626,7 +626,7 @@ addAmountB acc amt = withB $ \BalancingState{bsBalances} -> do
|
||||
H.insert bsBalances acc new
|
||||
return new
|
||||
|
||||
-- | Set an account's running balance to this amount, and return the difference from the old.
|
||||
-- | Set an account's running balance to this amount, and return the difference from the old.
|
||||
setAmountB :: AccountName -> MixedAmount -> Balancing s MixedAmount
|
||||
setAmountB acc amt = withB $ \BalancingState{bsBalances} -> do
|
||||
old <- fromMaybe 0 <$> H.lookup bsBalances acc
|
||||
@ -639,15 +639,15 @@ storeTransactionB t = withB $ \BalancingState{bsTransactions} ->
|
||||
void $ writeArray bsTransactions (tindex t) t
|
||||
|
||||
-- | Infer any missing amounts (to satisfy balance assignments and
|
||||
-- to balance transactions) and check that all transactions balance
|
||||
-- to balance transactions) and check that all transactions balance
|
||||
-- and (optional) all balance assertions pass. Or return an error message
|
||||
-- (just the first error encountered).
|
||||
--
|
||||
-- Assumes journalInferCommodityStyles has been called, since those affect transaction balancing.
|
||||
--
|
||||
-- This does multiple things because amount inferring, balance assignments,
|
||||
-- This does multiple things because amount inferring, balance assignments,
|
||||
-- balance assertions and posting dates are interdependent.
|
||||
--
|
||||
--
|
||||
-- This can be simplified further. Overview as of 20190219:
|
||||
-- @
|
||||
-- ****** parseAndFinaliseJournal['] (Cli/Utils.hs), journalAddForecast (Common.hs), budgetJournal (BudgetReport.hs), tests (BalanceReport.hs)
|
||||
@ -670,19 +670,19 @@ storeTransactionB t = withB $ \BalancingState{bsTransactions} ->
|
||||
journalBalanceTransactions :: Bool -> Journal -> Either String Journal
|
||||
journalBalanceTransactions assrt j' =
|
||||
let
|
||||
-- ensure transactions are numbered, so we can store them by number
|
||||
-- ensure transactions are numbered, so we can store them by number
|
||||
j@Journal{jtxns=ts} = journalNumberTransactions j'
|
||||
-- display precisions used in balanced checking
|
||||
styles = Just $ journalCommodityStyles j
|
||||
-- balance assignments will not be allowed on these
|
||||
txnmodifieraccts = S.fromList $ map paccount $ concatMap tmpostingrules $ jtxnmodifiers j
|
||||
in
|
||||
runST $ do
|
||||
txnmodifieraccts = S.fromList $ map paccount $ concatMap tmpostingrules $ jtxnmodifiers j
|
||||
in
|
||||
runST $ do
|
||||
-- We'll update a mutable array of transactions as we balance them,
|
||||
-- not strictly necessary but avoids a sort at the end I think.
|
||||
balancedtxns <- newListArray (1, genericLength ts) ts
|
||||
|
||||
-- Infer missing posting amounts, check transactions are balanced,
|
||||
-- Infer missing posting amounts, check transactions are balanced,
|
||||
-- and check balance assertions. This is done in two passes:
|
||||
runExceptT $ do
|
||||
|
||||
@ -691,14 +691,14 @@ journalBalanceTransactions assrt j' =
|
||||
-- The postings and not-yet-balanced transactions remain in the same relative order.
|
||||
psandts :: [Either Posting Transaction] <- fmap concat $ forM ts $ \case
|
||||
t | null $ assignmentPostings t -> case balanceTransaction styles t of
|
||||
Left e -> throwError e
|
||||
Left e -> throwError e
|
||||
Right t' -> do
|
||||
lift $ writeArray balancedtxns (tindex t') t'
|
||||
return $ map Left $ tpostings t'
|
||||
t -> return [Right t]
|
||||
|
||||
-- 2. Sort these items by date, preserving the order of same-day items,
|
||||
-- and step through them while keeping running account balances,
|
||||
-- and step through them while keeping running account balances,
|
||||
runningbals <- lift $ H.newSized (length $ journalAccountNamesUsed j)
|
||||
flip runReaderT (BalancingState styles txnmodifieraccts assrt runningbals balancedtxns) $ do
|
||||
-- performing balance assignments in, and balancing, the remaining transactions,
|
||||
@ -706,17 +706,17 @@ journalBalanceTransactions assrt j' =
|
||||
void $ mapM' balanceTransactionAndCheckAssertionsB $ sortOn (either postingDate tdate) psandts
|
||||
|
||||
ts' <- lift $ getElems balancedtxns
|
||||
return j{jtxns=ts'}
|
||||
return j{jtxns=ts'}
|
||||
|
||||
-- | This function is called statefully on each of a date-ordered sequence of
|
||||
-- 1. fully explicit postings from already-balanced transactions and
|
||||
-- | This function is called statefully on each of a date-ordered sequence of
|
||||
-- 1. fully explicit postings from already-balanced transactions and
|
||||
-- 2. not-yet-balanced transactions containing balance assignments.
|
||||
-- It executes balance assignments and finishes balancing the transactions,
|
||||
-- It executes balance assignments and finishes balancing the transactions,
|
||||
-- and checks balance assertions on each posting as it goes.
|
||||
-- An error will be thrown if a transaction can't be balanced
|
||||
-- An error will be thrown if a transaction can't be balanced
|
||||
-- or if an illegal balance assignment is found (cf checkIllegalBalanceAssignment).
|
||||
-- Transaction prices are removed, which helps eg balance-assertions.test: 15. Mix different commodities and assignments.
|
||||
-- This stores the balanced transactions in case 2 but not in case 1.
|
||||
-- This stores the balanced transactions in case 2 but not in case 1.
|
||||
balanceTransactionAndCheckAssertionsB :: Either Posting Transaction -> Balancing s ()
|
||||
|
||||
balanceTransactionAndCheckAssertionsB (Left p@Posting{}) =
|
||||
@ -726,28 +726,28 @@ balanceTransactionAndCheckAssertionsB (Left p@Posting{}) =
|
||||
balanceTransactionAndCheckAssertionsB (Right t@Transaction{tpostings=ps}) = do
|
||||
-- make sure we can handle the balance assignments
|
||||
mapM_ checkIllegalBalanceAssignmentB ps
|
||||
-- for each posting, infer its amount from the balance assignment if applicable,
|
||||
-- for each posting, infer its amount from the balance assignment if applicable,
|
||||
-- update the account's running balance and check the balance assertion if any
|
||||
ps' <- forM ps $ \p -> pure (removePrices p) >>= addOrAssignAmountAndCheckAssertionB
|
||||
-- infer any remaining missing amounts, and make sure the transaction is now fully balanced
|
||||
-- infer any remaining missing amounts, and make sure the transaction is now fully balanced
|
||||
styles <- R.reader bsStyles
|
||||
case balanceTransactionHelper styles t{tpostings=ps'} of
|
||||
Left err -> throwError err
|
||||
Left err -> throwError err
|
||||
Right (t', inferredacctsandamts) -> do
|
||||
-- for each amount just inferred, update the running balance
|
||||
-- for each amount just inferred, update the running balance
|
||||
mapM_ (uncurry addAmountB) inferredacctsandamts
|
||||
-- and save the balanced transaction.
|
||||
storeTransactionB t'
|
||||
storeTransactionB t'
|
||||
|
||||
-- | If this posting has an explicit amount, add it to the account's running balance.
|
||||
-- If it has a missing amount and a balance assignment, infer the amount from, and
|
||||
-- If it has a missing amount and a balance assignment, infer the amount from, and
|
||||
-- reset the running balance to, the assigned balance.
|
||||
-- If it has a missing amount and no balance assignment, leave it for later.
|
||||
-- Then test the balance assertion if any.
|
||||
addOrAssignAmountAndCheckAssertionB :: Posting -> Balancing s Posting
|
||||
addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanceassertion=mba}
|
||||
| hasAmount p = do
|
||||
newbal <- addAmountB acc amt
|
||||
newbal <- addAmountB acc amt
|
||||
whenM (R.reader bsAssrt) $ checkBalanceAssertionB p newbal
|
||||
return p
|
||||
| Just BalanceAssertion{baamount,batotal} <- mba = do
|
||||
@ -760,8 +760,8 @@ addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanc
|
||||
False -> do
|
||||
-- a partial balance assignment
|
||||
oldbalothercommodities <- filterMixedAmount ((acommodity baamount /=) . acommodity) <$> getAmountB acc
|
||||
let assignedbalthiscommodity = Mixed [baamount]
|
||||
newbal = oldbalothercommodities + assignedbalthiscommodity
|
||||
let assignedbalthiscommodity = Mixed [baamount]
|
||||
newbal = oldbalothercommodities + assignedbalthiscommodity
|
||||
diff <- setAmountB acc newbal
|
||||
return (diff,newbal)
|
||||
let p' = p{pamount=diff, poriginal=Just $ originalPosting p}
|
||||
@ -774,7 +774,7 @@ addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanc
|
||||
-- optionally check the posting's balance assertion if any.
|
||||
-- The posting is expected to have an explicit amount (otherwise this does nothing).
|
||||
-- Adding and checking balance assertions are tightly paired because we
|
||||
-- need to see the balance as it stands after each individual posting.
|
||||
-- need to see the balance as it stands after each individual posting.
|
||||
addAmountAndCheckAssertionB :: Posting -> Balancing s Posting
|
||||
addAmountAndCheckAssertionB p | hasAmount p = do
|
||||
newbal <- addAmountB (paccount p) (pamount p)
|
||||
@ -806,17 +806,17 @@ checkBalanceAssertionB _ _ = return ()
|
||||
checkBalanceAssertionOneCommodityB :: Posting -> Amount -> MixedAmount -> Balancing s ()
|
||||
checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt actualbal = do
|
||||
let isinclusive = maybe False bainclusive $ pbalanceassertion p
|
||||
actualbal' <-
|
||||
if isinclusive
|
||||
then
|
||||
-- sum the running balances of this account and any of its subaccounts seen so far
|
||||
withB $ \BalancingState{bsBalances} ->
|
||||
H.foldM
|
||||
(\ibal (acc, amt) -> return $ ibal +
|
||||
actualbal' <-
|
||||
if isinclusive
|
||||
then
|
||||
-- sum the running balances of this account and any of its subaccounts seen so far
|
||||
withB $ \BalancingState{bsBalances} ->
|
||||
H.foldM
|
||||
(\ibal (acc, amt) -> return $ ibal +
|
||||
if assertedacct==acc || assertedacct `isAccountNamePrefixOf` acc then amt else 0)
|
||||
0
|
||||
0
|
||||
bsBalances
|
||||
else return actualbal
|
||||
else return actualbal
|
||||
let
|
||||
assertedcomm = acommodity assertedamt
|
||||
actualbalincomm = headDef 0 $ amounts $ filterMixedAmountByCommodity assertedcomm $ actualbal'
|
||||
@ -863,17 +863,17 @@ checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt
|
||||
|
||||
-- | Throw an error if this posting is trying to do an illegal balance assignment.
|
||||
checkIllegalBalanceAssignmentB :: Posting -> Balancing s ()
|
||||
checkIllegalBalanceAssignmentB p = do
|
||||
checkIllegalBalanceAssignmentB p = do
|
||||
checkBalanceAssignmentPostingDateB p
|
||||
checkBalanceAssignmentUnassignableAccountB p
|
||||
|
||||
|
||||
-- XXX these should show position. annotateErrorWithTransaction t ?
|
||||
|
||||
-- | Throw an error if this posting is trying to do a balance assignment and
|
||||
-- has a custom posting date (which makes amount inference too hard/impossible).
|
||||
checkBalanceAssignmentPostingDateB :: Posting -> Balancing s ()
|
||||
checkBalanceAssignmentPostingDateB p =
|
||||
when (hasBalanceAssignment p && isJust (pdate p)) $
|
||||
when (hasBalanceAssignment p && isJust (pdate p)) $
|
||||
throwError $ unlines $
|
||||
["postings which are balance assignments may not have a custom date."
|
||||
,"Please write the posting amount explicitly, or remove the posting date:"
|
||||
@ -918,8 +918,8 @@ journalApplyCommodityStyles j@Journal{jtxns=ts, jpricedirectives=pds} = j''
|
||||
fixbalanceassertion ba = ba{baamount=styleAmount styles $ baamount ba}
|
||||
fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmount styles a}
|
||||
|
||||
-- | Get all the amount styles defined in this journal, either declared by
|
||||
-- a commodity directive or inferred from amounts, as a map from symbol to style.
|
||||
-- | Get all the amount styles defined in this journal, either declared by
|
||||
-- a commodity directive or inferred from amounts, as a map from symbol to style.
|
||||
-- Styles declared by commodity directives take precedence, and these also are
|
||||
-- guaranteed to know their decimal point character.
|
||||
journalCommodityStyles :: Journal -> M.Map CommoditySymbol AmountStyle
|
||||
@ -1078,23 +1078,23 @@ journalPivot fieldortagname j = j{jtxns = map (transactionPivot fieldortagname)
|
||||
|
||||
-- | Replace this transaction's postings' account names with the value
|
||||
-- of the given field or tag, if any.
|
||||
transactionPivot :: Text -> Transaction -> Transaction
|
||||
transactionPivot :: Text -> Transaction -> Transaction
|
||||
transactionPivot fieldortagname t = t{tpostings = map (postingPivot fieldortagname) . tpostings $ t}
|
||||
|
||||
-- | Replace this posting's account name with the value
|
||||
-- of the given field or tag, if any, otherwise the empty string.
|
||||
postingPivot :: Text -> Posting -> Posting
|
||||
postingPivot :: Text -> Posting -> Posting
|
||||
postingPivot fieldortagname p = p{paccount = pivotedacct, poriginal = Just $ originalPosting p}
|
||||
where
|
||||
pivotedacct
|
||||
| Just t <- ptransaction p, fieldortagname == "code" = tcode t
|
||||
| Just t <- ptransaction p, fieldortagname == "description" = tdescription t
|
||||
| Just t <- ptransaction p, fieldortagname == "payee" = transactionPayee t
|
||||
| Just t <- ptransaction p, fieldortagname == "note" = transactionNote t
|
||||
| Just t <- ptransaction p, fieldortagname == "code" = tcode t
|
||||
| Just t <- ptransaction p, fieldortagname == "description" = tdescription t
|
||||
| Just t <- ptransaction p, fieldortagname == "payee" = transactionPayee t
|
||||
| Just t <- ptransaction p, fieldortagname == "note" = transactionNote t
|
||||
| Just (_, value) <- postingFindTag fieldortagname p = value
|
||||
| otherwise = ""
|
||||
|
||||
postingFindTag :: TagName -> Posting -> Maybe (TagName, TagValue)
|
||||
postingFindTag :: TagName -> Posting -> Maybe (TagName, TagValue)
|
||||
postingFindTag tagname p = find ((tagname==) . fst) $ postingAllTags p
|
||||
|
||||
-- -- | Build a database of market prices in effect on the given date,
|
||||
@ -1333,8 +1333,8 @@ tests_Journal = tests "Journal" [
|
||||
nulljournal{ jtxns = [
|
||||
transaction "2019/01/01" [ vpost' "a" (num 2) (balassert (num 2)) ]
|
||||
,transaction "2019/01/01" [
|
||||
post' "b" (num 1) Nothing
|
||||
,post' "a" missingamt Nothing
|
||||
post' "b" (num 1) Nothing
|
||||
,post' "a" missingamt Nothing
|
||||
]
|
||||
,transaction "2019/01/01" [ post' "a" (num 0) (balassert (num 1)) ]
|
||||
]}
|
||||
|
@ -31,7 +31,7 @@ import qualified Data.Text as T
|
||||
import Safe (headDef)
|
||||
import Text.Printf
|
||||
|
||||
import Hledger.Utils.Test
|
||||
import Hledger.Utils.Test
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.Account
|
||||
import Hledger.Data.Journal
|
||||
|
@ -166,7 +166,7 @@ showPeriod (PeriodFrom b) = formatTime defaultTimeLocale "%0C%y/%m/%d-" b
|
||||
showPeriod (PeriodTo e) = formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e) -- -INCLUSIVEENDDATE
|
||||
showPeriod PeriodAll = "-"
|
||||
|
||||
-- | Like showPeriod, but if it's a month period show just
|
||||
-- | Like showPeriod, but if it's a month period show just
|
||||
-- the 3 letter month name abbreviation for the current locale.
|
||||
showPeriodMonthAbbrev (MonthPeriod _ m) -- Jan
|
||||
| m > 0 && m <= length monthnames = snd $ monthnames !! (m-1)
|
||||
|
@ -34,7 +34,7 @@ import Hledger.Utils.UTF8IOCompat (error')
|
||||
-- doctest helper, too much hassle to define in the comment
|
||||
-- XXX duplicates some logic in periodictransactionp
|
||||
_ptgen str = do
|
||||
let
|
||||
let
|
||||
t = T.pack str
|
||||
(i,s) = parsePeriodExpr' nulldate t
|
||||
case checkPeriodicTransactionStartDate i s t of
|
||||
@ -42,7 +42,7 @@ _ptgen str = do
|
||||
Nothing ->
|
||||
mapM_ (putStr . showTransaction) $
|
||||
runPeriodicTransaction
|
||||
nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] }
|
||||
nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] }
|
||||
nulldatespan
|
||||
|
||||
|
||||
@ -184,13 +184,13 @@ instance Show PeriodicTransaction where
|
||||
-- *** Exception: Unable to generate transactions according to "weekly from 2017" because 2017-01-01 is not a first day of the week
|
||||
--
|
||||
-- >>> _ptgen "monthly from 2017/5/4"
|
||||
-- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month
|
||||
-- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month
|
||||
--
|
||||
-- >>> _ptgen "every quarter from 2017/1/2"
|
||||
-- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter
|
||||
-- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter
|
||||
--
|
||||
-- >>> _ptgen "yearly from 2017/1/14"
|
||||
-- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year
|
||||
-- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year
|
||||
--
|
||||
-- >>> let reportperiod="daily from 2018/01/03" in let (i,s) = parsePeriodExpr' nulldate reportperiod in runPeriodicTransaction (nullperiodictransaction{ptperiodexpr=reportperiod, ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1]}) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03"))
|
||||
-- []
|
||||
@ -203,28 +203,28 @@ runPeriodicTransaction PeriodicTransaction{..} requestedspan =
|
||||
t = nulltransaction{
|
||||
tstatus = ptstatus
|
||||
,tcode = ptcode
|
||||
,tdescription = ptdescription
|
||||
,tdescription = ptdescription
|
||||
,tcomment = (if T.null ptcomment then "\n" else ptcomment) <> "recur: " <> ptperiodexpr
|
||||
,ttags = ("recur", ptperiodexpr) : pttags
|
||||
,ttags = ("recur", ptperiodexpr) : pttags
|
||||
,tpostings = ptpostings
|
||||
}
|
||||
|
||||
-- | Check that this date span begins at a boundary of this interval,
|
||||
-- | Check that this date span begins at a boundary of this interval,
|
||||
-- or return an explanatory error message including the provided period expression
|
||||
-- (from which the span and interval are derived).
|
||||
checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String
|
||||
checkPeriodicTransactionStartDate i s periodexpr =
|
||||
checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String
|
||||
checkPeriodicTransactionStartDate i s periodexpr =
|
||||
case (i, spanStart s) of
|
||||
(Weeks _, Just d) -> checkStart d "week"
|
||||
(Months _, Just d) -> checkStart d "month"
|
||||
(Quarters _, Just d) -> checkStart d "quarter"
|
||||
(Years _, Just d) -> checkStart d "year"
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
where
|
||||
checkStart d x =
|
||||
let firstDate = fixSmartDate d ("","this",x)
|
||||
in
|
||||
if d == firstDate
|
||||
let firstDate = fixSmartDate d ("","this",x)
|
||||
in
|
||||
if d == firstDate
|
||||
then Nothing
|
||||
else Just $
|
||||
"Unable to generate transactions according to "++show (T.unpack periodexpr)
|
||||
|
@ -78,7 +78,7 @@ import qualified Data.Text as T
|
||||
import Data.Time.Calendar
|
||||
import Safe
|
||||
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.Amount
|
||||
import Hledger.Data.AccountName
|
||||
@ -221,7 +221,7 @@ postingDate2 p = headDef nulldate $ catMaybes dates
|
||||
-- | Get a posting's status. This is cleared or pending if those are
|
||||
-- explicitly set on the posting, otherwise the status of its parent
|
||||
-- transaction, or unmarked if there is no parent transaction. (Note
|
||||
-- the ambiguity, unmarked can mean "posting and transaction are both
|
||||
-- the ambiguity, unmarked can mean "posting and transaction are both
|
||||
-- unmarked" or "posting is unmarked and don't know about the transaction".
|
||||
postingStatus :: Posting -> Status
|
||||
postingStatus Posting{pstatus=s, ptransaction=mt}
|
||||
|
@ -137,7 +137,7 @@ fieldp = do
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
formatStringTester fs value expected = actual `is` expected
|
||||
formatStringTester fs value expected = actual `is` expected
|
||||
where
|
||||
actual = case fs of
|
||||
FormatLiteral l -> formatString False Nothing Nothing l
|
||||
|
@ -26,7 +26,7 @@ import System.Locale (defaultTimeLocale)
|
||||
#endif
|
||||
import Text.Printf
|
||||
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.Dates
|
||||
import Hledger.Data.Amount
|
||||
@ -130,10 +130,10 @@ tests_Timeclock = tests "Timeclock" [
|
||||
parseTime defaultTimeLocale "%H:%M:%S"
|
||||
#endif
|
||||
showtime = formatTime defaultTimeLocale "%H:%M"
|
||||
txndescs = map (T.unpack . tdescription) . timeclockEntriesToTransactions now
|
||||
txndescs = map (T.unpack . tdescription) . timeclockEntriesToTransactions now
|
||||
future = utcToLocalTime tz $ addUTCTime 100 now'
|
||||
futurestr = showtime future
|
||||
tests "timeclockEntriesToTransactions" [
|
||||
tests "timeclockEntriesToTransactions" [
|
||||
test "started yesterday, split session at midnight" $
|
||||
txndescs [clockin (mktime yesterday "23:00:00") "" ""] `is` ["23:00-23:59","00:00-"++nowstr]
|
||||
,test "split multi-day sessions at each midnight" $
|
||||
|
@ -62,7 +62,7 @@ import Data.Time.Calendar
|
||||
import Text.Printf
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.Dates
|
||||
import Hledger.Data.Posting
|
||||
@ -101,7 +101,7 @@ nulltransaction = Transaction {
|
||||
}
|
||||
|
||||
-- | Make a simple transaction with the given date and postings.
|
||||
transaction :: String -> [Posting] -> Transaction
|
||||
transaction :: String -> [Posting] -> Transaction
|
||||
transaction datestr ps = txnTieKnot $ nulltransaction{tdate=parsedate datestr, tpostings=ps}
|
||||
|
||||
transactionPayee :: Transaction -> Text
|
||||
@ -122,7 +122,7 @@ payeeAndNoteFromDescription t
|
||||
(p, n) = T.span (/= '|') t
|
||||
|
||||
{-|
|
||||
Render a journal transaction as text in the style of Ledger's print command.
|
||||
Render a journal transaction as text in the style of Ledger's print command.
|
||||
|
||||
Ledger 2.x's standard format looks like this:
|
||||
|
||||
@ -139,7 +139,7 @@ pcommentwidth = no limit -- 22
|
||||
@
|
||||
|
||||
The output will be parseable journal syntax.
|
||||
To facilitate this, postings with explicit multi-commodity amounts
|
||||
To facilitate this, postings with explicit multi-commodity amounts
|
||||
are displayed as multiple similar postings, one per commodity.
|
||||
(Normally does not happen with this function).
|
||||
|
||||
@ -148,8 +148,8 @@ and the transaction appears obviously balanced
|
||||
(postings sum to 0, without needing to infer conversion prices),
|
||||
the last posting's amount will not be shown.
|
||||
-}
|
||||
-- XXX why that logic ?
|
||||
-- XXX where is/should this be still used ?
|
||||
-- XXX why that logic ?
|
||||
-- XXX where is/should this be still used ?
|
||||
-- XXX rename these, after amount expressions/mixed posting amounts lands
|
||||
-- eg showTransactionSimpleAmountsElidingLast, showTransactionSimpleAmounts, showTransaction
|
||||
showTransaction :: Transaction -> String
|
||||
@ -158,19 +158,19 @@ showTransaction = showTransactionHelper True False
|
||||
-- | Like showTransaction, but does not change amounts' explicitness.
|
||||
-- Explicit amounts are shown and implicit amounts are not.
|
||||
-- The output will be parseable journal syntax.
|
||||
-- To facilitate this, postings with explicit multi-commodity amounts
|
||||
-- To facilitate this, postings with explicit multi-commodity amounts
|
||||
-- are displayed as multiple similar postings, one per commodity.
|
||||
-- Most often, this is the one you want to use.
|
||||
showTransactionUnelided :: Transaction -> String
|
||||
showTransactionUnelided = showTransactionHelper False False
|
||||
|
||||
-- | Like showTransactionUnelided, but explicit multi-commodity amounts
|
||||
-- are shown on one line, comma-separated. In this case the output will
|
||||
-- | Like showTransactionUnelided, but explicit multi-commodity amounts
|
||||
-- are shown on one line, comma-separated. In this case the output will
|
||||
-- not be parseable journal syntax.
|
||||
showTransactionUnelidedOneLineAmounts :: Transaction -> String
|
||||
showTransactionUnelidedOneLineAmounts = showTransactionHelper False True
|
||||
|
||||
-- | Helper for showTransaction*.
|
||||
-- | Helper for showTransaction*.
|
||||
showTransactionHelper :: Bool -> Bool -> Transaction -> String
|
||||
showTransactionHelper elide onelineamounts t =
|
||||
unlines $ [descriptionline]
|
||||
@ -205,7 +205,7 @@ renderCommentLines t =
|
||||
-- for `print` output. Normally this output will be valid journal syntax which
|
||||
-- hledger can reparse (though it may include no-longer-valid balance assertions).
|
||||
--
|
||||
-- Explicit amounts are shown, any implicit amounts are not.
|
||||
-- Explicit amounts are shown, any implicit amounts are not.
|
||||
--
|
||||
-- Setting elide to true forces the last posting's amount to be implicit, if:
|
||||
-- there are other postings, all with explicit amounts, and the transaction
|
||||
@ -215,36 +215,36 @@ renderCommentLines t =
|
||||
-- if onelineamounts is true, these amounts are shown on one line,
|
||||
-- comma-separated, and the output will not be valid journal syntax.
|
||||
-- Otherwise, they are shown as several similar postings, one per commodity.
|
||||
--
|
||||
--
|
||||
-- The output will appear to be a balanced transaction.
|
||||
-- Amounts' display precisions, which may have been limited by commodity
|
||||
-- directives, will be increased if necessary to ensure this.
|
||||
--
|
||||
-- Posting amounts will be aligned with each other, starting about 4 columns
|
||||
-- beyond the widest account name (see postingAsLines for details).
|
||||
--
|
||||
--
|
||||
postingsAsLines :: Bool -> Bool -> Transaction -> [Posting] -> [String]
|
||||
postingsAsLines elide onelineamounts t ps
|
||||
| elide && length ps > 1 && all hasAmount ps && isTransactionBalanced Nothing t -- imprecise balanced check
|
||||
= concatMap (postingAsLines False onelineamounts ps) (init ps) ++ postingAsLines True onelineamounts ps (last ps)
|
||||
| otherwise = concatMap (postingAsLines False onelineamounts ps) ps
|
||||
|
||||
-- | Render one posting, on one or more lines, suitable for `print` output.
|
||||
-- | Render one posting, on one or more lines, suitable for `print` output.
|
||||
-- There will be an indented account name, plus one or more of status flag,
|
||||
-- posting amount, balance assertion, same-line comment, next-line comments.
|
||||
--
|
||||
--
|
||||
-- If the posting's amount is implicit or if elideamount is true, no amount is shown.
|
||||
--
|
||||
-- If the posting's amount is explicit and multi-commodity, multiple similar
|
||||
-- If the posting's amount is explicit and multi-commodity, multiple similar
|
||||
-- postings are shown, one for each commodity, to help produce parseable journal syntax.
|
||||
-- Or if onelineamounts is true, such amounts are shown on one line, comma-separated
|
||||
-- (and the output will not be valid journal syntax).
|
||||
--
|
||||
-- By default, 4 spaces (2 if there's a status flag) are shown between
|
||||
-- By default, 4 spaces (2 if there's a status flag) are shown between
|
||||
-- account name and start of amount area, which is typically 12 chars wide
|
||||
-- and contains a right-aligned amount (so 10-12 visible spaces between
|
||||
-- and contains a right-aligned amount (so 10-12 visible spaces between
|
||||
-- account name and amount is typical).
|
||||
-- When given a list of postings to be aligned with, the whitespace will be
|
||||
-- When given a list of postings to be aligned with, the whitespace will be
|
||||
-- increased if needed to match the posting with the longest account name.
|
||||
-- This is used to align the amounts of a transaction's postings.
|
||||
--
|
||||
@ -255,10 +255,10 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [
|
||||
| postingblock <- postingblocks]
|
||||
where
|
||||
postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, " ", amount, assertion, samelinecomment] | amount <- shownAmounts]
|
||||
assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p
|
||||
assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p
|
||||
statusandaccount = lineIndent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p
|
||||
where
|
||||
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
|
||||
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
|
||||
minwidth = maximum $ map ((2+) . textWidth . T.pack . pacctstr) pstoalignwith
|
||||
pstatusandacct p' = pstatusprefix p' ++ pacctstr p'
|
||||
pstatusprefix p' | null s = ""
|
||||
@ -279,8 +279,8 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [
|
||||
case renderCommentLines (pcomment p) of [] -> ("",[])
|
||||
c:cs -> (c,cs)
|
||||
|
||||
-- | Render a balance assertion, as the =[=][*] symbol and expected amount.
|
||||
showBalanceAssertion BalanceAssertion{..} =
|
||||
-- | Render a balance assertion, as the =[=][*] symbol and expected amount.
|
||||
showBalanceAssertion BalanceAssertion{..} =
|
||||
"=" ++ ['=' | batotal] ++ ['*' | bainclusive] ++ " " ++ showAmountWithZeroCommodity baamount
|
||||
|
||||
-- | Render a posting, simply. Used in balance assertion errors.
|
||||
@ -296,7 +296,7 @@ showBalanceAssertion BalanceAssertion{..} =
|
||||
-- assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . baamount) $ pbalanceassertion p
|
||||
|
||||
-- | Render a posting, at the appropriate width for aligning with
|
||||
-- its siblings if any. Used by the rewrite command.
|
||||
-- its siblings if any. Used by the rewrite command.
|
||||
showPostingLines :: Posting -> [String]
|
||||
showPostingLines p = postingAsLines False False ps p where
|
||||
ps | Just t <- ptransaction p = tpostings t
|
||||
@ -366,14 +366,14 @@ isTransactionBalanced styles t =
|
||||
bvsum' = canonicalise $ costOfMixedAmount bvsum
|
||||
canonicalise = maybe id canonicaliseMixedAmount styles
|
||||
|
||||
-- | Balance this transaction, ensuring that its postings
|
||||
-- | Balance this transaction, ensuring that its postings
|
||||
-- (and its balanced virtual postings) sum to 0,
|
||||
-- by inferring a missing amount or conversion price(s) if needed.
|
||||
-- by inferring a missing amount or conversion price(s) if needed.
|
||||
-- Or if balancing is not possible, because the amounts don't sum to 0 or
|
||||
-- because there's more than one missing amount, return an error message.
|
||||
--
|
||||
-- Transactions with balance assignments can have more than one
|
||||
-- missing amount; to balance those you should use the more powerful
|
||||
-- missing amount; to balance those you should use the more powerful
|
||||
-- journalBalanceTransactions.
|
||||
--
|
||||
-- The "sum to 0" test is done using commodity display precisions,
|
||||
@ -383,18 +383,18 @@ balanceTransaction ::
|
||||
Maybe (Map.Map CommoditySymbol AmountStyle) -- ^ commodity display styles
|
||||
-> Transaction
|
||||
-> Either String Transaction
|
||||
balanceTransaction mstyles = fmap fst . balanceTransactionHelper mstyles
|
||||
balanceTransaction mstyles = fmap fst . balanceTransactionHelper mstyles
|
||||
|
||||
-- | Helper used by balanceTransaction and balanceTransactionWithBalanceAssignmentAndCheckAssertionsB;
|
||||
-- use one of those instead. It also returns a list of accounts
|
||||
-- use one of those instead. It also returns a list of accounts
|
||||
-- and amounts that were inferred.
|
||||
balanceTransactionHelper ::
|
||||
Maybe (Map.Map CommoditySymbol AmountStyle) -- ^ commodity display styles
|
||||
-> Transaction
|
||||
-> Either String (Transaction, [(AccountName, MixedAmount)])
|
||||
balanceTransactionHelper mstyles t = do
|
||||
(t', inferredamtsandaccts) <-
|
||||
inferBalancingAmount (fromMaybe Map.empty mstyles) $ inferBalancingPrices t
|
||||
(t', inferredamtsandaccts) <-
|
||||
inferBalancingAmount (fromMaybe Map.empty mstyles) $ inferBalancingPrices t
|
||||
if isTransactionBalanced mstyles t'
|
||||
then Right (txnTieKnot t', inferredamtsandaccts)
|
||||
else Left $ annotateErrorWithTransaction t' $ nonzerobalanceerror t'
|
||||
@ -413,7 +413,7 @@ balanceTransactionHelper mstyles t = do
|
||||
sep = if not (null rmsg) && not (null bvmsg) then "; " else "" :: String
|
||||
|
||||
annotateErrorWithTransaction :: Transaction -> String -> String
|
||||
annotateErrorWithTransaction t s = intercalate "\n" [showGenericSourcePos $ tsourcepos t, s, showTransactionUnelided t]
|
||||
annotateErrorWithTransaction t s = intercalate "\n" [showGenericSourcePos $ tsourcepos t, s, showTransactionUnelided t]
|
||||
|
||||
-- | Infer up to one missing amount for this transactions's real postings, and
|
||||
-- likewise for its balanced virtual postings, if needed; or return an error
|
||||
@ -423,7 +423,7 @@ annotateErrorWithTransaction t s = intercalate "\n" [showGenericSourcePos $ tsou
|
||||
-- We can infer a missing amount when there are multiple postings and exactly
|
||||
-- one of them is amountless. If the amounts had price(s) the inferred amount
|
||||
-- have the same price(s), and will be converted to the price commodity.
|
||||
inferBalancingAmount ::
|
||||
inferBalancingAmount ::
|
||||
Map.Map CommoditySymbol AmountStyle -- ^ commodity display styles
|
||||
-> Transaction
|
||||
-> Either String (Transaction, [(AccountName, MixedAmount)])
|
||||
@ -446,16 +446,16 @@ inferBalancingAmount styles t@Transaction{tpostings=ps}
|
||||
inferamount p =
|
||||
let
|
||||
minferredamt = case ptype p of
|
||||
RegularPosting | not (hasAmount p) -> Just realsum
|
||||
BalancedVirtualPosting | not (hasAmount p) -> Just bvsum
|
||||
_ -> Nothing
|
||||
RegularPosting | not (hasAmount p) -> Just realsum
|
||||
BalancedVirtualPosting | not (hasAmount p) -> Just bvsum
|
||||
_ -> Nothing
|
||||
in
|
||||
case minferredamt of
|
||||
Nothing -> (p, Nothing)
|
||||
Just a -> (p{pamount=a', poriginal=Just $ originalPosting p}, Just a')
|
||||
Just a -> (p{pamount=a', poriginal=Just $ originalPosting p}, Just a')
|
||||
where
|
||||
-- Inferred amounts are converted to cost.
|
||||
-- Also ensure the new amount has the standard style for its commodity
|
||||
-- Also ensure the new amount has the standard style for its commodity
|
||||
-- (since the main amount styling pass happened before this balancing pass);
|
||||
a' = styleMixedAmount styles $ normaliseMixedAmount $ costOfMixedAmount (-a)
|
||||
|
||||
@ -613,7 +613,7 @@ tests_Transaction =
|
||||
]
|
||||
]
|
||||
-- postingsAsLines
|
||||
-- one implicit amount
|
||||
-- one implicit amount
|
||||
, let timp = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` missingamt]}
|
||||
-- explicit amounts, balanced
|
||||
texp = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` usd (-1)]}
|
||||
@ -659,7 +659,7 @@ tests_Transaction =
|
||||
, test "one-explicit-amount-elide-true" $
|
||||
let t = texp1
|
||||
in postingsAsLines True False t (tpostings t) `is`
|
||||
[ " (a) $1.00" -- explicit amount remains explicit since only one posting
|
||||
[ " (a) $1.00" -- explicit amount remains explicit since only one posting
|
||||
]
|
||||
, test "explicit-amounts-two-commodities-elide-true" $
|
||||
let t = texp2
|
||||
|
@ -4,7 +4,7 @@
|
||||
{-|
|
||||
|
||||
A 'TransactionModifier' is a rule that modifies certain 'Transaction's,
|
||||
typically adding automated postings to them.
|
||||
typically adding automated postings to them.
|
||||
|
||||
-}
|
||||
module Hledger.Data.TransactionModifier (
|
||||
@ -41,7 +41,7 @@ modifyTransactions tmods = map applymods
|
||||
-- | Converts a 'TransactionModifier' to a 'Transaction'-transforming function,
|
||||
-- which applies the modification(s) specified by the TransactionModifier.
|
||||
-- Currently this means adding automated postings when certain other postings are present.
|
||||
-- The postings of the transformed transaction will reference it in the usual
|
||||
-- The postings of the transformed transaction will reference it in the usual
|
||||
-- way (ie, 'txnTieKnot' is called).
|
||||
--
|
||||
-- >>> putStr $ showTransaction $ transactionModifierToFunction (TransactionModifier "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
|
||||
@ -60,16 +60,16 @@ modifyTransactions tmods = map applymods
|
||||
-- <BLANKLINE>
|
||||
--
|
||||
transactionModifierToFunction :: TransactionModifier -> (Transaction -> Transaction)
|
||||
transactionModifierToFunction mt =
|
||||
transactionModifierToFunction mt =
|
||||
\t@(tpostings -> ps) -> txnTieKnot t{ tpostings=generatePostings ps } -- TODO add modifier txn comment/tags ?
|
||||
where
|
||||
q = simplifyQuery $ tmParseQuery mt (error' "a transaction modifier's query cannot depend on current date")
|
||||
mods = map tmPostingRuleToFunction $ tmpostingrules mt
|
||||
generatePostings ps = [p' | p <- ps
|
||||
, p' <- if q `matchesPosting` p then p:[ m p | m <- mods] else [p]]
|
||||
|
||||
-- | Parse the 'Query' from a 'TransactionModifier's 'tmquerytxt',
|
||||
-- and return it as a function requiring the current date.
|
||||
|
||||
-- | Parse the 'Query' from a 'TransactionModifier's 'tmquerytxt',
|
||||
-- and return it as a function requiring the current date.
|
||||
--
|
||||
-- >>> tmParseQuery (TransactionModifier "" []) undefined
|
||||
-- Any
|
||||
@ -85,9 +85,9 @@ tmParseQuery mt = fst . flip parseQuery (tmquerytxt mt)
|
||||
-- | Converts a 'TransactionModifier''s posting rule to a 'Posting'-generating function,
|
||||
-- which will be used to make a new posting based on the old one (an "automated posting").
|
||||
-- The new posting's amount can optionally be the old posting's amount multiplied by a constant.
|
||||
-- If the old posting had a total-priced amount, the new posting's multiplied amount will be unit-priced.
|
||||
-- If the old posting had a total-priced amount, the new posting's multiplied amount will be unit-priced.
|
||||
tmPostingRuleToFunction :: TMPostingRule -> (Posting -> Posting)
|
||||
tmPostingRuleToFunction pr =
|
||||
tmPostingRuleToFunction pr =
|
||||
\p -> renderPostingCommentDates $ pr
|
||||
{ pdate = pdate p
|
||||
, pdate2 = pdate2 p
|
||||
@ -103,15 +103,15 @@ tmPostingRuleToFunction pr =
|
||||
matchedamount = dbg6 "matchedamount" $ pamount p
|
||||
-- Handle a matched amount with a total price carefully so as to keep the transaction balanced (#928).
|
||||
-- Approach 1: convert to a unit price and increase the display precision slightly
|
||||
-- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount
|
||||
-- Approach 2: multiply the total price (keeping it positive) as well as the quantity
|
||||
Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmountAndPrice` matchedamount
|
||||
-- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount
|
||||
-- Approach 2: multiply the total price (keeping it positive) as well as the quantity
|
||||
Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmountAndPrice` matchedamount
|
||||
in
|
||||
case acommodity pramount of
|
||||
"" -> Mixed as
|
||||
-- TODO multipliers with commodity symbols are not yet a documented feature.
|
||||
-- For now: in addition to multiplying the quantity, it also replaces the
|
||||
-- matched amount's commodity, display style, and price with those of the posting rule.
|
||||
-- For now: in addition to multiplying the quantity, it also replaces the
|
||||
-- matched amount's commodity, display style, and price with those of the posting rule.
|
||||
c -> Mixed [a{acommodity = c, astyle = astyle pramount, aprice = aprice pramount} | a <- as]
|
||||
|
||||
postingRuleMultiplier :: TMPostingRule -> Maybe Quantity
|
||||
|
@ -30,7 +30,7 @@ import Data.Functor (($>))
|
||||
import Data.Graph.Inductive (Gr,Node,NodeMap)
|
||||
import Data.List (intercalate)
|
||||
import Text.Blaze (ToMarkup(..))
|
||||
--XXX https://hackage.haskell.org/package/containers/docs/Data-Map.html
|
||||
--XXX https://hackage.haskell.org/package/containers/docs/Data-Map.html
|
||||
--Note: You should use Data.Map.Strict instead of this module if:
|
||||
--You will eventually need all the values stored.
|
||||
--The stored values don't represent large virtual data structures to be lazily computed.
|
||||
@ -158,7 +158,7 @@ instance ToMarkup Quantity
|
||||
-- | An amount's per-unit or total cost/selling price in another
|
||||
-- commodity, as recorded in the journal entry eg with @ or @@.
|
||||
-- Docs call this "transaction price". The amount is always positive.
|
||||
data AmountPrice = UnitPrice Amount | TotalPrice Amount
|
||||
data AmountPrice = UnitPrice Amount | TotalPrice Amount
|
||||
deriving (Eq,Ord,Typeable,Data,Generic,Show)
|
||||
|
||||
instance NFData AmountPrice
|
||||
@ -301,7 +301,7 @@ data Posting = Posting {
|
||||
-- Tying this knot gets tedious, Maybe makes it easier/optional.
|
||||
poriginal :: Maybe Posting -- ^ When this posting has been transformed in some way
|
||||
-- (eg its amount or price was inferred, or the account name was
|
||||
-- changed by a pivot or budget report), this references the original
|
||||
-- changed by a pivot or budget report), this references the original
|
||||
-- untransformed posting (which will have Nothing in this field).
|
||||
} deriving (Typeable,Data,Generic)
|
||||
|
||||
@ -358,10 +358,10 @@ data Transaction = Transaction {
|
||||
instance NFData Transaction
|
||||
|
||||
-- | A transaction modifier rule. This has a query which matches postings
|
||||
-- in the journal, and a list of transformations to apply to those
|
||||
-- in the journal, and a list of transformations to apply to those
|
||||
-- postings or their transactions. Currently there is one kind of transformation:
|
||||
-- the TMPostingRule, which adds a posting ("auto posting") to the transaction,
|
||||
-- optionally setting its amount to the matched posting's amount multiplied by a constant.
|
||||
-- the TMPostingRule, which adds a posting ("auto posting") to the transaction,
|
||||
-- optionally setting its amount to the matched posting's amount multiplied by a constant.
|
||||
data TransactionModifier = TransactionModifier {
|
||||
tmquerytxt :: Text,
|
||||
tmpostingrules :: [TMPostingRule]
|
||||
@ -383,8 +383,8 @@ type TMPostingRule = Posting
|
||||
-- | A periodic transaction rule, describing a transaction that recurs.
|
||||
data PeriodicTransaction = PeriodicTransaction {
|
||||
ptperiodexpr :: Text, -- ^ the period expression as written
|
||||
ptinterval :: Interval, -- ^ the interval at which this transaction recurs
|
||||
ptspan :: DateSpan, -- ^ the (possibly unbounded) period during which this transaction recurs. Contains a whole number of intervals.
|
||||
ptinterval :: Interval, -- ^ the interval at which this transaction recurs
|
||||
ptspan :: DateSpan, -- ^ the (possibly unbounded) period during which this transaction recurs. Contains a whole number of intervals.
|
||||
--
|
||||
ptstatus :: Status, -- ^ some of Transaction's fields
|
||||
ptcode :: Text,
|
||||
@ -496,8 +496,8 @@ data Journal = Journal {
|
||||
,jparsetimeclockentries :: [TimeclockEntry] -- ^ timeclock sessions which have not been clocked out
|
||||
,jincludefilestack :: [FilePath]
|
||||
-- principal data
|
||||
,jdeclaredaccounts :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation)
|
||||
,jdeclaredaccounttypes :: M.Map AccountType [AccountName] -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts)
|
||||
,jdeclaredaccounts :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation)
|
||||
,jdeclaredaccounttypes :: M.Map AccountType [AccountName] -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts)
|
||||
,jcommodities :: M.Map CommoditySymbol Commodity -- ^ commodities and formats declared by commodity directives
|
||||
,jinferredcommodities :: M.Map CommoditySymbol AmountStyle -- ^ commodities and formats inferred from journal amounts TODO misnamed - jusedstyles
|
||||
,jpricedirectives :: [PriceDirective] -- ^ All market price declarations (P directives), in parse order (after journal finalisation).
|
||||
@ -558,12 +558,12 @@ data Account = Account {
|
||||
,aibalance :: MixedAmount -- ^ this account's balance, including subaccounts
|
||||
} deriving (Typeable, Data, Generic)
|
||||
|
||||
-- | Whether an account's balance is normally a positive number (in
|
||||
-- accounting terms, a debit balance) or a negative number (credit balance).
|
||||
-- | Whether an account's balance is normally a positive number (in
|
||||
-- accounting terms, a debit balance) or a negative number (credit balance).
|
||||
-- Assets and expenses are normally positive (debit), while liabilities, equity
|
||||
-- and income are normally negative (credit).
|
||||
-- https://en.wikipedia.org/wiki/Normal_balance
|
||||
data NormalSign = NormallyPositive | NormallyNegative deriving (Show, Data, Eq)
|
||||
data NormalSign = NormallyPositive | NormallyNegative deriving (Show, Data, Eq)
|
||||
|
||||
-- | A Ledger has the journal it derives from, and the accounts
|
||||
-- derived from that. Accounts are accessible both list-wise and
|
||||
|
@ -46,9 +46,9 @@ tests_Valuation = tests "Valuation" [
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Valuation
|
||||
|
||||
|
||||
-- Apply a specified valuation to this mixed amount, using the provided
|
||||
-- prices db, commodity styles, period-end/current dates,
|
||||
-- prices db, commodity styles, period-end/current dates,
|
||||
-- and whether this is for a multiperiod report or not.
|
||||
mixedAmountApplyValuation :: [PriceDirective] -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Bool -> ValuationType -> MixedAmount -> MixedAmount
|
||||
mixedAmountApplyValuation prices styles periodend today ismultiperiod v (Mixed as) =
|
||||
@ -63,7 +63,7 @@ mixedAmountValueAtDate :: [PriceDirective] -> M.Map CommoditySymbol AmountStyle
|
||||
mixedAmountValueAtDate prices styles mc d (Mixed as) = Mixed $ map (amountValueAtDate prices styles mc d) as
|
||||
|
||||
-- | Apply a specified valuation to this amount, using the provided
|
||||
-- prices db, commodity styles, period-end/current dates,
|
||||
-- prices db, commodity styles, period-end/current dates,
|
||||
-- and whether this is for a multiperiod report or not.
|
||||
amountApplyValuation :: [PriceDirective] -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Bool -> ValuationType -> Amount -> Amount
|
||||
amountApplyValuation prices styles periodend today ismultiperiod v a =
|
||||
@ -101,7 +101,7 @@ amountValueAtDate pricedirectives styles mto d a =
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Building a price graph
|
||||
|
||||
|
||||
-- | Convert a list of market price directives in parse order to a
|
||||
-- graph of all prices in effect on a given day, allowing efficient
|
||||
-- lookup of exchange rates between commodity pairs.
|
||||
@ -148,7 +148,7 @@ marketPriceReverse mp@MarketPrice{..} = mp{mpfrom=mpto, mpto=mpfrom, mprate=1/mp
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Market price lookup
|
||||
|
||||
|
||||
tests_priceLookup =
|
||||
let
|
||||
d = parsedate
|
||||
@ -214,7 +214,7 @@ priceLookup pricedirectives d from mto =
|
||||
where
|
||||
-- If to is unspecified, try to pick a default valuation commodity from declared prices (only).
|
||||
-- XXX how to choose ? Take lowest sorted ?
|
||||
-- Take first, hoping current order is useful ? <-
|
||||
-- Take first, hoping current order is useful ? <-
|
||||
-- Keep parse order in label and take latest parsed ?
|
||||
mdefaultto =
|
||||
dbg4 ("default valuation commodity for "++T.unpack from) $
|
||||
@ -257,7 +257,7 @@ node m = fst . fst . mkNode m
|
||||
pathEdgeLabels :: (Show b, Ord b) => Gr a b -> [Node] -> [b]
|
||||
pathEdgeLabels g = map frommaybe . map (nodesEdgeLabel g) . pathEdges
|
||||
where frommaybe = fromMaybe (error' "pathEdgeLabels: expected no Nothings here")
|
||||
|
||||
|
||||
-- | Convert a path to node pairs representing the path's edges.
|
||||
pathEdges :: [Node] -> [(Node,Node)]
|
||||
pathEdges p = [(f,t) | f:t:_ <- tails p]
|
||||
|
@ -654,7 +654,7 @@ matchesPriceDirective _ _ = True
|
||||
|
||||
tests_Query = tests "Query" [
|
||||
tests "simplifyQuery" [
|
||||
|
||||
|
||||
(simplifyQuery $ Or [Acct "a"]) `is` (Acct "a")
|
||||
,(simplifyQuery $ Or [Any,None]) `is` (Any)
|
||||
,(simplifyQuery $ And [Any,None]) `is` (None)
|
||||
@ -665,7 +665,7 @@ tests_Query = tests "Query" [
|
||||
`is` (Date (DateSpan (Just $ parsedate "2012-01-01") (Just $ parsedate "2013-01-01")))
|
||||
,(simplifyQuery $ And [Or [],Or [Desc "b b"]]) `is` (Desc "b b")
|
||||
]
|
||||
|
||||
|
||||
,tests "parseQuery" [
|
||||
(parseQuery nulldate "acct:'expenses:autres d\233penses' desc:b") `is` (And [Acct "expenses:autres d\233penses", Desc "b"], [])
|
||||
,parseQuery nulldate "inacct:a desc:\"b b\"" `is` (Desc "b b", [QueryOptInAcct "a"])
|
||||
@ -674,18 +674,18 @@ tests_Query = tests "Query" [
|
||||
,parseQuery nulldate "'a a' 'b" `is` (Or [Acct "a a",Acct "'b"], [])
|
||||
,parseQuery nulldate "\"" `is` (Acct "\"", [])
|
||||
]
|
||||
|
||||
|
||||
,tests "words''" [
|
||||
(words'' [] "a b") `is` ["a","b"]
|
||||
, (words'' [] "'a b'") `is` ["a b"]
|
||||
, (words'' [] "not:a b") `is` ["not:a","b"]
|
||||
, (words'' [] "not:'a b'") `is` ["not:a b"]
|
||||
, (words'' [] "'not:a b'") `is` ["not:a b"]
|
||||
, (words'' ["desc:"] "not:desc:'a b'") `is` ["not:desc:a b"]
|
||||
(words'' [] "a b") `is` ["a","b"]
|
||||
, (words'' [] "'a b'") `is` ["a b"]
|
||||
, (words'' [] "not:a b") `is` ["not:a","b"]
|
||||
, (words'' [] "not:'a b'") `is` ["not:a b"]
|
||||
, (words'' [] "'not:a b'") `is` ["not:a b"]
|
||||
, (words'' ["desc:"] "not:desc:'a b'") `is` ["not:desc:a b"]
|
||||
, (words'' prefixes "\"acct:expenses:autres d\233penses\"") `is` ["acct:expenses:autres d\233penses"]
|
||||
, (words'' prefixes "\"") `is` ["\""]
|
||||
]
|
||||
|
||||
|
||||
,tests "filterQuery" [
|
||||
filterQuery queryIsDepth Any `is` Any
|
||||
,filterQuery queryIsDepth (Depth 1) `is` Depth 1
|
||||
@ -714,7 +714,7 @@ tests_Query = tests "Query" [
|
||||
,parseQueryTerm nulldate "amt:<0" `is` (Left $ Amt Lt 0)
|
||||
,parseQueryTerm nulldate "amt:>10000.10" `is` (Left $ Amt AbsGt 10000.1)
|
||||
]
|
||||
|
||||
|
||||
,tests "parseAmountQueryTerm" [
|
||||
parseAmountQueryTerm "<0" `is` (Lt,0) -- special case for convenience, since AbsLt 0 would be always false
|
||||
,parseAmountQueryTerm ">0" `is` (Gt,0) -- special case for convenience and consistency with above
|
||||
@ -725,7 +725,7 @@ tests_Query = tests "Query" [
|
||||
,parseAmountQueryTerm "-0.23" `is` (Eq,(-0.23))
|
||||
,_test "number beginning with decimal mark" $ parseAmountQueryTerm "=.23" `is` (AbsEq,0.23) -- XXX
|
||||
]
|
||||
|
||||
|
||||
,tests "matchesAccount" [
|
||||
expect $ (Acct "b:c") `matchesAccount` "a:bb:c:d"
|
||||
,expect $ not $ (Acct "^a:b") `matchesAccount` "c:a:b"
|
||||
@ -736,7 +736,7 @@ tests_Query = tests "Query" [
|
||||
,expect $ Date2 nulldatespan `matchesAccount` "a"
|
||||
,expect $ not $ (Tag "a" Nothing) `matchesAccount` "a"
|
||||
]
|
||||
|
||||
|
||||
,tests "matchesPosting" [
|
||||
test "positive match on cleared posting status" $
|
||||
expect $ (StatusQ Cleared) `matchesPosting` nullposting{pstatus=Cleared}
|
||||
@ -766,7 +766,7 @@ tests_Query = tests "Query" [
|
||||
,test "l" $ expect $ (Sym "shekels") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}
|
||||
,test "m" $ expect $ not $ (Sym "shek") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}
|
||||
]
|
||||
|
||||
|
||||
,tests "matchesTransaction" [
|
||||
expect $ Any `matchesTransaction` nulltransaction
|
||||
,expect $ not $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x"}
|
||||
|
@ -184,7 +184,7 @@ findReader Nothing (Just path) =
|
||||
-- Combining Journals means concatenating them, basically.
|
||||
-- The parse state resets at the start of each file, which means that
|
||||
-- directives & aliases do not affect subsequent sibling or parent files.
|
||||
-- They do affect included child files though.
|
||||
-- They do affect included child files though.
|
||||
-- Also the final parse state saved in the Journal does span all files.
|
||||
readJournalFiles :: InputOpts -> [PrefixedFilePath] -> IO (Either String Journal)
|
||||
readJournalFiles iopts =
|
||||
@ -207,7 +207,7 @@ readJournalFiles iopts =
|
||||
-- generation, a rules file for converting CSV data, etc.
|
||||
readJournalFile :: InputOpts -> PrefixedFilePath -> IO (Either String Journal)
|
||||
readJournalFile iopts prefixedfile = do
|
||||
let
|
||||
let
|
||||
(mfmt, f) = splitReaderPrefix prefixedfile
|
||||
iopts' = iopts{mformat_=firstJust [mfmt, mformat_ iopts]}
|
||||
requireJournalFileExists f
|
||||
@ -235,13 +235,13 @@ latestDates = headDef [] . take 1 . group . reverse . sort
|
||||
|
||||
-- | Remember that these transaction dates were the latest seen when
|
||||
-- reading this journal file.
|
||||
saveLatestDates :: LatestDates -> FilePath -> IO ()
|
||||
saveLatestDates :: LatestDates -> FilePath -> IO ()
|
||||
saveLatestDates dates f = writeFile (latestDatesFileFor f) $ unlines $ map showDate dates
|
||||
|
||||
-- | What were the latest transaction dates seen the last time this
|
||||
-- | What were the latest transaction dates seen the last time this
|
||||
-- journal file was read ? If there were multiple transactions on the
|
||||
-- latest date, that number of dates is returned, otherwise just one.
|
||||
-- Or none if no transactions were read, or if latest dates info is not
|
||||
-- Or none if no transactions were read, or if latest dates info is not
|
||||
-- available for this file.
|
||||
previousLatestDates :: FilePath -> IO LatestDates
|
||||
previousLatestDates f = do
|
||||
@ -299,7 +299,7 @@ readJournal iopts mfile txt =
|
||||
--
|
||||
-- Try to parse the given text to a Journal using each reader in turn,
|
||||
-- returning the first success, or if all of them fail, the first error message.
|
||||
--
|
||||
--
|
||||
-- Input ioptions (@iopts@) specify CSV conversion rules file to help convert CSV data,
|
||||
-- enable or disable balance assertion checking and automated posting generation.
|
||||
--
|
||||
|
@ -163,12 +163,12 @@ data InputOpts = InputOpts {
|
||||
,mrules_file_ :: Maybe FilePath -- ^ a conversion rules file to use (when reading CSV)
|
||||
,separator_ :: Char -- ^ the separator to use (when reading CSV)
|
||||
,aliases_ :: [String] -- ^ account name aliases to apply
|
||||
,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data
|
||||
,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data
|
||||
,ignore_assertions_ :: Bool -- ^ don't check balance assertions
|
||||
,new_ :: Bool -- ^ read only new transactions since this file was last read
|
||||
,new_save_ :: Bool -- ^ save latest new transactions state for next time
|
||||
,pivot_ :: String -- ^ use the given field's value as the account name
|
||||
,auto_ :: Bool -- ^ generate automatic postings when journal is parsed
|
||||
,pivot_ :: String -- ^ use the given field's value as the account name
|
||||
,auto_ :: Bool -- ^ generate automatic postings when journal is parsed
|
||||
} deriving (Show, Data) --, Typeable)
|
||||
|
||||
instance Default InputOpts where def = definputopts
|
||||
@ -188,7 +188,7 @@ rawOptsToInputOpts rawopts = InputOpts{
|
||||
,new_ = boolopt "new" rawopts
|
||||
,new_save_ = True
|
||||
,pivot_ = stringopt "pivot" rawopts
|
||||
,auto_ = boolopt "auto" rawopts
|
||||
,auto_ = boolopt "auto" rawopts
|
||||
}
|
||||
|
||||
--- * parsing utilities
|
||||
@ -219,7 +219,7 @@ rejp = runErroringJournalParser
|
||||
genericSourcePos :: SourcePos -> GenericSourcePos
|
||||
genericSourcePos p = GenericSourcePos (sourceName p) (fromIntegral . unPos $ sourceLine p) (fromIntegral . unPos $ sourceColumn p)
|
||||
|
||||
-- | Construct a generic start & end line parse position from start and end megaparsec SourcePos's.
|
||||
-- | Construct a generic start & end line parse position from start and end megaparsec SourcePos's.
|
||||
journalSourcePos :: SourcePos -> SourcePos -> GenericSourcePos
|
||||
journalSourcePos p p' = JournalSourcePos (sourceName p) (fromIntegral . unPos $ sourceLine p, fromIntegral $ line')
|
||||
where line'
|
||||
@ -355,7 +355,7 @@ getAmountStyle commodity = do
|
||||
return effectiveStyle
|
||||
|
||||
addDeclaredAccountType :: AccountName -> AccountType -> JournalParser m ()
|
||||
addDeclaredAccountType acct atype =
|
||||
addDeclaredAccountType acct atype =
|
||||
modify' (\j -> j{jdeclaredaccounttypes = M.insertWith (++) atype [acct] (jdeclaredaccounttypes j)})
|
||||
|
||||
pushParentAccount :: AccountName -> JournalParser m ()
|
||||
@ -542,7 +542,7 @@ secondarydatep primaryDate = char '=' *> datep' (Just primaryYear)
|
||||
|
||||
--- ** account names
|
||||
|
||||
-- | Parse an account name (plus one following space if present),
|
||||
-- | Parse an account name (plus one following space if present),
|
||||
-- then apply any parent account prefix and/or account aliases currently in effect,
|
||||
-- in that order. (Ie first add the parent account prefix, then rewrite with aliases).
|
||||
modifiedaccountnamep :: JournalParser m AccountName
|
||||
@ -556,9 +556,9 @@ modifiedaccountnamep = do
|
||||
joinAccountNames parent
|
||||
a
|
||||
|
||||
-- | Parse an account name, plus one following space if present.
|
||||
-- | Parse an account name, plus one following space if present.
|
||||
-- Account names have one or more parts separated by the account separator character,
|
||||
-- and are terminated by two or more spaces (or end of input).
|
||||
-- and are terminated by two or more spaces (or end of input).
|
||||
-- Each part is at least one character long, may have single spaces inside it,
|
||||
-- and starts with a non-whitespace.
|
||||
-- Note, this means "{account}", "%^!" and ";comment" are all accepted
|
||||
@ -791,7 +791,7 @@ exponentp = char' 'e' *> signp <*> decimal <?> "exponent"
|
||||
--
|
||||
-- Returns:
|
||||
-- - the decimal number
|
||||
-- - the precision (number of digits after the decimal point)
|
||||
-- - the precision (number of digits after the decimal point)
|
||||
-- - the decimal point character, if any
|
||||
-- - the digit group style, if any (digit group character and sizes of digit groups)
|
||||
fromRawNumber
|
||||
@ -811,7 +811,7 @@ fromRawNumber raw mExp = case raw of
|
||||
in Right (quantity, precision, mDecPt, Nothing)
|
||||
|
||||
WithSeparators digitSep digitGrps mDecimals -> case mExp of
|
||||
Nothing ->
|
||||
Nothing ->
|
||||
let mDecPt = fmap fst mDecimals
|
||||
decimalGrp = maybe mempty snd mDecimals
|
||||
digitGroupStyle = DigitGroups digitSep (groupSizes digitGrps)
|
||||
@ -1038,7 +1038,7 @@ followingcommentp' contentp = do
|
||||
-- if there's just a next-line comment, insert an empty same-line comment
|
||||
-- so the next-line comment doesn't get rendered as a same-line comment.
|
||||
sameLine' | null sameLine && not (null nextLines) = [("",mempty)]
|
||||
| otherwise = sameLine
|
||||
| otherwise = sameLine
|
||||
(texts, contents) = unzip $ sameLine' ++ nextLines
|
||||
strippedCommentText = T.unlines $ map T.strip texts
|
||||
commentContent = mconcat contents
|
||||
@ -1306,32 +1306,32 @@ tests_Common = tests "Common" [
|
||||
tests "amountp" [
|
||||
test "basic" $ expectParseEq amountp "$47.18" (usd 47.18)
|
||||
,test "ends with decimal mark" $ expectParseEq amountp "$1." (usd 1 `withPrecision` 0)
|
||||
,test "unit price" $ expectParseEq amountp "$10 @ €0.5"
|
||||
,test "unit price" $ expectParseEq amountp "$10 @ €0.5"
|
||||
-- not precise enough:
|
||||
-- (usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1)) -- `withStyle` asdecimalpoint=Just '.'
|
||||
amount{
|
||||
acommodity="$"
|
||||
,aquantity=10 -- need to test internal precision with roundTo ? I think not
|
||||
,aquantity=10 -- need to test internal precision with roundTo ? I think not
|
||||
,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing}
|
||||
,aprice=Just $ UnitPrice $
|
||||
amount{
|
||||
acommodity="€"
|
||||
,aquantity=0.5
|
||||
,astyle=amountstyle{asprecision=1, asdecimalpoint=Just '.'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
,test "total price" $ expectParseEq amountp "$10 @@ €5"
|
||||
amount{
|
||||
acommodity="$"
|
||||
,aquantity=10
|
||||
,aquantity=10
|
||||
,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing}
|
||||
,aprice=Just $ TotalPrice $
|
||||
amount{
|
||||
acommodity="€"
|
||||
,aquantity=5
|
||||
,astyle=amountstyle{asprecision=0, asdecimalpoint=Nothing}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
,let p = lift (numberp Nothing) :: JournalParser IO (Quantity, Int, Maybe Char, Maybe DigitGroupStyle) in
|
||||
@ -1355,7 +1355,7 @@ tests_Common = tests "Common" [
|
||||
,test "." $ expectParseError p ".1," ""
|
||||
,test "." $ expectParseError p ",1." ""
|
||||
]
|
||||
|
||||
|
||||
,tests "spaceandamountormissingp" [
|
||||
test "space and amount" $ expectParseEq spaceandamountormissingp " $47.18" (Mixed [usd 47.18])
|
||||
,test "empty string" $ expectParseEq spaceandamountormissingp "" missingmixedamt
|
||||
|
@ -107,9 +107,9 @@ reader = Reader
|
||||
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
|
||||
parse iopts = parseAndFinaliseJournal journalp' iopts
|
||||
where
|
||||
journalp' = do
|
||||
journalp' = do
|
||||
-- reverse parsed aliases to ensure that they are applied in order given on commandline
|
||||
mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts)
|
||||
mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts)
|
||||
journalp
|
||||
|
||||
-- | Get the account name aliases from options, if any.
|
||||
@ -267,12 +267,12 @@ accountdirectivep = do
|
||||
-- maybe an account type code (ALERX) after two or more spaces
|
||||
-- XXX added in 1.11, deprecated in 1.13, remove in 1.14
|
||||
mtypecode :: Maybe Char <- lift $ optional $ try $ do
|
||||
skipSome spacenonewline -- at least one more space in addition to the one consumed by modifiedaccountp
|
||||
skipSome spacenonewline -- at least one more space in addition to the one consumed by modifiedaccountp
|
||||
choice $ map char "ALERX"
|
||||
|
||||
-- maybe a comment, on this and/or following lines
|
||||
(cmt, tags) <- lift transactioncommentp
|
||||
|
||||
|
||||
-- maybe Ledger-style subdirectives (ignored)
|
||||
skipMany indentedlinep
|
||||
|
||||
@ -386,7 +386,7 @@ formatdirectivep expectedsym = do
|
||||
Amount{acommodity,astyle} <- amountp
|
||||
_ <- lift followingcommentp
|
||||
if acommodity==expectedsym
|
||||
then
|
||||
then
|
||||
if asdecimalpoint astyle == Nothing
|
||||
then customFailure $ parseErrorAt off pleaseincludedecimalpoint
|
||||
else return $ dbg2 "style from format subdirective" astyle
|
||||
@ -532,7 +532,7 @@ transactionmodifierp = do
|
||||
-- | Parse a periodic transaction
|
||||
--
|
||||
-- This reuses periodexprp which parses period expressions on the command line.
|
||||
-- This is awkward because periodexprp supports relative and partial dates,
|
||||
-- This is awkward because periodexprp supports relative and partial dates,
|
||||
-- which we don't really need here, and it doesn't support the notion of a
|
||||
-- default year set by a Y directive, which we do need to consider here.
|
||||
-- We resolve it as follows: in periodic transactions' period expressions,
|
||||
@ -546,12 +546,12 @@ periodictransactionp = do
|
||||
lift $ skipMany spacenonewline
|
||||
-- a period expression
|
||||
off <- getOffset
|
||||
|
||||
|
||||
-- if there's a default year in effect, use Y/1/1 as base for partial/relative dates
|
||||
today <- liftIO getCurrentDay
|
||||
mdefaultyear <- getYear
|
||||
let refdate = case mdefaultyear of
|
||||
Nothing -> today
|
||||
Nothing -> today
|
||||
Just y -> fromGregorian y 1 1
|
||||
periodExcerpt <- lift $ excerpt_ $
|
||||
singlespacedtextsatisfyingp (\c -> c /= ';' && c /= '\n')
|
||||
@ -576,7 +576,7 @@ periodictransactionp = do
|
||||
case checkPeriodicTransactionStartDate interval span periodtxt of
|
||||
Just e -> customFailure $ parseErrorAt off e
|
||||
Nothing -> pure ()
|
||||
|
||||
|
||||
status <- lift statusp <?> "cleared status"
|
||||
code <- lift codep <?> "transaction code"
|
||||
description <- lift $ T.strip <$> descriptionp
|
||||
@ -678,7 +678,7 @@ tests_JournalReader = tests "JournalReader" [
|
||||
test "YYYY-MM-DD" $ expectParse datep "2018-01-01"
|
||||
test "YYYY.MM.DD" $ expectParse datep "2018.01.01"
|
||||
test "yearless date with no default year" $ expectParseError datep "1/1" "current year is unknown"
|
||||
test "yearless date with default year" $ do
|
||||
test "yearless date with default year" $ do
|
||||
let s = "1/1"
|
||||
ep <- parseWithState mempty{jparsedefaultyear=Just 2018} datep s
|
||||
either (fail.("parse error at "++).customErrorBundlePretty) (const ok) ep
|
||||
@ -703,7 +703,7 @@ tests_JournalReader = tests "JournalReader" [
|
||||
,tests "periodictransactionp" [
|
||||
|
||||
test "more period text in comment after one space" $ expectParseEq periodictransactionp
|
||||
"~ monthly from 2018/6 ;In 2019 we will change this\n"
|
||||
"~ monthly from 2018/6 ;In 2019 we will change this\n"
|
||||
nullperiodictransaction {
|
||||
ptperiodexpr = "monthly from 2018/6"
|
||||
,ptinterval = Months 1
|
||||
@ -713,7 +713,7 @@ tests_JournalReader = tests "JournalReader" [
|
||||
}
|
||||
|
||||
,test "more period text in description after two spaces" $ expectParseEq periodictransactionp
|
||||
"~ monthly from 2018/6 In 2019 we will change this\n"
|
||||
"~ monthly from 2018/6 In 2019 we will change this\n"
|
||||
nullperiodictransaction {
|
||||
ptperiodexpr = "monthly from 2018/6"
|
||||
,ptinterval = Months 1
|
||||
@ -748,16 +748,16 @@ tests_JournalReader = tests "JournalReader" [
|
||||
]
|
||||
|
||||
,tests "postingp" [
|
||||
test "basic" $ expectParseEq (postingp Nothing)
|
||||
test "basic" $ expectParseEq (postingp Nothing)
|
||||
" expenses:food:dining $10.00 ; a: a a \n ; b: b b \n"
|
||||
posting{
|
||||
paccount="expenses:food:dining",
|
||||
pamount=Mixed [usd 10],
|
||||
pcomment="a: a a\nb: b b\n",
|
||||
paccount="expenses:food:dining",
|
||||
pamount=Mixed [usd 10],
|
||||
pcomment="a: a a\nb: b b\n",
|
||||
ptags=[("a","a a"), ("b","b b")]
|
||||
}
|
||||
|
||||
,test "posting dates" $ expectParseEq (postingp Nothing)
|
||||
,test "posting dates" $ expectParseEq (postingp Nothing)
|
||||
" a 1. ; date:2012/11/28, date2=2012/11/29,b:b\n"
|
||||
nullposting{
|
||||
paccount="a"
|
||||
@ -768,14 +768,14 @@ tests_JournalReader = tests "JournalReader" [
|
||||
,pdate2=Nothing -- Just $ fromGregorian 2012 11 29
|
||||
}
|
||||
|
||||
,test "posting dates bracket syntax" $ expectParseEq (postingp Nothing)
|
||||
,test "posting dates bracket syntax" $ expectParseEq (postingp Nothing)
|
||||
" a 1. ; [2012/11/28=2012/11/29]\n"
|
||||
nullposting{
|
||||
paccount="a"
|
||||
,pamount=Mixed [num 1]
|
||||
,pcomment="[2012/11/28=2012/11/29]\n"
|
||||
,ptags=[]
|
||||
,pdate= Just $ fromGregorian 2012 11 28
|
||||
,pdate= Just $ fromGregorian 2012 11 28
|
||||
,pdate2=Just $ fromGregorian 2012 11 29
|
||||
}
|
||||
|
||||
@ -788,7 +788,7 @@ tests_JournalReader = tests "JournalReader" [
|
||||
|
||||
,tests "transactionmodifierp" [
|
||||
|
||||
test "basic" $ expectParseEq transactionmodifierp
|
||||
test "basic" $ expectParseEq transactionmodifierp
|
||||
"= (some value expr)\n some:postings 1.\n"
|
||||
nulltransactionmodifier {
|
||||
tmquerytxt = "(some value expr)"
|
||||
@ -797,10 +797,10 @@ tests_JournalReader = tests "JournalReader" [
|
||||
]
|
||||
|
||||
,tests "transactionp" [
|
||||
|
||||
|
||||
test "just a date" $ expectParseEq transactionp "2015/1/1\n" nulltransaction{tdate=fromGregorian 2015 1 1}
|
||||
|
||||
,test "more complex" $ expectParseEq transactionp
|
||||
|
||||
,test "more complex" $ expectParseEq transactionp
|
||||
(T.unlines [
|
||||
"2012/05/14=2012/05/15 (code) desc ; tcomment1",
|
||||
" ; tcomment2",
|
||||
@ -833,7 +833,7 @@ tests_JournalReader = tests "JournalReader" [
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
,test "parses a well-formed transaction" $
|
||||
expect $ isRight $ rjp transactionp $ T.unlines
|
||||
["2007/01/28 coopportunity"
|
||||
@ -841,10 +841,10 @@ tests_JournalReader = tests "JournalReader" [
|
||||
," assets:checking $-47.18"
|
||||
,""
|
||||
]
|
||||
|
||||
|
||||
,test "does not parse a following comment as part of the description" $
|
||||
expectParseEqOn transactionp "2009/1/1 a ;comment\n b 1\n" tdescription "a"
|
||||
|
||||
|
||||
,test "transactionp parses a following whitespace line" $
|
||||
expect $ isRight $ rjp transactionp $ T.unlines
|
||||
["2012/1/1"
|
||||
@ -863,7 +863,7 @@ tests_JournalReader = tests "JournalReader" [
|
||||
]
|
||||
|
||||
,test "comments everywhere, two postings parsed" $
|
||||
expectParseEqOn transactionp
|
||||
expectParseEqOn transactionp
|
||||
(T.unlines
|
||||
["2009/1/1 x ; transaction comment"
|
||||
," a 1 ; posting 1 comment"
|
||||
@ -873,13 +873,13 @@ tests_JournalReader = tests "JournalReader" [
|
||||
])
|
||||
(length . tpostings)
|
||||
2
|
||||
|
||||
|
||||
]
|
||||
|
||||
-- directives
|
||||
|
||||
,tests "directivep" [
|
||||
test "supports !" $ do
|
||||
test "supports !" $ do
|
||||
expectParseE directivep "!account a\n"
|
||||
expectParseE directivep "!D 1.0\n"
|
||||
]
|
||||
|
@ -96,7 +96,7 @@ timeclockfilep = do many timeclockitemp
|
||||
-- As all ledger line types can be distinguished by the first
|
||||
-- character, excepting transactions versus empty (blank or
|
||||
-- comment-only) lines, can use choice w/o try
|
||||
timeclockitemp = choice [
|
||||
timeclockitemp = choice [
|
||||
void (lift emptyorcommentlinep)
|
||||
, timeclockentryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j})
|
||||
] <?> "timeclock entry, or default year or historical price directive"
|
||||
|
@ -142,16 +142,16 @@ timedotnumericp = do
|
||||
(q, _, _, _) <- lift $ numberp Nothing
|
||||
msymbol <- optional $ choice $ map (string . fst) timeUnits
|
||||
lift (skipMany spacenonewline)
|
||||
let q' =
|
||||
let q' =
|
||||
case msymbol of
|
||||
Nothing -> q
|
||||
Just sym ->
|
||||
case lookup sym timeUnits of
|
||||
Just mult -> q * mult
|
||||
Just mult -> q * mult
|
||||
Nothing -> q -- shouldn't happen.. ignore
|
||||
return q'
|
||||
|
||||
-- (symbol, equivalent in hours).
|
||||
-- (symbol, equivalent in hours).
|
||||
timeUnits =
|
||||
[("s",2.777777777777778e-4)
|
||||
,("mo",5040) -- before "m"
|
||||
|
@ -11,7 +11,7 @@ module Hledger.Reports.BalanceReport (
|
||||
BalanceReportItem,
|
||||
balanceReport,
|
||||
flatShowsExclusiveBalance,
|
||||
sortAccountItemsLike,
|
||||
sortAccountItemsLike,
|
||||
|
||||
-- * Tests
|
||||
tests_BalanceReport
|
||||
@ -26,7 +26,7 @@ import Data.Time.Calendar
|
||||
import Hledger.Data
|
||||
import Hledger.Read (mamountp')
|
||||
import Hledger.Query
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
import Hledger.Reports.ReportOptions
|
||||
|
||||
|
||||
@ -64,8 +64,8 @@ flatShowsExclusiveBalance = True
|
||||
-- This is like PeriodChangeReport with a single column (but more mature,
|
||||
-- eg this can do hierarchical display).
|
||||
balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
|
||||
balanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
(if invert_ then brNegate else id) $
|
||||
balanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
(if invert_ then brNegate else id) $
|
||||
(sorteditems, total)
|
||||
where
|
||||
-- dbg1 = const id -- exclude from debug output
|
||||
@ -117,24 +117,24 @@ balanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
items = dbg1 "items" $ map (balanceReportItem ropts q) displayaccts
|
||||
|
||||
-- Sort report rows (except sorting by amount in tree mode, which was done above).
|
||||
sorteditems
|
||||
sorteditems
|
||||
| sort_amount_ && tree_ ropts = items
|
||||
| sort_amount_ = sortFlatBRByAmount items
|
||||
| otherwise = sortBRByAccountDeclaration items
|
||||
where
|
||||
-- Sort the report rows, representing a flat account list, by row total.
|
||||
where
|
||||
-- Sort the report rows, representing a flat account list, by row total.
|
||||
sortFlatBRByAmount :: [BalanceReportItem] -> [BalanceReportItem]
|
||||
sortFlatBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fourth4))
|
||||
where
|
||||
maybeflip = if normalbalance_ == Just NormallyNegative then id else flip
|
||||
-- Sort the report rows by account declaration order then account name.
|
||||
-- Sort the report rows by account declaration order then account name.
|
||||
sortBRByAccountDeclaration :: [BalanceReportItem] -> [BalanceReportItem]
|
||||
sortBRByAccountDeclaration rows = sortedrows
|
||||
where
|
||||
where
|
||||
anamesandrows = [(first4 r, r) | r <- rows]
|
||||
anames = map fst anamesandrows
|
||||
sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
|
||||
-- Calculate the grand total.
|
||||
total | not (flat_ ropts) = dbg1 "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0]
|
||||
@ -145,7 +145,7 @@ balanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
|
||||
-- | A sorting helper: sort a list of things (eg report rows) keyed by account name
|
||||
-- to match the provided ordering of those same account names.
|
||||
sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b]
|
||||
sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b]
|
||||
sortAccountItemsLike sortedas items =
|
||||
concatMap (\a -> maybe [] (:[]) $ lookup a items) sortedas
|
||||
|
||||
@ -181,7 +181,7 @@ balanceReportItem opts q a
|
||||
|
||||
-- | Flip the sign of all amounts in a BalanceReport.
|
||||
brNegate :: BalanceReport -> BalanceReport
|
||||
brNegate (is, tot) = (map brItemNegate is, -tot)
|
||||
brNegate (is, tot) = (map brItemNegate is, -tot)
|
||||
where
|
||||
brItemNegate (a, a', d, amt) = (a, a', d, -amt)
|
||||
|
||||
@ -222,10 +222,10 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
(showMixedAmountDebug etotal) `is` (showMixedAmountDebug atotal)
|
||||
usd0 = usd 0
|
||||
in [
|
||||
|
||||
|
||||
test "balanceReport with no args on null journal" $
|
||||
(defreportopts, nulljournal) `gives` ([], Mixed [nullamt])
|
||||
|
||||
|
||||
,test "balanceReport with no args on sample journal" $
|
||||
(defreportopts, samplejournal) `gives`
|
||||
([
|
||||
@ -242,7 +242,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,("income:salary","salary",1, mamountp' "$-1.00")
|
||||
],
|
||||
Mixed [usd0])
|
||||
|
||||
|
||||
,test "balanceReport with --depth=N" $
|
||||
(defreportopts{depth_=Just 1}, samplejournal) `gives`
|
||||
([
|
||||
@ -250,7 +250,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,("income", "income", 0, mamountp' "$-2.00")
|
||||
],
|
||||
Mixed [usd0])
|
||||
|
||||
|
||||
,test "balanceReport with depth:N" $
|
||||
(defreportopts{query_="depth:1"}, samplejournal) `gives`
|
||||
([
|
||||
@ -258,7 +258,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,("income", "income", 0, mamountp' "$-2.00")
|
||||
],
|
||||
Mixed [usd0])
|
||||
|
||||
|
||||
,tests "balanceReport with a date or secondary date span" [
|
||||
(defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives`
|
||||
([],
|
||||
@ -278,7 +278,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,("income:salary","income:salary",0, mamountp' "$-1.00")
|
||||
],
|
||||
Mixed [usd0])
|
||||
|
||||
|
||||
,test "balanceReport with not:desc:" $
|
||||
(defreportopts{query_="not:desc:income"}, samplejournal) `gives`
|
||||
([
|
||||
@ -291,7 +291,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,("income:gifts","income:gifts",0, mamountp' "$-1.00")
|
||||
],
|
||||
Mixed [usd0])
|
||||
|
||||
|
||||
,test "balanceReport with period on a populated period" $
|
||||
(defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2)}, samplejournal) `gives`
|
||||
(
|
||||
@ -300,13 +300,13 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,("income:salary","income:salary",0, mamountp' "$-1.00")
|
||||
],
|
||||
Mixed [usd0])
|
||||
|
||||
|
||||
,test "balanceReport with period on an unpopulated period" $
|
||||
(defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}, samplejournal) `gives`
|
||||
([],Mixed [nullamt])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{-
|
||||
,test "accounts report with account pattern o" ~:
|
||||
defreportopts{patterns_=["o"]} `gives`
|
||||
@ -317,7 +317,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,"--------------------"
|
||||
," $-1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with account pattern o and --depth 1" ~:
|
||||
defreportopts{patterns_=["o"],depth_=Just 1} `gives`
|
||||
[" $1 expenses"
|
||||
@ -325,7 +325,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,"--------------------"
|
||||
," $-1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with account pattern a" ~:
|
||||
defreportopts{patterns_=["a"]} `gives`
|
||||
[" $-1 assets"
|
||||
@ -336,7 +336,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,"--------------------"
|
||||
," $-1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with account pattern e" ~:
|
||||
defreportopts{patterns_=["e"]} `gives`
|
||||
[" $-1 assets"
|
||||
@ -352,7 +352,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,"--------------------"
|
||||
," 0"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with unmatched parent of two matched subaccounts" ~:
|
||||
defreportopts{patterns_=["cash","saving"]} `gives`
|
||||
[" $-1 assets"
|
||||
@ -361,14 +361,14 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,"--------------------"
|
||||
," $-1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with multi-part account name" ~:
|
||||
defreportopts{patterns_=["expenses:food"]} `gives`
|
||||
[" $1 expenses:food"
|
||||
,"--------------------"
|
||||
," $1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with negative account pattern" ~:
|
||||
defreportopts{patterns_=["not:assets"]} `gives`
|
||||
[" $2 expenses"
|
||||
@ -381,20 +381,20 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,"--------------------"
|
||||
," $1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report negative account pattern always matches full name" ~:
|
||||
defreportopts{patterns_=["not:e"]} `gives`
|
||||
["--------------------"
|
||||
," 0"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report negative patterns affect totals" ~:
|
||||
defreportopts{patterns_=["expenses","not:food"]} `gives`
|
||||
[" $1 expenses:supplies"
|
||||
,"--------------------"
|
||||
," $1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with -E shows zero-balance accounts" ~:
|
||||
defreportopts{patterns_=["assets"],empty_=True} `gives`
|
||||
[" $-1 assets"
|
||||
@ -405,7 +405,7 @@ tests_BalanceReport = tests "BalanceReport" [
|
||||
,"--------------------"
|
||||
," $-1"
|
||||
]
|
||||
|
||||
|
||||
,test "accounts report with cost basis" $
|
||||
j <- (readJournal def Nothing $ unlines
|
||||
[""
|
||||
|
@ -72,14 +72,14 @@ budgetReport ropts' assrt reportspan d j =
|
||||
-- and that reports with and without --empty make sense when compared side by side
|
||||
ropts = ropts' { accountlistmode_ = ALTree }
|
||||
showunbudgeted = empty_ ropts
|
||||
q = queryFromOpts d ropts
|
||||
budgetedaccts =
|
||||
q = queryFromOpts d ropts
|
||||
budgetedaccts =
|
||||
dbg2 "budgetedacctsinperiod" $
|
||||
nub $
|
||||
nub $
|
||||
concatMap expandAccountName $
|
||||
accountNamesFromPostings $
|
||||
concatMap tpostings $
|
||||
concatMap (flip runPeriodicTransaction reportspan) $
|
||||
accountNamesFromPostings $
|
||||
concatMap tpostings $
|
||||
concatMap (flip runPeriodicTransaction reportspan) $
|
||||
jperiodictxns j
|
||||
actualj = dbg1 "actualj" $ budgetRollUp budgetedaccts showunbudgeted j
|
||||
budgetj = dbg1 "budgetj" $ budgetJournal assrt ropts reportspan j
|
||||
@ -87,10 +87,10 @@ budgetReport ropts' assrt reportspan d j =
|
||||
budgetgoalreport@(MultiBalanceReport (_, budgetgoalitems, budgetgoaltotals)) = dbg1 "budgetgoalreport" $ multiBalanceReport (ropts{empty_=True}) q budgetj
|
||||
budgetgoalreport'
|
||||
-- If no interval is specified:
|
||||
-- budgetgoalreport's span might be shorter actualreport's due to periodic txns;
|
||||
-- it should be safe to replace it with the latter, so they combine well.
|
||||
-- budgetgoalreport's span might be shorter actualreport's due to periodic txns;
|
||||
-- it should be safe to replace it with the latter, so they combine well.
|
||||
| interval_ ropts == NoInterval = MultiBalanceReport (actualspans, budgetgoalitems, budgetgoaltotals)
|
||||
| otherwise = budgetgoalreport
|
||||
| otherwise = budgetgoalreport
|
||||
budgetreport = combineBudgetAndActual budgetgoalreport' actualreport
|
||||
sortedbudgetreport = sortBudgetReport ropts j budgetreport
|
||||
in
|
||||
@ -100,13 +100,13 @@ budgetReport ropts' assrt reportspan d j =
|
||||
sortBudgetReport :: ReportOpts -> Journal -> BudgetReport -> BudgetReport
|
||||
sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps, sortedrows, trow)
|
||||
where
|
||||
sortedrows
|
||||
sortedrows
|
||||
| sort_amount_ ropts && tree_ ropts = sortTreeBURByActualAmount rows
|
||||
| sort_amount_ ropts = sortFlatBURByActualAmount rows
|
||||
| otherwise = sortByAccountDeclaration rows
|
||||
|
||||
-- Sort a tree-mode budget report's rows by total actual amount at each level.
|
||||
sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
|
||||
sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
|
||||
sortTreeBURByActualAmount rows = sortedrows
|
||||
where
|
||||
anamesandrows = [(first6 r, r) | r <- rows]
|
||||
@ -116,21 +116,21 @@ sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps,
|
||||
accounttreewithbals = mapAccounts setibalance accounttree
|
||||
where
|
||||
setibalance a = a{aibalance=
|
||||
fromMaybe 0 $ -- when there's no actual amount, assume 0; will mess up with negative amounts ? TODO
|
||||
fromMaybe (error "sortTreeByAmount 1") $ -- should not happen, but it's ugly; TODO
|
||||
fromMaybe 0 $ -- when there's no actual amount, assume 0; will mess up with negative amounts ? TODO
|
||||
fromMaybe (error "sortTreeByAmount 1") $ -- should not happen, but it's ugly; TODO
|
||||
lookup (aname a) atotals
|
||||
}
|
||||
sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ ropts) accounttreewithbals
|
||||
sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
|
||||
-- Sort a flat-mode budget report's rows by total actual amount.
|
||||
sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
|
||||
sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
|
||||
sortFlatBURByActualAmount = sortBy (maybeflip $ comparing (fst . fifth6))
|
||||
where
|
||||
maybeflip = if normalbalance_ ropts == Just NormallyNegative then id else flip
|
||||
|
||||
-- Sort the report rows by account declaration order then account name.
|
||||
-- Sort the report rows by account declaration order then account name.
|
||||
-- <unbudgeted> remains at the top.
|
||||
sortByAccountDeclaration rows = sortedrows
|
||||
where
|
||||
@ -138,9 +138,9 @@ sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps,
|
||||
anamesandrows = [(first6 r, r) | r <- rows']
|
||||
anames = map fst anamesandrows
|
||||
sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames
|
||||
sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows
|
||||
sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows
|
||||
|
||||
-- | Use all periodic transactions in the journal to generate
|
||||
-- | Use all periodic transactions in the journal to generate
|
||||
-- budget transactions in the specified report period.
|
||||
-- Budget transactions are similar to forecast transactions except
|
||||
-- their purpose is to set goal amounts (of change) per account and period.
|
||||
@ -159,11 +159,11 @@ budgetJournal assrt _ropts reportspan j =
|
||||
|
||||
-- | Adjust a journal's account names for budget reporting, in two ways:
|
||||
--
|
||||
-- 1. accounts with no budget goal anywhere in their ancestry are moved
|
||||
-- 1. accounts with no budget goal anywhere in their ancestry are moved
|
||||
-- under the "unbudgeted" top level account.
|
||||
--
|
||||
-- 2. subaccounts with no budget goal are merged with their closest parent account
|
||||
-- with a budget goal, so that only budgeted accounts are shown.
|
||||
-- with a budget goal, so that only budgeted accounts are shown.
|
||||
-- This can be disabled by --empty.
|
||||
--
|
||||
budgetRollUp :: [AccountName] -> Bool -> Journal -> Journal
|
||||
@ -176,7 +176,7 @@ budgetRollUp budgetedaccts showunbudgeted j = j { jtxns = remapTxn <$> jtxns j }
|
||||
where
|
||||
remapAccount a
|
||||
| hasbudget = a
|
||||
| hasbudgetedparent = if showunbudgeted then a else budgetedparent
|
||||
| hasbudgetedparent = if showunbudgeted then a else budgetedparent
|
||||
| otherwise = if showunbudgeted then u <> acctsep <> a else u
|
||||
where
|
||||
hasbudget = a `elem` budgetedaccts
|
||||
@ -270,7 +270,7 @@ budgetReportSpan (PeriodicReport (spans, _, _)) = DateSpan (spanStart $ head spa
|
||||
-- | Render a budget report as plain text suitable for console output.
|
||||
budgetReportAsText :: ReportOpts -> BudgetReport -> String
|
||||
budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) =
|
||||
title ++ "\n\n" ++
|
||||
title ++ "\n\n" ++
|
||||
tableAsText ropts showcell (maybetranspose $ budgetReportAsTable ropts budgetr)
|
||||
where
|
||||
multiperiod = interval_ /= NoInterval
|
||||
@ -319,7 +319,7 @@ budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) =
|
||||
percentage :: Change -> BudgetGoal -> Maybe Percentage
|
||||
percentage actual budget =
|
||||
case (maybecost $ normaliseMixedAmount actual, maybecost $ normaliseMixedAmount budget) of
|
||||
(Mixed [a], Mixed [b]) | (acommodity a == acommodity b || isZeroAmount a) && not (isZeroAmount b)
|
||||
(Mixed [a], Mixed [b]) | (acommodity a == acommodity b || isZeroAmount a) && not (isZeroAmount b)
|
||||
-> Just $ 100 * aquantity a / aquantity b
|
||||
_ -> -- trace (pshow $ (maybecost actual, maybecost budget)) -- debug missing percentage
|
||||
Nothing
|
||||
@ -337,14 +337,14 @@ budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) =
|
||||
|
||||
-- | Build a 'Table' from a multi-column balance report.
|
||||
budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount)
|
||||
budgetReportAsTable
|
||||
ropts
|
||||
budgetReportAsTable
|
||||
ropts
|
||||
(PeriodicReport
|
||||
( periods
|
||||
, rows
|
||||
, (_, _, _, coltots, grandtot, grandavg)
|
||||
)) =
|
||||
addtotalrow $
|
||||
addtotalrow $
|
||||
Table
|
||||
(T.Group NoLine $ map Header accts)
|
||||
(T.Group NoLine $ map Header colheadings)
|
||||
@ -368,7 +368,7 @@ budgetReportAsTable
|
||||
))
|
||||
|
||||
-- XXX here for now
|
||||
-- TODO: does not work for flat-by-default reports with --flat not specified explicitly
|
||||
-- TODO: does not work for flat-by-default reports with --flat not specified explicitly
|
||||
-- | Drop leading components of accounts names as specified by --drop, but only in --flat mode.
|
||||
maybeAccountNameDrop :: ReportOpts -> AccountName -> AccountName
|
||||
maybeAccountNameDrop opts a | flat_ opts = accountNameDrop (drop_ opts) a
|
||||
|
@ -23,7 +23,7 @@ import Data.Time.Calendar (Day, addDays)
|
||||
import Hledger.Data
|
||||
import Hledger.Query
|
||||
import Hledger.Reports.ReportOptions
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
|
||||
|
||||
-- | A journal entries report is a list of whole transactions as
|
||||
|
@ -30,7 +30,7 @@ import Text.Tabular.AsciiWide
|
||||
|
||||
import Hledger.Data
|
||||
import Hledger.Query
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
import Hledger.Read (mamountp')
|
||||
import Hledger.Reports.ReportOptions
|
||||
import Hledger.Reports.BalanceReport
|
||||
@ -85,13 +85,13 @@ type ClippedAccountName = AccountName
|
||||
-- | Generate a multicolumn balance report for the matched accounts,
|
||||
-- showing the change of balance, accumulated balance, or historical balance
|
||||
-- in each of the specified periods. Does not support tree-mode boring parent eliding.
|
||||
-- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts
|
||||
-- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts
|
||||
-- (see ReportOpts and CompoundBalanceCommand).
|
||||
-- hledger's most powerful and useful report, used by the balance
|
||||
-- command (in multiperiod mode) and by the bs/cf/is commands.
|
||||
multiBalanceReport :: ReportOpts -> Query -> Journal -> MultiBalanceReport
|
||||
multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
(if invert_ then mbrNegate else id) $
|
||||
(if invert_ then mbrNegate else id) $
|
||||
MultiBalanceReport (colspans, sortedrows, totalsrow)
|
||||
where
|
||||
dbg1 s = let p = "multiBalanceReport" in Hledger.Utils.dbg1 (p++" "++s) -- add prefix in this function's debug output
|
||||
@ -115,18 +115,18 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
-- This list can be empty if the journal was empty,
|
||||
-- or if hledger-ui has added its special date:-tomorrow to the query
|
||||
-- and all txns are in the future.
|
||||
intervalspans = dbg1 "intervalspans" $ splitSpan interval_ requestedspan'
|
||||
intervalspans = dbg1 "intervalspans" $ splitSpan interval_ requestedspan'
|
||||
-- The requested span enlarged to enclose a whole number of intervals.
|
||||
-- This can be the null span if there were no intervals.
|
||||
-- This can be the null span if there were no intervals.
|
||||
reportspan = dbg1 "reportspan" $ DateSpan (maybe Nothing spanStart $ headMay intervalspans)
|
||||
(maybe Nothing spanEnd $ lastMay intervalspans)
|
||||
mreportstart = spanStart reportspan
|
||||
-- The user's query with no depth limit, and expanded to the report span
|
||||
-- if there is one (otherwise any date queries are left as-is, which
|
||||
-- handles the hledger-ui+future txns case above).
|
||||
reportq = dbg1 "reportq" $ depthless $
|
||||
if reportspan == nulldatespan
|
||||
then q
|
||||
reportq = dbg1 "reportq" $ depthless $
|
||||
if reportspan == nulldatespan
|
||||
then q
|
||||
else And [datelessq, reportspandatesq]
|
||||
where
|
||||
reportspandatesq = dbg1 "reportspandatesq" $ dateqcons reportspan
|
||||
@ -157,12 +157,12 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
precedingperiod = dateSpanAsPeriod $ spanIntersect (DateSpan Nothing mreportstart) $ periodAsDateSpan period_
|
||||
-- q projected back before the report start date.
|
||||
-- When there's no report start date, in case there are future txns (the hledger-ui case above),
|
||||
-- we use emptydatespan to make sure they aren't counted as starting balance.
|
||||
-- we use emptydatespan to make sure they aren't counted as starting balance.
|
||||
startbalq = dbg1 "startbalq" $ And [datelessq, dateqcons precedingspan]
|
||||
where
|
||||
precedingspan = case mreportstart of
|
||||
Just d -> DateSpan Nothing (Just d)
|
||||
Nothing -> emptydatespan
|
||||
Nothing -> emptydatespan
|
||||
-- The matched accounts with a starting balance. All of these should appear
|
||||
-- in the report even if they have no postings during the report period.
|
||||
startaccts = dbg1 "startaccts" $ map fst startbals
|
||||
@ -282,7 +282,7 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
(error' "multiBalanceReport: expected all spans to have an end date") -- XXX should not happen
|
||||
(addDays (-1)))
|
||||
. spanEnd) colspans
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- 7. Sort the report rows.
|
||||
|
||||
@ -307,24 +307,24 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
accounttree = accountTree "root" anames
|
||||
accounttreewithbals = mapAccounts setibalance accounttree
|
||||
where
|
||||
-- should not happen, but it's dangerous; TODO
|
||||
-- should not happen, but it's dangerous; TODO
|
||||
setibalance a = a{aibalance=fromMaybe (error "sortTreeMBRByAmount 1") $ lookup (aname a) atotals}
|
||||
sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive normalbalance_) accounttreewithbals
|
||||
sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
|
||||
-- Sort the report rows, representing a flat account list, by row total.
|
||||
-- Sort the report rows, representing a flat account list, by row total.
|
||||
sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fifth6))
|
||||
where
|
||||
maybeflip = if normalbalance_ == Just NormallyNegative then id else flip
|
||||
|
||||
-- Sort the report rows by account declaration order then account name.
|
||||
-- Sort the report rows by account declaration order then account name.
|
||||
sortMBRByAccountDeclaration rows = sortedrows
|
||||
where
|
||||
where
|
||||
anamesandrows = [(first6 r, r) | r <- rows]
|
||||
anames = map fst anamesandrows
|
||||
sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- 8. Build the report totals row.
|
||||
@ -364,9 +364,9 @@ multiBalanceReportSpan :: MultiBalanceReport -> DateSpan
|
||||
multiBalanceReportSpan (MultiBalanceReport ([], _, _)) = DateSpan Nothing Nothing
|
||||
multiBalanceReportSpan (MultiBalanceReport (colspans, _, _)) = DateSpan (spanStart $ head colspans) (spanEnd $ last colspans)
|
||||
|
||||
-- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport,
|
||||
-- in order to support --historical. Does not support tree-mode boring parent eliding.
|
||||
-- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts
|
||||
-- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport,
|
||||
-- in order to support --historical. Does not support tree-mode boring parent eliding.
|
||||
-- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts
|
||||
-- (see ReportOpts and CompoundBalanceCommand).
|
||||
balanceReportFromMultiBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
|
||||
balanceReportFromMultiBalanceReport opts q j = (rows', total)
|
||||
@ -408,11 +408,11 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [
|
||||
((\(_, b, _) -> showMixedAmountDebug b) atotal) `is` (showMixedAmountDebug etotal) -- we only check the sum of the totals
|
||||
usd0 = usd 0
|
||||
amount0 = Amount {acommodity="$", aquantity=0, aprice=Nothing, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 2, asdecimalpoint = Just '.', asdigitgroups = Nothing}, aismultiplier=False}
|
||||
in
|
||||
in
|
||||
tests "multiBalanceReport" [
|
||||
test "null journal" $
|
||||
(defreportopts, nulljournal) `gives` ([], Mixed [nullamt])
|
||||
|
||||
|
||||
,test "with -H on a populated period" $
|
||||
(defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`
|
||||
(
|
||||
@ -421,7 +421,7 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [
|
||||
,("income:salary" ,"salary" , 2, [mamountp' "$-1.00"], Mixed [nullamt], Mixed [amount0 {aquantity=(-1)}])
|
||||
],
|
||||
Mixed [nullamt])
|
||||
|
||||
|
||||
,_test "a valid history on an empty period" $
|
||||
(defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3), balancetype_=HistoricalBalance}, samplejournal) `gives`
|
||||
(
|
||||
@ -430,7 +430,7 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [
|
||||
,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",Mixed [amount0 {aquantity=(-1)}])
|
||||
],
|
||||
Mixed [usd0])
|
||||
|
||||
|
||||
,_test "a valid history on an empty period (more complex)" $
|
||||
(defreportopts{period_= PeriodBetween (fromGregorian 2009 1 1) (fromGregorian 2009 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives`
|
||||
(
|
||||
|
@ -32,7 +32,7 @@ import Safe (headMay, lastMay)
|
||||
|
||||
import Hledger.Data
|
||||
import Hledger.Query
|
||||
import Hledger.Utils
|
||||
import Hledger.Utils
|
||||
import Hledger.Reports.ReportOptions
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ postingsReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
|
||||
-- Postings, or summary postings with their subperiod's end date, to be displayed.
|
||||
displayps :: [(Posting, Maybe Day)]
|
||||
| multiperiod =
|
||||
| multiperiod =
|
||||
let summaryps = summarisePostingsByInterval interval_ whichdate depth showempty reportspan reportps
|
||||
in [(pvalue p lastday, Just periodend) | (p, periodend) <- summaryps, let lastday = addDays (-1) periodend]
|
||||
| otherwise =
|
||||
@ -286,13 +286,13 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
,(Depth 2, samplejournal) `gives` 13
|
||||
,(And [Depth 1, StatusQ Cleared, Acct "expenses"], samplejournal) `gives` 2
|
||||
,(And [And [Depth 1, StatusQ Cleared], Acct "expenses"], samplejournal) `gives` 2
|
||||
|
||||
|
||||
-- with query and/or command-line options
|
||||
,(length $ snd $ postingsReport defreportopts Any samplejournal) `is` 13
|
||||
,(length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal) `is` 11
|
||||
,(length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal) `is` 20
|
||||
,(length $ snd $ postingsReport defreportopts (Acct "assets:bank:checking") samplejournal) `is` 5
|
||||
|
||||
|
||||
-- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
|
||||
-- [(Just (parsedate "2008-01-01","income"),assets:bank:checking $1,$1)
|
||||
-- ,(Nothing,income:salary $-1,0)
|
||||
@ -304,7 +304,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
-- ,(Nothing,expenses:supplies $1,$2)
|
||||
-- ,(Nothing,assets:cash $-2,0)
|
||||
-- ,(Just (2008-12-31,"pay off"),liabilities:debts $1,$1)
|
||||
-- ,(Nothing,assets:bank:checking $-1,0)
|
||||
-- ,(Nothing,assets:bank:checking $-1,0)
|
||||
|
||||
{-
|
||||
let opts = defreportopts
|
||||
@ -321,7 +321,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
,"2008/12/31 pay off liabilities:debts $1 $1"
|
||||
," assets:bank:checking $-1 0"
|
||||
]
|
||||
|
||||
|
||||
,"postings report with cleared option" ~:
|
||||
do
|
||||
let opts = defreportopts{cleared_=True}
|
||||
@ -333,7 +333,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
,"2008/12/31 pay off liabilities:debts $1 $1"
|
||||
," assets:bank:checking $-1 0"
|
||||
]
|
||||
|
||||
|
||||
,"postings report with uncleared option" ~:
|
||||
do
|
||||
let opts = defreportopts{uncleared_=True}
|
||||
@ -346,7 +346,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
,"2008/06/02 save assets:bank:saving $1 $1"
|
||||
," assets:bank:checking $-1 0"
|
||||
]
|
||||
|
||||
|
||||
,"postings report sorts by date" ~:
|
||||
do
|
||||
j <- readJournal' $ unlines
|
||||
@ -360,7 +360,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
]
|
||||
let opts = defreportopts
|
||||
registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/02/02"]
|
||||
|
||||
|
||||
,"postings report with account pattern" ~:
|
||||
do
|
||||
j <- samplejournal
|
||||
@ -368,7 +368,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
|
||||
["2008/06/03 eat & shop assets:cash $-2 $-2"
|
||||
]
|
||||
|
||||
|
||||
,"postings report with account pattern, case insensitive" ~:
|
||||
do
|
||||
j <- samplejournal
|
||||
@ -376,7 +376,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
|
||||
["2008/06/03 eat & shop assets:cash $-2 $-2"
|
||||
]
|
||||
|
||||
|
||||
,"postings report with display expression" ~:
|
||||
do
|
||||
j <- samplejournal
|
||||
@ -388,7 +388,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
"d=[2008/6/2]" `gives` ["2008/06/02"]
|
||||
"d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"]
|
||||
"d>[2008/6/2]" `gives` ["2008/06/03","2008/12/31"]
|
||||
|
||||
|
||||
,"postings report with period expression" ~:
|
||||
do
|
||||
j <- samplejournal
|
||||
@ -416,9 +416,9 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/10/01"]
|
||||
let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True}
|
||||
registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
, "postings report with depth arg" ~:
|
||||
do
|
||||
j <- samplejournal
|
||||
@ -436,7 +436,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
,"2008/12/31 pay off liabilities:debts $1 $1"
|
||||
," assets:bank $-1 0"
|
||||
]
|
||||
|
||||
|
||||
-}
|
||||
]
|
||||
|
||||
@ -445,7 +445,7 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
summarisePostingsByInterval (Quarters 1) PrimaryDate 99999 False (DateSpan Nothing Nothing) [] `is` []
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
-- ,tests_summarisePostingsInDateSpan = [
|
||||
-- "summarisePostingsInDateSpan" ~: do
|
||||
-- let gives (b,e,depth,showempty,ps) =
|
||||
@ -481,5 +481,5 @@ tests_PostingsReport = tests "PostingsReport" [
|
||||
-- [
|
||||
-- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [usd 15]}
|
||||
-- ]
|
||||
|
||||
|
||||
]
|
||||
|
@ -81,7 +81,7 @@ instance Default AccountListMode where def = ALDefault
|
||||
-- | Standard options for customising report filtering and output.
|
||||
-- Most of these correspond to standard hledger command-line options
|
||||
-- or query arguments, but not all. Some are used only by certain
|
||||
-- commands, as noted below.
|
||||
-- commands, as noted below.
|
||||
data ReportOpts = ReportOpts {
|
||||
today_ :: Maybe Day -- ^ The current date. A late addition to ReportOpts.
|
||||
-- Optional, but when set it may affect some reports:
|
||||
@ -116,10 +116,10 @@ data ReportOpts = ReportOpts {
|
||||
-- ^ This can be set when running balance reports on a set of accounts
|
||||
-- with the same normal balance type (eg all assets, or all incomes).
|
||||
-- - It helps --sort-amount know how to sort negative numbers
|
||||
-- (eg in the income section of an income statement)
|
||||
-- - It helps compound balance report commands (is, bs etc.) do
|
||||
-- sign normalisation, converting normally negative subreports to
|
||||
-- normally positive for a more conventional display.
|
||||
-- (eg in the income section of an income statement)
|
||||
-- - It helps compound balance report commands (is, bs etc.) do
|
||||
-- sign normalisation, converting normally negative subreports to
|
||||
-- normally positive for a more conventional display.
|
||||
,color_ :: Bool
|
||||
,forecast_ :: Bool
|
||||
,transpose_ :: Bool
|
||||
@ -328,7 +328,7 @@ simplifyStatuses l
|
||||
| length l' >= numstatuses = []
|
||||
| otherwise = l'
|
||||
where
|
||||
l' = nub $ sort l
|
||||
l' = nub $ sort l
|
||||
numstatuses = length [minBound .. maxBound :: Status]
|
||||
|
||||
-- | Add/remove this status from the status list. Used by hledger-ui.
|
||||
@ -442,7 +442,7 @@ queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts
|
||||
-- Report dates.
|
||||
|
||||
-- | The effective report span is the start and end dates specified by
|
||||
-- options or queries, or otherwise the earliest and latest transaction or
|
||||
-- options or queries, or otherwise the earliest and latest transaction or
|
||||
-- posting dates in the journal. If no dates are specified by options/queries
|
||||
-- and the journal is empty, returns the null date span.
|
||||
-- Needs IO to parse smart dates in options/queries.
|
||||
@ -500,7 +500,7 @@ reportPeriodOrJournalStart ropts@ReportOpts{..} j =
|
||||
reportPeriodStart ropts <|> journalStartDate False j
|
||||
|
||||
-- Get the last day of the overall report period.
|
||||
-- This the inclusive end date (one day before the
|
||||
-- This the inclusive end date (one day before the
|
||||
-- more commonly used, exclusive, report end date).
|
||||
-- If no report period is specified, will be Nothing.
|
||||
-- Will also be Nothing if ReportOpts does not have today_ set,
|
||||
@ -528,7 +528,7 @@ tests_ReportOptions = tests "ReportOptions" [
|
||||
(queryFromOpts nulldate defreportopts) `is` Any
|
||||
,(queryFromOpts nulldate defreportopts{query_="a"}) `is` (Acct "a")
|
||||
,(queryFromOpts nulldate defreportopts{query_="desc:'a a'"}) `is` (Desc "a a")
|
||||
,(queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01"),query_="date:'to 2013'" })
|
||||
,(queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01"),query_="date:'to 2013'" })
|
||||
`is` (Date $ mkdatespan "2012/01/01" "2013/01/01")
|
||||
,(queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"}) `is` (Date2 $ mkdatespan "2012/01/01" "2013/01/01")
|
||||
,(queryFromOpts nulldate defreportopts{query_="'a a' 'b"}) `is` (Or [Acct "a a", Acct "'b"])
|
||||
|
@ -33,7 +33,7 @@ data PeriodicReport a =
|
||||
type PeriodicReportRow a =
|
||||
( AccountName -- A full account name.
|
||||
, AccountName -- Shortened form of the account name to display in tree mode. Usually the leaf name, possibly with parent accounts prefixed.
|
||||
, Int -- Indent level for displaying this account name in tree mode. 0, 1, 2...
|
||||
, Int -- Indent level for displaying this account name in tree mode. 0, 1, 2...
|
||||
, [a] -- The data value for each subperiod.
|
||||
, a -- The total of this row's values.
|
||||
, a -- The average of this row's values.
|
||||
|
@ -161,14 +161,14 @@ firstJust ms = case dropWhile (==Nothing) ms of
|
||||
[] -> Nothing
|
||||
(md:_) -> md
|
||||
|
||||
-- | Read text from a file,
|
||||
-- | Read text from a file,
|
||||
-- handling any of the usual line ending conventions,
|
||||
-- using the system locale's text encoding,
|
||||
-- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8.
|
||||
-- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8.
|
||||
readFilePortably :: FilePath -> IO Text
|
||||
readFilePortably f = openFile f ReadMode >>= readHandlePortably
|
||||
|
||||
-- | Like readFilePortably, but read from standard input if the path is "-".
|
||||
-- | Like readFilePortably, but read from standard input if the path is "-".
|
||||
readFileOrStdinPortably :: String -> IO Text
|
||||
readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably
|
||||
where
|
||||
@ -236,7 +236,7 @@ embedFileRelative f = makeRelativeToProject f >>= embedStringFile
|
||||
-- hereFileRelative f = makeRelativeToProject f >>= hereFileExp
|
||||
-- where
|
||||
-- QuasiQuoter{quoteExp=hereFileExp} = hereFile
|
||||
|
||||
|
||||
tests_Utils = tests "Utils" [
|
||||
tests_Text
|
||||
]
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Hledger.Utils.Color
|
||||
module Hledger.Utils.Color
|
||||
(
|
||||
color,
|
||||
bgColor,
|
||||
|
@ -94,7 +94,7 @@ traceWith f a = trace (f a) a
|
||||
-- touch and reload this module to see the effect of a new --debug option.
|
||||
-- After command-line processing, it is also available as the @debug_@
|
||||
-- field of 'Hledger.Cli.CliOptions.CliOpts'.
|
||||
-- {-# OPTIONS_GHC -fno-cse #-}
|
||||
-- {-# OPTIONS_GHC -fno-cse #-}
|
||||
-- {-# NOINLINE debugLevel #-}
|
||||
debugLevel :: Int
|
||||
debugLevel = case snd $ break (=="--debug") args of
|
||||
@ -251,7 +251,7 @@ dbg9IO = ptraceAtIO 9
|
||||
plog :: Show a => String -> a -> a
|
||||
plog = plogAt 0
|
||||
|
||||
-- | Log a label and a pretty-printed showable value to ./debug.log,
|
||||
-- | Log a label and a pretty-printed showable value to ./debug.log,
|
||||
-- if the global debug level is at or above the specified level.
|
||||
-- At level 0, always logs. Otherwise, uses unsafePerformIO.
|
||||
-- Tends to fail if called more than once, at least when built with -threaded
|
||||
@ -259,7 +259,7 @@ plog = plogAt 0
|
||||
plogAt :: Show a => Int -> String -> a -> a
|
||||
plogAt lvl
|
||||
| lvl > 0 && debugLevel < lvl = flip const
|
||||
| otherwise = \s a ->
|
||||
| otherwise = \s a ->
|
||||
let p = ppShow a
|
||||
ls = lines p
|
||||
nlorspace | length ls > 1 = "\n"
|
||||
|
@ -322,9 +322,9 @@ takeWidth w (c:cs) | cw <= w = c:takeWidth (w-cw) cs
|
||||
-- see also http://unicode.org/reports/tr11/#Description
|
||||
|
||||
-- | Calculate the render width of a string, considering
|
||||
-- wide characters (counted as double width), ANSI escape codes
|
||||
-- wide characters (counted as double width), ANSI escape codes
|
||||
-- (not counted), and line breaks (in a multi-line string, the longest
|
||||
-- line determines the width).
|
||||
-- line determines the width).
|
||||
strWidth :: String -> Int
|
||||
strWidth "" = 0
|
||||
strWidth s = maximum $ map (foldr (\a b -> charWidth a + b) 0) $ lines s'
|
||||
|
@ -24,7 +24,7 @@ module Hledger.Utils.Test (
|
||||
,expectParseEqOn
|
||||
,expectParseEqOnE
|
||||
,expectParseStateOn
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception
|
||||
@ -36,7 +36,7 @@ import Data.Monoid ((<>))
|
||||
import Data.CallStack
|
||||
import Data.List
|
||||
import qualified Data.Text as T
|
||||
import Safe
|
||||
import Safe
|
||||
import System.Exit
|
||||
import Text.Megaparsec
|
||||
import Text.Megaparsec.Custom
|
||||
@ -50,38 +50,38 @@ import Hledger.Utils.UTF8IOCompat (error')
|
||||
-- * easytest helpers
|
||||
|
||||
-- | Name the given test(s). A readability synonym for easytest's "scope".
|
||||
test :: T.Text -> E.Test a -> E.Test a
|
||||
test :: T.Text -> E.Test a -> E.Test a
|
||||
test = E.scope
|
||||
|
||||
-- | Skip the given test(s), with the same type signature as "test".
|
||||
-- If called in a monadic sequence of tests, also skips following tests.
|
||||
_test :: T.Text -> E.Test a -> E.Test a
|
||||
_test _name = (E.skip >>)
|
||||
_test :: T.Text -> E.Test a -> E.Test a
|
||||
_test _name = (E.skip >>)
|
||||
|
||||
-- | Name the given test(s). A synonym for "test".
|
||||
it :: T.Text -> E.Test a -> E.Test a
|
||||
it :: T.Text -> E.Test a -> E.Test a
|
||||
it = test
|
||||
|
||||
-- | Skip the given test(s), and any following tests in a monadic sequence.
|
||||
-- | Skip the given test(s), and any following tests in a monadic sequence.
|
||||
-- A synonym for "_test".
|
||||
_it :: T.Text -> E.Test a -> E.Test a
|
||||
_it :: T.Text -> E.Test a -> E.Test a
|
||||
_it = _test
|
||||
|
||||
-- | Name and group a list of tests. Combines easytest's "scope" and "tests".
|
||||
tests :: T.Text -> [E.Test ()] -> E.Test ()
|
||||
tests :: T.Text -> [E.Test ()] -> E.Test ()
|
||||
tests name = E.scope name . E.tests
|
||||
|
||||
-- | Skip the given list of tests, and any following tests in a monadic sequence,
|
||||
-- with the same type signature as "group".
|
||||
_tests :: T.Text -> [E.Test ()] -> E.Test ()
|
||||
_tests :: T.Text -> [E.Test ()] -> E.Test ()
|
||||
_tests _name = (E.skip >>) . E.tests
|
||||
|
||||
-- | Run some easytest tests, catching easytest's ExitCode exception,
|
||||
-- returning True if there was a problem.
|
||||
-- With arguments, runs only the scope (or single test) named by the first argument
|
||||
-- (exact, case sensitive).
|
||||
-- (exact, case sensitive).
|
||||
-- If there is a second argument, it should be an integer and will be used
|
||||
-- as the seed for randomness.
|
||||
-- as the seed for randomness.
|
||||
runEasytests :: [String] -> E.Test () -> IO Bool
|
||||
runEasytests args tests = (do
|
||||
case args of
|
||||
@ -96,7 +96,7 @@ runEasytests args tests = (do
|
||||
`catch` (\(_::ExitCode) -> return True)
|
||||
|
||||
-- | Like easytest's expectEq (asserts the second (actual) value equals the first (expected) value)
|
||||
-- but pretty-prints the values in the failure output.
|
||||
-- but pretty-prints the values in the failure output.
|
||||
expectEqPP :: (Eq a, Show a, HasCallStack) => a -> a -> E.Test ()
|
||||
expectEqPP expected actual = if expected == actual then E.ok else E.crash $
|
||||
"\nexpected:\n" <> T.pack (pshow expected) <> "\nbut got:\n" <> T.pack (pshow actual) <> "\n"
|
||||
@ -105,10 +105,10 @@ expectEqPP expected actual = if expected == actual then E.ok else E.crash $
|
||||
is :: (Eq a, Show a, HasCallStack) => a -> a -> Test ()
|
||||
is = flip expectEqPP
|
||||
|
||||
-- | Test that this stateful parser runnable in IO successfully parses
|
||||
-- all of the given input text, showing the parse error if it fails.
|
||||
-- | Test that this stateful parser runnable in IO successfully parses
|
||||
-- all of the given input text, showing the parse error if it fails.
|
||||
-- Suitable for hledger's JournalParser parsers.
|
||||
expectParse :: (Monoid st, Eq a, Show a, HasCallStack) =>
|
||||
expectParse :: (Monoid st, Eq a, Show a, HasCallStack) =>
|
||||
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> E.Test ()
|
||||
expectParse parser input = do
|
||||
ep <- E.io (runParserT (evalStateT (parser <* eof) mempty) "" input)
|
||||
@ -135,9 +135,9 @@ expectParseE parser input = do
|
||||
(const ok)
|
||||
ep
|
||||
|
||||
-- | Test that this stateful parser runnable in IO fails to parse
|
||||
-- the given input text, with a parse error containing the given string.
|
||||
expectParseError :: (Monoid st, Eq a, Show a, HasCallStack) =>
|
||||
-- | Test that this stateful parser runnable in IO fails to parse
|
||||
-- the given input text, with a parse error containing the given string.
|
||||
expectParseError :: (Monoid st, Eq a, Show a, HasCallStack) =>
|
||||
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> String -> E.Test ()
|
||||
expectParseError parser input errstr = do
|
||||
ep <- E.io (runParserT (evalStateT parser mempty) "" input)
|
||||
@ -173,8 +173,8 @@ expectParseErrorE parser input errstr = do
|
||||
else fail $ "\nparse error is not as expected:\n" ++ e' ++ "\n"
|
||||
|
||||
-- | Like expectParse, but also test the parse result is an expected value,
|
||||
-- pretty-printing both if it fails.
|
||||
expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) =>
|
||||
-- pretty-printing both if it fails.
|
||||
expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) =>
|
||||
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> E.Test ()
|
||||
expectParseEq parser input expected = expectParseEqOn parser input id expected
|
||||
|
||||
@ -186,9 +186,9 @@ expectParseEqE
|
||||
-> E.Test ()
|
||||
expectParseEqE parser input expected = expectParseEqOnE parser input id expected
|
||||
|
||||
-- | Like expectParseEq, but transform the parse result with the given function
|
||||
-- | Like expectParseEq, but transform the parse result with the given function
|
||||
-- before comparing it.
|
||||
expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) =>
|
||||
expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) =>
|
||||
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> E.Test ()
|
||||
expectParseEqOn parser input f expected = do
|
||||
ep <- E.io $ runParserT (evalStateT (parser <* eof) mempty) "" input
|
||||
|
@ -423,11 +423,11 @@ textWidth s = maximum $ map (T.foldr (\a b -> charWidth a + b) 0) $ T.lines s
|
||||
tests_Text = tests "Text" [
|
||||
tests "quoteIfSpaced" [
|
||||
quoteIfSpaced "a'a" `is` "a'a"
|
||||
,quoteIfSpaced "a\"a" `is` "a\"a"
|
||||
,quoteIfSpaced "a a" `is` "\"a a\""
|
||||
,quoteIfSpaced "mimi's cafe" `is` "\"mimi's cafe\""
|
||||
,quoteIfSpaced "\"alex\" cafe" `is` "\"\\\"alex\\\" cafe\""
|
||||
,quoteIfSpaced "le'shan's cafe" `is` "\"le'shan's cafe\""
|
||||
,quoteIfSpaced "\"be'any's\" cafe" `is` "\"\\\"be'any's\\\" cafe\""
|
||||
]
|
||||
,quoteIfSpaced "a\"a" `is` "a\"a"
|
||||
,quoteIfSpaced "a a" `is` "\"a a\""
|
||||
,quoteIfSpaced "mimi's cafe" `is` "\"mimi's cafe\""
|
||||
,quoteIfSpaced "\"alex\" cafe" `is` "\"\\\"alex\\\" cafe\""
|
||||
,quoteIfSpaced "le'shan's cafe" `is` "\"le'shan's cafe\""
|
||||
,quoteIfSpaced "\"be'any's\" cafe" `is` "\"\\\"be'any's\\\" cafe\""
|
||||
]
|
||||
]
|
||||
|
@ -16,7 +16,7 @@ do the right thing, so this file is a no-op and on its way to being removed.
|
||||
Not carefully tested.
|
||||
|
||||
-}
|
||||
-- TODO obsolete ?
|
||||
-- TODO obsolete ?
|
||||
|
||||
module Hledger.Utils.UTF8IOCompat (
|
||||
readFile,
|
||||
@ -119,5 +119,5 @@ userError' = userError . toSystemString
|
||||
|
||||
-- | A SystemString-aware version of error that adds a usage hint.
|
||||
usageError :: String -> a
|
||||
usageError = error' . (++ " (use -h to see usage)")
|
||||
usageError = error' . (++ " (use -h to see usage)")
|
||||
|
||||
|
@ -230,7 +230,7 @@ customErrorBundlePretty errBundle =
|
||||
-- (since only one custom error should be used at a time).
|
||||
findCustomError :: ParseError Text CustomErr -> Maybe CustomErr
|
||||
findCustomError err = case err of
|
||||
FancyError _ errSet ->
|
||||
FancyError _ errSet ->
|
||||
finds (\case {ErrorCustom e -> Just e; _ -> Nothing}) errSet
|
||||
_ -> Nothing
|
||||
|
||||
|
@ -98,7 +98,7 @@ renderHLine' pretty prop is sep h = [ cross pretty, sep ] ++ coreLine ++ [sep, c
|
||||
coreLine = concatMap helper $ flattenHeader $ zipHeader 0 is h
|
||||
helper = either vsep dashes
|
||||
dashes (i,_) = replicate i sep
|
||||
vsep NoLine = replicate 2 sep -- match the double space sep in renderColumns
|
||||
vsep NoLine = replicate 2 sep -- match the double space sep in renderColumns
|
||||
vsep SingleLine = sep : cross pretty : [sep]
|
||||
vsep DoubleLine = sep : cross' ++ [sep]
|
||||
cross' = case prop of
|
||||
|
@ -1,4 +1,4 @@
|
||||
{-
|
||||
{-
|
||||
Run doctests in Hledger source files under the current directory
|
||||
(./Hledger.hs, ./Hledger/**, ./Text/**) using the doctest runner.
|
||||
|
||||
@ -7,7 +7,7 @@ Arguments are case-insensitive file path substrings, to limit the files searched
|
||||
--slow reloads ghci between each test (https://github.com/sol/doctest#a-note-on-performance).
|
||||
|
||||
Eg, in hledger source dir:
|
||||
|
||||
|
||||
$ make ghci-doctest, :main [--verbose] [--slow] [CIFILEPATHSUBSTRINGS]
|
||||
|
||||
or:
|
||||
@ -40,20 +40,20 @@ main = do
|
||||
]
|
||||
|
||||
-- filter by patterns (case insensitive infix substring match)
|
||||
let
|
||||
let
|
||||
fs | null pats = sourcefiles
|
||||
| otherwise = [f | f <- sourcefiles, let f' = map toLower f, any (`isInfixOf` f') pats']
|
||||
where pats' = map (map toLower) pats
|
||||
fslen = length fs
|
||||
|
||||
|
||||
if (null fs)
|
||||
then do
|
||||
putStrLn $ "No file paths found matching: " ++ unwords pats
|
||||
|
||||
else do
|
||||
putStrLn $
|
||||
"Loading and searching for doctests in "
|
||||
++ show fslen
|
||||
putStrLn $
|
||||
"Loading and searching for doctests in "
|
||||
++ show fslen
|
||||
++ if fslen > 1 then " files, plus any files they import:" else " file, plus any files it imports:"
|
||||
when verbose $ putStrLn $ unwords fs
|
||||
|
||||
|
@ -71,7 +71,7 @@ asInit d reset ui@UIState{
|
||||
selidx = case (reset, listSelectedElement $ _asList s) of
|
||||
(True, _) -> 0
|
||||
(_, Nothing) -> 0
|
||||
(_, Just (_,AccountsScreenItem{asItemAccountName=a})) ->
|
||||
(_, Just (_,AccountsScreenItem{asItemAccountName=a})) ->
|
||||
headDef 0 $ catMaybes [
|
||||
findIndex (a ==) as
|
||||
,findIndex (a `isAccountNamePrefixOf`) as
|
||||
@ -88,7 +88,7 @@ asInit d reset ui@UIState{
|
||||
pfq | presentorfuture_ uopts == PFFuture = Any
|
||||
| otherwise = Date $ DateSpan Nothing (Just $ addDays 1 d)
|
||||
q = And [queryFromOpts d ropts, pfq]
|
||||
|
||||
|
||||
|
||||
-- run the report
|
||||
(items,_total) = report ropts' q j
|
||||
@ -104,14 +104,14 @@ asInit d reset ui@UIState{
|
||||
displayitem (fullacct, shortacct, indent, bal) =
|
||||
AccountsScreenItem{asItemIndentLevel = indent
|
||||
,asItemAccountName = fullacct
|
||||
,asItemDisplayAccountName = replaceHiddenAccountsNameWith "All" $ if tree_ ropts' then shortacct else fullacct
|
||||
,asItemDisplayAccountName = replaceHiddenAccountsNameWith "All" $ if tree_ ropts' then shortacct else fullacct
|
||||
,asItemRenderedAmounts = map showAmountWithoutPrice amts -- like showMixedAmountOneLineWithoutPrice
|
||||
}
|
||||
where
|
||||
Mixed amts = normaliseMixedAmountSquashPricesForDisplay $ stripPrices bal
|
||||
stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=Nothing}
|
||||
displayitems = map displayitem items
|
||||
-- blanks added for scrolling control, cf RegisterScreen
|
||||
-- blanks added for scrolling control, cf RegisterScreen
|
||||
blankitems = replicate 100
|
||||
AccountsScreenItem{asItemIndentLevel = 0
|
||||
,asItemAccountName = ""
|
||||
@ -201,7 +201,7 @@ asDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}
|
||||
curidx = case _asList s ^. listSelectedL of
|
||||
Nothing -> "-"
|
||||
Just i -> show (i + 1)
|
||||
totidx = show $ V.length nonblanks
|
||||
totidx = show $ V.length nonblanks
|
||||
where
|
||||
nonblanks = V.takeWhile (not . T.null . asItemAccountName) $ s ^. asList . listElementsL
|
||||
|
||||
@ -215,7 +215,7 @@ asDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}
|
||||
,("-+", str "depth")
|
||||
,("T", renderToggle (tree_ ropts) "flat" "tree")
|
||||
,("H", renderToggle (not ishistorical) "end-bals" "changes")
|
||||
,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future")
|
||||
,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future")
|
||||
--,("/", "filter")
|
||||
--,("DEL", "unfilter")
|
||||
--,("ESC", "cancel/top")
|
||||
@ -346,14 +346,14 @@ asHandle ui0@UIState{
|
||||
VtyEvent (EvKey (KChar 'l') [MCtrl]) -> scrollSelectionToMiddle _asList >> redraw ui
|
||||
VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui
|
||||
|
||||
-- enter register screen for selected account (if there is one),
|
||||
-- enter register screen for selected account (if there is one),
|
||||
-- centering its selected transaction if possible
|
||||
VtyEvent e | e `elem` moveRightEvents
|
||||
VtyEvent e | e `elem` moveRightEvents
|
||||
, not $ isBlankElement $ listSelectedElement _asList->
|
||||
-- TODO center selection after entering register screen; neither of these works till second time entering; easy strictifications didn't help
|
||||
rsCenterAndContinue $
|
||||
-- TODO center selection after entering register screen; neither of these works till second time entering; easy strictifications didn't help
|
||||
rsCenterAndContinue $
|
||||
-- flip rsHandle (VtyEvent (EvKey (KChar 'l') [MCtrl])) $
|
||||
screenEnter d regscr ui
|
||||
screenEnter d regscr ui
|
||||
where
|
||||
regscr = rsSetAccount selacct isdepthclipped registerScreen
|
||||
isdepthclipped = case getDepth ui of
|
||||
@ -363,9 +363,9 @@ asHandle ui0@UIState{
|
||||
-- prevent moving down over blank padding items;
|
||||
-- instead scroll down by one, until maximally scrolled - shows the end has been reached
|
||||
VtyEvent (EvKey (KDown) []) | isBlankElement mnextelement -> do
|
||||
vScrollBy (viewportScroll $ _asList^.listNameL) 1
|
||||
vScrollBy (viewportScroll $ _asList^.listNameL) 1
|
||||
continue ui
|
||||
where
|
||||
where
|
||||
mnextelement = listSelectedElement $ listMoveDown _asList
|
||||
|
||||
-- if page down or end leads to a blank padding item, stop at last non-blank
|
||||
@ -378,7 +378,7 @@ asHandle ui0@UIState{
|
||||
continue ui{aScreen=scr{_asList=list'}}
|
||||
else
|
||||
continue ui{aScreen=scr{_asList=list}}
|
||||
|
||||
|
||||
-- fall through to the list's event handler (handles up/down)
|
||||
VtyEvent ev -> do
|
||||
newitems <- handleListEvent (normaliseMovementKeys ev) _asList
|
||||
@ -398,7 +398,7 @@ asHandle _ _ = error "event handler called with wrong screen type, should not ha
|
||||
asSetSelectedAccount a s@AccountsScreen{} = s & asSelectedAccount .~ a
|
||||
asSetSelectedAccount _ s = s
|
||||
|
||||
isBlankElement mel = ((asItemAccountName . snd) <$> mel) == Just ""
|
||||
isBlankElement mel = ((asItemAccountName . snd) <$> mel) == Just ""
|
||||
|
||||
asCenterAndContinue ui = do
|
||||
scrollSelectionToMiddle $ _asList $ aScreen ui
|
||||
|
@ -163,7 +163,7 @@ runBrickUi uopts@UIOpts{cliopts_=copts@CliOpts{inputopts_=_iopts,reportopts_=rop
|
||||
}
|
||||
|
||||
-- print (length (show ui)) >> exitSuccess -- show any debug output to this point & quit
|
||||
|
||||
|
||||
if not (watch_ uopts')
|
||||
then
|
||||
void $ defaultMain brickapp ui
|
||||
|
@ -112,10 +112,10 @@ rsInit d reset ui@UIState{aopts=uopts@UIOpts{cliopts_=CliOpts{reportopts_=ropts}
|
||||
-- otherwise, the previously selected transaction if possible;
|
||||
-- otherwise, the transaction nearest in date to it;
|
||||
-- or if there's several with the same date, the nearest in journal order;
|
||||
-- otherwise, the last (latest) transaction.
|
||||
-- otherwise, the last (latest) transaction.
|
||||
newitems' = listMoveTo newselidx newitems
|
||||
where
|
||||
newselidx =
|
||||
newselidx =
|
||||
case (reset, listSelectedElement rsList) of
|
||||
(True, _) -> endidx
|
||||
(_, Nothing) -> endidx
|
||||
@ -164,7 +164,7 @@ rsDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}
|
||||
changewidthproportion = fromIntegral maxchangewidthseen / fromIntegral (maxchangewidthseen + maxbalwidthseen)
|
||||
maxchangewidth = round $ changewidthproportion * fromIntegral maxamtswidth
|
||||
maxbalwidth = maxamtswidth - maxchangewidth
|
||||
changewidth = min maxchangewidth maxchangewidthseen
|
||||
changewidth = min maxchangewidth maxchangewidthseen
|
||||
balwidth = min maxbalwidth maxbalwidthseen
|
||||
-- assign the remaining space to the description and accounts columns
|
||||
-- maxdescacctswidth = totalwidth - (whitespacewidth - 4) - changewidth - balwidth
|
||||
@ -177,7 +177,7 @@ rsDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}
|
||||
-- descwidthproportion = (descwidth' + acctswidth') / descwidth'
|
||||
-- maxdescwidth = min (maxdescacctswidth - 7) (maxdescacctswidth / descwidthproportion)
|
||||
-- maxacctswidth = maxdescacctswidth - maxdescwidth
|
||||
-- descwidth = min maxdescwidth descwidth'
|
||||
-- descwidth = min maxdescwidth descwidth'
|
||||
-- acctswidth = min maxacctswidth acctswidth'
|
||||
-- allocating equally.
|
||||
descwidth = maxdescacctswidth `div` 2
|
||||
@ -232,7 +232,7 @@ rsDraw UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}
|
||||
-- ,("RIGHT", str "transaction")
|
||||
,("T", renderToggle (tree_ ropts) "flat(-subs)" "tree(+subs)") -- rsForceInclusive may override, but use tree_ to ensure a visible toggle effect
|
||||
,("H", renderToggle (not ishistorical) "historical" "period")
|
||||
,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future")
|
||||
,("F", renderToggle (presentorfuture_ uopts == PFFuture) "present" "future")
|
||||
-- ,("a", "add")
|
||||
-- ,("g", "reload")
|
||||
-- ,("q", "quit")
|
||||
@ -271,11 +271,11 @@ rsHandle ui@UIState{
|
||||
,aMode=mode
|
||||
} ev = do
|
||||
d <- liftIO getCurrentDay
|
||||
let
|
||||
let
|
||||
journalspan = journalDateSpan False j
|
||||
nonblanks = V.takeWhile (not . null . rsItemDate) $ rsList^.listElementsL
|
||||
lastnonblankidx = max 0 (length nonblanks - 1)
|
||||
|
||||
|
||||
case mode of
|
||||
Minibuffer ed ->
|
||||
case ev of
|
||||
@ -358,9 +358,9 @@ rsHandle ui@UIState{
|
||||
-- prevent moving down over blank padding items;
|
||||
-- instead scroll down by one, until maximally scrolled - shows the end has been reached
|
||||
VtyEvent e | e `elem` moveDownEvents, isBlankElement mnextelement -> do
|
||||
vScrollBy (viewportScroll $ rsList^.listNameL) 1
|
||||
vScrollBy (viewportScroll $ rsList^.listNameL) 1
|
||||
continue ui
|
||||
where
|
||||
where
|
||||
mnextelement = listSelectedElement $ listMoveDown rsList
|
||||
|
||||
-- if page down or end leads to a blank padding item, stop at last non-blank
|
||||
@ -373,7 +373,7 @@ rsHandle ui@UIState{
|
||||
continue ui{aScreen=s{rsList=list'}}
|
||||
else
|
||||
continue ui{aScreen=s{rsList=list}}
|
||||
|
||||
|
||||
-- fall through to the list's event handler (handles other [pg]up/down events)
|
||||
VtyEvent ev -> do
|
||||
let ev' = normaliseMovementKeys ev
|
||||
@ -386,7 +386,7 @@ rsHandle ui@UIState{
|
||||
|
||||
rsHandle _ _ = error "event handler called with wrong screen type, should not happen"
|
||||
|
||||
isBlankElement mel = ((rsItemDate . snd) <$> mel) == Just ""
|
||||
isBlankElement mel = ((rsItemDate . snd) <$> mel) == Just ""
|
||||
|
||||
rsCenterAndContinue ui = do
|
||||
scrollSelectionToMiddle $ rsList $ aScreen ui
|
||||
|
@ -73,7 +73,7 @@ themesList = [
|
||||
,("border" <> "bold" , currentAttr & bold)
|
||||
,("border" <> "depth" , active)
|
||||
,("border" <> "filename" , currentAttr)
|
||||
,("border" <> "key" , active)
|
||||
,("border" <> "key" , active)
|
||||
,("border" <> "minibuffer" , white `on` black & bold)
|
||||
,("border" <> "query" , active)
|
||||
,("border" <> "selected" , active)
|
||||
|
@ -136,7 +136,7 @@ tsHandle ui@UIState{aScreen=s@TransactionScreen{tsTransaction=(i,t)
|
||||
where
|
||||
(pos,f) = case tsourcepos t of
|
||||
GenericSourcePos f l c -> (Just (l, Just c),f)
|
||||
JournalSourcePos f (l1,_) -> (Just (l1, Nothing),f)
|
||||
JournalSourcePos f (l1,_) -> (Just (l1, Nothing),f)
|
||||
AppEvent (DateChange old _) | isStandardPeriod p && p `periodContainsDate` old ->
|
||||
continue $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui
|
||||
where
|
||||
|
@ -85,7 +85,7 @@ rawOptsToUIOpts rawopts = checkUIOpts <$> do
|
||||
,cliopts_ = cliopts
|
||||
}
|
||||
|
||||
-- | Should transactions dated later than today be included ?
|
||||
-- | Should transactions dated later than today be included ?
|
||||
-- Like flat/tree mode, there are three states, and the meaning of default can vary by command.
|
||||
data PresentOrFutureOpt = PFDefault | PFPresent | PFFuture deriving (Eq, Show, Data, Typeable)
|
||||
instance Default PresentOrFutureOpt where def = PFDefault
|
||||
@ -109,7 +109,7 @@ getHledgerUIOpts :: IO UIOpts
|
||||
--getHledgerUIOpts = processArgs uimode >>= return . decodeRawOpts >>= rawOptsToUIOpts
|
||||
getHledgerUIOpts = do
|
||||
args <- getArgs >>= expandArgsAt
|
||||
let args' = replaceNumericFlags args
|
||||
let args' = replaceNumericFlags args
|
||||
let cmdargopts = either usageError id $ process uimode args'
|
||||
rawOptsToUIOpts $ decodeRawOpts cmdargopts
|
||||
rawOptsToUIOpts $ decodeRawOpts cmdargopts
|
||||
|
||||
|
@ -35,15 +35,15 @@ toggleCleared :: UIState -> UIState
|
||||
toggleCleared ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=reportOptsToggleStatusSomehow Cleared copts ropts}}}
|
||||
|
||||
-- TODO testing different status toggle styles
|
||||
-- TODO testing different status toggle styles
|
||||
|
||||
-- | Generate zero or more indicators of the status filters currently active,
|
||||
-- | Generate zero or more indicators of the status filters currently active,
|
||||
-- which will be shown comma-separated as part of the indicators list.
|
||||
uiShowStatus :: CliOpts -> [Status] -> [String]
|
||||
uiShowStatus copts ss =
|
||||
case style of
|
||||
-- in style 2, instead of "Y, Z" show "not X"
|
||||
Just 2 | length ss == numstatuses-1
|
||||
-- in style 2, instead of "Y, Z" show "not X"
|
||||
Just 2 | length ss == numstatuses-1
|
||||
-> map (("not "++). showstatus) $ sort $ complement ss -- should be just one
|
||||
_ -> map showstatus $ sort ss
|
||||
where
|
||||
@ -55,7 +55,7 @@ uiShowStatus copts ss =
|
||||
|
||||
reportOptsToggleStatusSomehow :: Status -> CliOpts -> ReportOpts -> ReportOpts
|
||||
reportOptsToggleStatusSomehow s copts ropts =
|
||||
case maybeintopt "status-toggles" $ rawopts_ copts of
|
||||
case maybeintopt "status-toggles" $ rawopts_ copts of
|
||||
Just 2 -> reportOptsToggleStatus2 s ropts
|
||||
Just 3 -> reportOptsToggleStatus3 s ropts
|
||||
-- Just 4 -> reportOptsToggleStatus4 s ropts
|
||||
@ -78,7 +78,7 @@ reportOptsToggleStatus1 s ropts@ReportOpts{statuses_=ss}
|
||||
reportOptsToggleStatus2 s ropts@ReportOpts{statuses_=ss}
|
||||
| ss == [s] = ropts{statuses_=complement [s]}
|
||||
| ss == complement [s] = ropts{statuses_=[]}
|
||||
| otherwise = ropts{statuses_=[s]} -- XXX assume only three values
|
||||
| otherwise = ropts{statuses_=[s]} -- XXX assume only three values
|
||||
|
||||
-- 3 UPC toggles each X
|
||||
reportOptsToggleStatus3 s ropts@ReportOpts{statuses_=ss}
|
||||
|
@ -59,7 +59,7 @@ suspendSignal = return ()
|
||||
#else
|
||||
import System.Posix.Signals
|
||||
suspendSignal :: IO ()
|
||||
suspendSignal = raiseSignal sigSTOP
|
||||
suspendSignal = raiseSignal sigSTOP
|
||||
#endif
|
||||
|
||||
-- | On posix platforms, suspend the program using the STOP signal,
|
||||
@ -121,7 +121,7 @@ helpDialog _copts =
|
||||
vBox [
|
||||
withAttr ("help" <> "heading") $ str "Filtering"
|
||||
,renderKey ("/ ", "set a filter query")
|
||||
,renderKey ("UPC ", "show unmarked/pending/cleared")
|
||||
,renderKey ("UPC ", "show unmarked/pending/cleared")
|
||||
,renderKey ("F ", "show future/present txns")
|
||||
,renderKey ("R ", "show real/all postings")
|
||||
,renderKey ("Z ", "show nonzero/all amounts")
|
||||
@ -208,12 +208,12 @@ borderKeysStr' keydescs =
|
||||
-- sep = str " | "
|
||||
sep = str " "
|
||||
|
||||
-- | Render the two states of a toggle, highlighting the active one.
|
||||
-- | Render the two states of a toggle, highlighting the active one.
|
||||
renderToggle :: Bool -> String -> String -> Widget Name
|
||||
renderToggle isright l r =
|
||||
let bold = withAttr ("border" <> "selected") in
|
||||
if isright
|
||||
then str (l++"/") <+> bold (str r)
|
||||
then str (l++"/") <+> bold (str r)
|
||||
else bold (str l) <+> str ("/"++r)
|
||||
|
||||
-- temporary shenanigans:
|
||||
@ -310,13 +310,13 @@ withBorderAttr attr = updateAttrMap (applyAttrMappings [("border", attr)])
|
||||
--scrollToTop :: List Name e -> EventM Name ()
|
||||
--scrollToTop list = do
|
||||
-- let vpname = list^.listNameL
|
||||
-- setTop (viewportScroll vpname) 0
|
||||
-- setTop (viewportScroll vpname) 0
|
||||
|
||||
-- | Scroll a list's viewport so that the selected item is centered in the
|
||||
-- middle of the display area.
|
||||
scrollSelectionToMiddle :: List Name e -> EventM Name ()
|
||||
scrollSelectionToMiddle list = do
|
||||
let mselectedrow = list^.listSelectedL
|
||||
let mselectedrow = list^.listSelectedL
|
||||
vpname = list^.listNameL
|
||||
mvp <- lookupViewport vpname
|
||||
case (mselectedrow, mvp) of
|
||||
@ -326,7 +326,7 @@ scrollSelectionToMiddle list = do
|
||||
vpheight = dbg4 "vpheight" $ vp^.vpSize._2
|
||||
itemsperpage = dbg4 "itemsperpage" $ vpheight `div` itemheight
|
||||
toprow = dbg4 "toprow" $ max 0 (selectedrow - (itemsperpage `div` 2)) -- assuming ViewportScroll's row offset is measured in list items not screen rows
|
||||
setTop (viewportScroll vpname) toprow
|
||||
setTop (viewportScroll vpname) toprow
|
||||
_ -> return ()
|
||||
|
||||
-- arrow keys vi keys emacs keys
|
||||
|
@ -17,7 +17,7 @@ import Yesod.Default.Config
|
||||
import Hledger.Data (Journal, nulljournal)
|
||||
|
||||
import Hledger.Web.Handler.AddR
|
||||
import Hledger.Web.Handler.MiscR
|
||||
import Hledger.Web.Handler.MiscR
|
||||
import Hledger.Web.Handler.EditR
|
||||
import Hledger.Web.Handler.UploadR
|
||||
import Hledger.Web.Handler.JournalR
|
||||
|
@ -55,7 +55,7 @@ postAddR = do
|
||||
|]
|
||||
|
||||
-- Add a single new transaction, send as JSON via PUT, to the journal.
|
||||
-- The web form handler above should probably use PUT as well.
|
||||
-- The web form handler above should probably use PUT as well.
|
||||
putAddR :: Handler RepJson
|
||||
putAddR = do
|
||||
VD{caps, j, opts} <- getViewData
|
||||
@ -66,4 +66,4 @@ putAddR = do
|
||||
Error err -> sendStatusJSON status400 ("could not parse json: " ++ err ::String)
|
||||
Success t -> do
|
||||
void $ liftIO $ journalAddTransaction j (cliopts_ opts) t
|
||||
sendResponseCreated TransactionsR
|
||||
sendResponseCreated TransactionsR
|
||||
|
@ -7,11 +7,11 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Hledger.Web.Handler.MiscR
|
||||
( getAccountnamesR
|
||||
, getTransactionsR
|
||||
, getPricesR
|
||||
, getCommoditiesR
|
||||
, getAccountsR
|
||||
( getAccountnamesR
|
||||
, getTransactionsR
|
||||
, getPricesR
|
||||
, getCommoditiesR
|
||||
, getAccountsR
|
||||
, getAccounttransactionsR
|
||||
, getDownloadR
|
||||
, getFaviconR
|
||||
|
@ -21,7 +21,7 @@
|
||||
--{-# LANGUAGE TypeFamilies #-}
|
||||
--{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module Hledger.Web.Json (
|
||||
module Hledger.Web.Json (
|
||||
-- * Instances
|
||||
-- * Utilities
|
||||
readJsonFile
|
||||
@ -66,11 +66,11 @@ instance ToJSON Posting where
|
||||
,"ptype" .= toJSON ptype
|
||||
,"ptags" .= toJSON ptags
|
||||
,"pbalanceassertion" .= toJSON pbalanceassertion
|
||||
-- To avoid a cycle, show just the parent transaction's index number
|
||||
-- To avoid a cycle, show just the parent transaction's index number
|
||||
-- in a dummy field. When re-parsed, there will be no parent.
|
||||
,"ptransaction_" .= toJSON (maybe "" (show.tindex) ptransaction)
|
||||
-- This is probably not wanted in json, we discard it.
|
||||
,"poriginal" .= toJSON (Nothing :: Maybe Posting)
|
||||
,"poriginal" .= toJSON (Nothing :: Maybe Posting)
|
||||
]
|
||||
|
||||
instance ToJSON Transaction
|
||||
@ -82,7 +82,7 @@ instance ToJSON Account where
|
||||
,"aibalance" .= toJSON (aibalance a)
|
||||
,"anumpostings" .= toJSON (anumpostings a)
|
||||
,"aboring" .= toJSON (aboring a)
|
||||
-- To avoid a cycle, show just the parent account's name
|
||||
-- To avoid a cycle, show just the parent account's name
|
||||
-- in a dummy field. When re-parsed, there will be no parent.
|
||||
,"aparent_" .= toJSON (maybe "" aname $ aparent a)
|
||||
-- Just the names of subaccounts, as a dummy field, ignored when parsed.
|
||||
@ -110,14 +110,14 @@ instance FromJSON Posting
|
||||
instance FromJSON Transaction
|
||||
instance FromJSON AccountDeclarationInfo
|
||||
-- XXX The ToJSON instance replaces subaccounts with just names.
|
||||
-- Here we should try to make use of those to reconstruct the
|
||||
-- Here we should try to make use of those to reconstruct the
|
||||
-- parent-child relationships.
|
||||
instance FromJSON Account
|
||||
|
||||
-- Decimal, various attempts
|
||||
--
|
||||
-- https://stackoverflow.com/questions/40331851/haskell-data-decimal-as-aeson-type
|
||||
----instance FromJSON Decimal where parseJSON =
|
||||
----instance FromJSON Decimal where parseJSON =
|
||||
---- A.withScientific "Decimal" (return . right . eitherFromRational . toRational)
|
||||
--
|
||||
-- https://github.com/bos/aeson/issues/474
|
||||
@ -156,7 +156,7 @@ instance FromJSON (DecimalRaw Integer)
|
||||
readJsonFile :: FromJSON a => FilePath -> IO a
|
||||
readJsonFile f = do
|
||||
bs <- BL.readFile f
|
||||
let v = fromMaybe (error "could not decode bytestring as json value") (decode bs :: Maybe Value)
|
||||
let v = fromMaybe (error "could not decode bytestring as json value") (decode bs :: Maybe Value)
|
||||
case fromJSON v :: FromJSON a => Result a of
|
||||
Error e -> error e
|
||||
Success t -> return t
|
||||
|
@ -282,7 +282,7 @@ type CommandDoc = String
|
||||
-- from a help template and flag/argument specifications.
|
||||
-- Reduces boilerplate a little, though the complicated cmdargs
|
||||
-- flag and argument specs are still required.
|
||||
hledgerCommandMode :: CommandDoc -> [Flag RawOpts] -> [(String, [Flag RawOpts])]
|
||||
hledgerCommandMode :: CommandDoc -> [Flag RawOpts] -> [(String, [Flag RawOpts])]
|
||||
-> [Flag RawOpts] -> ([Arg RawOpts], Maybe (Arg RawOpts)) -> Mode RawOpts
|
||||
hledgerCommandMode doc unnamedflaggroup namedflaggroups hiddenflaggroup argsdescr =
|
||||
case parseCommandDoc doc of
|
||||
@ -404,7 +404,7 @@ defaultWidth :: Int
|
||||
defaultWidth = 80
|
||||
|
||||
-- | Replace any numeric flags (eg -2) with their long form (--depth 2),
|
||||
-- as I'm guessing cmdargs doesn't support this directly.
|
||||
-- as I'm guessing cmdargs doesn't support this directly.
|
||||
replaceNumericFlags :: [String] -> [String]
|
||||
replaceNumericFlags = map replace
|
||||
where
|
||||
@ -452,10 +452,10 @@ checkCliOpts opts =
|
||||
Right _ -> Right ()
|
||||
-- XXX check registerWidthsFromOpts opts
|
||||
|
||||
-- | A helper for addon commands: this parses options and arguments from
|
||||
-- the current command line using the given hledger-style cmdargs mode,
|
||||
-- and returns a CliOpts. Or, with --help or -h present, it prints
|
||||
-- long or short help, and exits the program.
|
||||
-- | A helper for addon commands: this parses options and arguments from
|
||||
-- the current command line using the given hledger-style cmdargs mode,
|
||||
-- and returns a CliOpts. Or, with --help or -h present, it prints
|
||||
-- long or short help, and exits the program.
|
||||
-- When --debug is present, also prints some debug output.
|
||||
-- Note this is not used by the main hledger executable.
|
||||
--
|
||||
@ -472,7 +472,7 @@ checkCliOpts opts =
|
||||
-- hledger options not displayed.
|
||||
--
|
||||
-- Tips:
|
||||
-- Empty lines in the pre/postamble are removed by cmdargs;
|
||||
-- Empty lines in the pre/postamble are removed by cmdargs;
|
||||
-- add a space character to preserve them.
|
||||
--
|
||||
getHledgerCliOpts :: Mode RawOpts -> IO CliOpts
|
||||
@ -640,7 +640,7 @@ defaultBalanceLineFormat = BottomAligned [
|
||||
-- or more versions (or two versions that don't look like a
|
||||
-- source/compiled pair), they are all included, with file extensions
|
||||
-- intact.
|
||||
--
|
||||
--
|
||||
hledgerAddons :: IO [String]
|
||||
hledgerAddons = do
|
||||
-- past bug generator
|
||||
@ -658,10 +658,10 @@ dropRedundantSourceVersion [f,g]
|
||||
| takeExtension g `elem` compiledExts = [g]
|
||||
dropRedundantSourceVersion fs = fs
|
||||
|
||||
compiledExts = ["",".com",".exe"]
|
||||
compiledExts = ["",".com",".exe"]
|
||||
|
||||
|
||||
-- | Get all sorted unique filenames in the current user's PATH.
|
||||
-- | Get all sorted unique filenames in the current user's PATH.
|
||||
-- We do not currently filter out non-file objects or files without execute permission.
|
||||
likelyExecutablesInPath :: IO [String]
|
||||
likelyExecutablesInPath = do
|
||||
@ -677,8 +677,8 @@ likelyExecutablesInPath = do
|
||||
|
||||
-- | Get the sorted unique filenames of all hledger-* executables in
|
||||
-- the current user's PATH. These are files in any of the PATH directories,
|
||||
-- named hledger-*, with either no extension (and no periods in the name)
|
||||
-- or one of the addonExtensions.
|
||||
-- named hledger-*, with either no extension (and no periods in the name)
|
||||
-- or one of the addonExtensions.
|
||||
-- We do not currently filter out non-file objects or files without execute permission.
|
||||
hledgerExecutablesInPath :: IO [String]
|
||||
hledgerExecutablesInPath = filter isHledgerExeName <$> likelyExecutablesInPath
|
||||
|
@ -66,24 +66,24 @@ accounts CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do
|
||||
accts = dbg5 "accts to show" $ -- no need to nub/sort, accountTree will
|
||||
if | declared && not used -> matcheddeclaredaccts
|
||||
| not declared && used -> matchedusedaccts
|
||||
| otherwise -> matcheddeclaredaccts ++ matchedusedaccts
|
||||
| otherwise -> matcheddeclaredaccts ++ matchedusedaccts
|
||||
|
||||
-- 2. sort them by declaration order and name, at each level of their tree structure
|
||||
sortedaccts = sortAccountNamesByDeclaration j tree accts
|
||||
|
||||
-- 3. if there's a depth limit, depth-clip and remove any no longer useful items
|
||||
-- 3. if there's a depth limit, depth-clip and remove any no longer useful items
|
||||
clippedaccts =
|
||||
dbg1 "clippedaccts" $
|
||||
filter (matchesAccount acctq) $ -- clipping can leave accounts that no longer match the query, remove such
|
||||
nub $ -- clipping can leave duplicates (adjacent, hopefully)
|
||||
filter (not . T.null) $ -- depth:0 can leave nulls
|
||||
map (clipAccountName depth) $ -- clip at depth if specified
|
||||
sortedaccts
|
||||
map (clipAccountName depth) $ -- clip at depth if specified
|
||||
sortedaccts
|
||||
|
||||
-- 4. print what remains as a list or tree, maybe applying --drop in the former case
|
||||
-- 4. print what remains as a list or tree, maybe applying --drop in the former case
|
||||
mapM_ (T.putStrLn . render) clippedaccts
|
||||
where
|
||||
render a
|
||||
render a
|
||||
| tree_ ropts = T.replicate (2 * (accountNameLevel a - 1)) " " <> accountLeafName a
|
||||
| otherwise = accountNameDrop (drop_ ropts) a
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{-|
|
||||
|
||||
Print a bar chart of posting activity per day, or other report interval.
|
||||
Print a bar chart of posting activity per day, or other report interval.
|
||||
|
||||
-}
|
||||
|
||||
|
@ -320,7 +320,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts@ReportOpts{..}} j = do
|
||||
"html" -> const $ error' "Sorry, HTML output is not yet implemented for this kind of report." -- TODO
|
||||
_ -> budgetReportAsText ropts
|
||||
writeOutput opts $ render budgetreport
|
||||
|
||||
|
||||
else
|
||||
if multiperiod then do -- multi period balance report
|
||||
let report = multiBalanceReport ropts (queryFromOpts d ropts) j
|
||||
@ -337,7 +337,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts@ReportOpts{..}} j = do
|
||||
| otherwise = ropts{accountlistmode_=ALTree}
|
||||
in balanceReportFromMultiBalanceReport ropts' (queryFromOpts d ropts) j
|
||||
-- for historical balances we must use balanceReportFromMultiBalanceReport (also forces --no-elide)
|
||||
| otherwise = balanceReport ropts (queryFromOpts d ropts) j -- simple Ledger-style balance report
|
||||
| otherwise = balanceReport ropts (queryFromOpts d ropts) j -- simple Ledger-style balance report
|
||||
render = case format of
|
||||
"csv" -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r
|
||||
"html" -> \_ _ -> error' "Sorry, HTML output is not yet implemented for this kind of report." -- TODO
|
||||
@ -458,7 +458,7 @@ renderComponent1 opts (acctname, depth, total) (FormatField ljust min max field)
|
||||
-- and will include the final totals row unless --no-total is set.
|
||||
multiBalanceReportAsCsv :: ReportOpts -> MultiBalanceReport -> CSV
|
||||
multiBalanceReportAsCsv opts@ReportOpts{average_, row_total_} (MultiBalanceReport (colspans, items, (coltotals,tot,avg))) =
|
||||
maybetranspose $
|
||||
maybetranspose $
|
||||
("Account" : map showDateSpan colspans
|
||||
++ ["Total" | row_total_]
|
||||
++ ["Average" | average_]
|
||||
@ -481,7 +481,7 @@ multiBalanceReportAsCsv opts@ReportOpts{average_, row_total_} (MultiBalanceRepor
|
||||
where
|
||||
maybetranspose | transpose_ opts = transpose
|
||||
| otherwise = id
|
||||
|
||||
|
||||
-- | Render a multi-column balance report as HTML.
|
||||
multiBalanceReportAsHtml :: ReportOpts -> MultiBalanceReport -> Html ()
|
||||
multiBalanceReportAsHtml ropts mbr =
|
||||
@ -505,7 +505,7 @@ multiBalanceReportHtmlRows ropts mbr =
|
||||
in
|
||||
(multiBalanceReportHtmlHeadRow ropts headingsrow
|
||||
,map (multiBalanceReportHtmlBodyRow ropts) bodyrows
|
||||
,multiBalanceReportHtmlFootRow ropts <$> mtotalsrow -- TODO pad totals row with zeros when there are
|
||||
,multiBalanceReportHtmlFootRow ropts <$> mtotalsrow -- TODO pad totals row with zeros when there are
|
||||
)
|
||||
|
||||
-- | Render one MultiBalanceReport heading row as a HTML table row.
|
||||
@ -548,8 +548,8 @@ multiBalanceReportHtmlBodyRow ropts (label:rest) =
|
||||
multiBalanceReportHtmlFootRow :: ReportOpts -> [String] -> Html ()
|
||||
multiBalanceReportHtmlFootRow _ropts [] = mempty
|
||||
-- TODO pad totals row with zeros when subreport is empty
|
||||
-- multiBalanceReportHtmlFootRow ropts $
|
||||
-- ""
|
||||
-- multiBalanceReportHtmlFootRow ropts $
|
||||
-- ""
|
||||
-- : repeat nullmixedamt zeros
|
||||
-- ++ (if row_total_ ropts then [nullmixedamt] else [])
|
||||
-- ++ (if average_ ropts then [nullmixedamt] else [])
|
||||
@ -597,7 +597,7 @@ multiBalanceReportAsText ropts@ReportOpts{..} r =
|
||||
balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount
|
||||
balanceReportAsTable opts@ReportOpts{average_, row_total_, balancetype_} (MultiBalanceReport (colspans, items, (coltotals,tot,avg))) =
|
||||
maybetranspose $
|
||||
addtotalrow $
|
||||
addtotalrow $
|
||||
Table
|
||||
(T.Group NoLine $ map Header accts)
|
||||
(T.Group NoLine $ map Header colheadings)
|
||||
@ -625,7 +625,7 @@ balanceReportAsTable opts@ReportOpts{average_, row_total_, balancetype_} (MultiB
|
||||
))
|
||||
maybetranspose | transpose_ opts = \(Table rh ch vals) -> Table ch rh (transpose vals)
|
||||
| otherwise = id
|
||||
|
||||
|
||||
-- | Given a table representing a multi-column balance report (for example,
|
||||
-- made using 'balanceReportAsTable'), render it in a format suitable for
|
||||
-- console output.
|
||||
|
@ -3,7 +3,7 @@
|
||||
module Hledger.Cli.Commands.Checkdupes (
|
||||
checkdupesmode
|
||||
,checkdupes
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Function
|
||||
|
@ -4,7 +4,7 @@
|
||||
module Hledger.Cli.Commands.Close (
|
||||
closemode
|
||||
,close
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (when)
|
||||
@ -29,8 +29,8 @@ closemode = hledgerCommandMode
|
||||
|
||||
close CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do
|
||||
today <- getCurrentDay
|
||||
let
|
||||
(opening, closing) =
|
||||
let
|
||||
(opening, closing) =
|
||||
case (boolopt "opening" rawopts, boolopt "closing" rawopts) of
|
||||
(False, False) -> (True, True) -- by default show both opening and closing
|
||||
(o, c) -> (o, c)
|
||||
|
@ -34,7 +34,7 @@ files :: CliOpts -> Journal -> IO ()
|
||||
files CliOpts{rawopts_=rawopts} j = do
|
||||
let args = listofstringopt "args" rawopts
|
||||
regex = headMay args
|
||||
files = maybe id (filter . regexMatches) regex
|
||||
$ map fst
|
||||
files = maybe id (filter . regexMatches) regex
|
||||
$ map fst
|
||||
$ jfiles j
|
||||
mapM_ putStrLn files
|
||||
|
@ -46,10 +46,10 @@ helpmode = hledgerCommandMode
|
||||
[]
|
||||
([], Just $ argsFlag "[MANUAL]")
|
||||
|
||||
-- | List or display one of the hledger manuals in various formats.
|
||||
-- | List or display one of the hledger manuals in various formats.
|
||||
-- You can select a docs viewer with one of the `--info`, `--man`, `--pager`, `--cat` flags.
|
||||
-- Otherwise it will use the first available of: info, man, $PAGER, less, stdout
|
||||
-- (and always stdout if output is non-interactive).
|
||||
-- (and always stdout if output is non-interactive).
|
||||
help' :: CliOpts -> Journal -> IO ()
|
||||
help' opts _ = do
|
||||
exes <- likelyExecutablesInPath
|
||||
@ -60,18 +60,18 @@ help' opts _ = do
|
||||
topic = case args of
|
||||
[pat] -> headMay [t | t <- docTopics, map toLower pat `isInfixOf` t]
|
||||
_ -> Nothing
|
||||
[info, man, pager, cat] =
|
||||
[info, man, pager, cat] =
|
||||
[runInfoForTopic, runManForTopic, runPagerForTopic pagerprog, printHelpForTopic]
|
||||
viewer
|
||||
| boolopt "info" $ rawopts_ opts = info
|
||||
| boolopt "man" $ rawopts_ opts = man
|
||||
| boolopt "pager" $ rawopts_ opts = pager
|
||||
| boolopt "cat" $ rawopts_ opts = cat
|
||||
| not interactive = cat
|
||||
| not interactive = cat
|
||||
| "info" `elem` exes = info
|
||||
| "man" `elem` exes = man
|
||||
| pagerprog `elem` exes = pager
|
||||
| otherwise = cat
|
||||
| otherwise = cat
|
||||
case topic of
|
||||
Nothing -> putStrLn $ unlines [
|
||||
"Please choose a manual by typing \"hledger help MANUAL\" (any substring is ok)."
|
||||
|
@ -4,7 +4,7 @@
|
||||
module Hledger.Cli.Commands.Import (
|
||||
importmode
|
||||
,importcmd
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad
|
||||
@ -18,7 +18,7 @@ import Text.Printf
|
||||
|
||||
importmode = hledgerCommandMode
|
||||
$(embedFileRelative "Hledger/Cli/Commands/Import.txt")
|
||||
[flagNone ["dry-run"] (setboolopt "dry-run") "just show the transactions to be imported"]
|
||||
[flagNone ["dry-run"] (setboolopt "dry-run") "just show the transactions to be imported"]
|
||||
[generalflagsgroup1]
|
||||
hiddenflags
|
||||
([], Just $ argsFlag "FILE [...]")
|
||||
@ -33,7 +33,7 @@ importcmd opts@CliOpts{rawopts_=rawopts,inputopts_=iopts} j = do
|
||||
fs -> do
|
||||
enewj <- readJournalFiles iopts' fs
|
||||
case enewj of
|
||||
Left e -> error' e
|
||||
Left e -> error' e
|
||||
Right newj ->
|
||||
case sortOn tdate $ jtxns newj of
|
||||
[] -> return ()
|
||||
|
@ -3,7 +3,7 @@
|
||||
module Hledger.Cli.Commands.Prices (
|
||||
pricesmode
|
||||
,prices
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Maybe
|
||||
@ -22,7 +22,7 @@ pricesmode = hledgerCommandMode
|
||||
hiddenflags
|
||||
([], Just $ argsFlag "[QUERY]")
|
||||
|
||||
-- XXX the original hledger-prices script always ignored assertions
|
||||
-- XXX the original hledger-prices script always ignored assertions
|
||||
prices opts j = do
|
||||
d <- getCurrentDay
|
||||
let
|
||||
|
@ -59,13 +59,13 @@ printEntries opts@CliOpts{reportopts_=ropts} j = do
|
||||
writeOutput opts $ render $ entriesReport ropts' q j
|
||||
|
||||
entriesReportAsText :: CliOpts -> EntriesReport -> String
|
||||
entriesReportAsText opts = concatMap (showTransactionUnelided . gettxn)
|
||||
entriesReportAsText opts = concatMap (showTransactionUnelided . gettxn)
|
||||
where
|
||||
gettxn | useexplicittxn = id -- use fully inferred amounts & txn prices
|
||||
gettxn | useexplicittxn = id -- use fully inferred amounts & txn prices
|
||||
| otherwise = originalTransaction -- use original as-written amounts/txn prices
|
||||
-- Original vs inferred transactions/postings were causing problems here, disabling -B (#551).
|
||||
-- Use the explicit one if -B or -x are active.
|
||||
-- This passes tests; does it also mean -B sometimes shows missing amounts unnecessarily ?
|
||||
-- This passes tests; does it also mean -B sometimes shows missing amounts unnecessarily ?
|
||||
useexplicittxn = boolopt "explicit" (rawopts_ opts) || (valuationTypeIsCost $ reportopts_ opts)
|
||||
|
||||
-- Replace this transaction's postings with the original postings if any, but keep the
|
||||
|
@ -3,7 +3,7 @@
|
||||
module Hledger.Cli.Commands.Printunique (
|
||||
printuniquemode
|
||||
,printunique
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Data.List
|
||||
|
@ -4,7 +4,7 @@
|
||||
module Hledger.Cli.Commands.Registermatch (
|
||||
registermatchmode
|
||||
,registermatch
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Char (toUpper)
|
||||
|
@ -5,7 +5,7 @@
|
||||
module Hledger.Cli.Commands.Rewrite (
|
||||
rewritemode
|
||||
,rewrite
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
#if !(MIN_VERSION_base(4,11,0))
|
||||
@ -36,7 +36,7 @@ rewritemode = hledgerCommandMode
|
||||
-- TODO interpolating match groups in replacement
|
||||
-- TODO allow using this on unbalanced entries, eg to rewrite while editing
|
||||
|
||||
rewrite opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j@Journal{jtxns=ts} = do
|
||||
rewrite opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j@Journal{jtxns=ts} = do
|
||||
-- rewrite matched transactions
|
||||
let modifiers = transactionModifierFromOpts opts : jtxnmodifiers j
|
||||
let j' = j{jtxns=modifyTransactions modifiers ts}
|
||||
@ -46,7 +46,7 @@ rewrite opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j@Journal{jtxns=ts} = d
|
||||
-- | Build a 'TransactionModifier' from any query arguments and --add-posting flags
|
||||
-- provided on the command line, or throw a parse error.
|
||||
transactionModifierFromOpts :: CliOpts -> TransactionModifier
|
||||
transactionModifierFromOpts CliOpts{rawopts_=rawopts,reportopts_=ropts} =
|
||||
transactionModifierFromOpts CliOpts{rawopts_=rawopts,reportopts_=ropts} =
|
||||
TransactionModifier{tmquerytxt=q, tmpostingrules=ps}
|
||||
where
|
||||
q = T.pack $ query_ ropts
|
||||
|
@ -6,7 +6,7 @@ The @roi@ command prints internal rate of return and time-weighted rate of retur
|
||||
|
||||
-}
|
||||
|
||||
module Hledger.Cli.Commands.Roi (
|
||||
module Hledger.Cli.Commands.Roi (
|
||||
roimode
|
||||
, roi
|
||||
) where
|
||||
@ -40,40 +40,40 @@ roimode = hledgerCommandMode
|
||||
hiddenflags
|
||||
([], Just $ argsFlag "[QUERY]")
|
||||
|
||||
-- One reporting span,
|
||||
data OneSpan = OneSpan
|
||||
-- One reporting span,
|
||||
data OneSpan = OneSpan
|
||||
Day -- start date, inclusive
|
||||
Day -- end date, exclusive
|
||||
Quantity -- value of investment at the beginning of day on spanBegin_
|
||||
Quantity -- value of investment at the end of day on spanEnd_
|
||||
[(Day,Quantity)] -- all deposits and withdrawals (but not changes of value) in the DateSpan [spanBegin_,spanEnd_)
|
||||
deriving (Show)
|
||||
|
||||
|
||||
|
||||
roi :: CliOpts -> Journal -> IO ()
|
||||
roi CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do
|
||||
d <- getCurrentDay
|
||||
let
|
||||
let
|
||||
investmentsQuery = queryFromOpts d $ ropts{query_ = stringopt "investment" rawopts,period_=PeriodAll}
|
||||
pnlQuery = queryFromOpts d $ ropts{query_ = stringopt "pnl" rawopts,period_=PeriodAll}
|
||||
showCashFlow = boolopt "cashflow" rawopts
|
||||
prettyTables = pretty_tables_ ropts
|
||||
|
||||
|
||||
trans = dbg3 "investments" $ jtxns $ filterJournalTransactions investmentsQuery j
|
||||
|
||||
journalSpan =
|
||||
let dates = map transactionDate2 trans in
|
||||
DateSpan (Just $ minimum dates) (Just $ addDays 1 $ maximum dates)
|
||||
|
||||
|
||||
journalSpan =
|
||||
let dates = map transactionDate2 trans in
|
||||
DateSpan (Just $ minimum dates) (Just $ addDays 1 $ maximum dates)
|
||||
|
||||
requestedSpan = periodAsDateSpan $ period_ ropts
|
||||
requestedInterval = interval_ ropts
|
||||
|
||||
wholeSpan = spanDefaultsFrom requestedSpan journalSpan
|
||||
|
||||
wholeSpan = spanDefaultsFrom requestedSpan journalSpan
|
||||
|
||||
when (null trans) $ do
|
||||
putStrLn "No relevant transactions found. Check your investments query"
|
||||
exitFailure
|
||||
|
||||
|
||||
let spans = case requestedInterval of
|
||||
NoInterval -> [wholeSpan]
|
||||
interval ->
|
||||
@ -82,23 +82,23 @@ roi CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do
|
||||
|
||||
tableBody <- forM spans $ \(DateSpan (Just spanBegin) (Just spanEnd)) -> do
|
||||
-- Spans are [spanBegin,spanEnd), and spanEnd is 1 day after then actual end date we are interested in
|
||||
let
|
||||
let
|
||||
valueBefore =
|
||||
total trans (And [ investmentsQuery
|
||||
, Date (DateSpan Nothing (Just spanBegin))])
|
||||
|
||||
valueAfter =
|
||||
|
||||
valueAfter =
|
||||
total trans (And [investmentsQuery
|
||||
, Date (DateSpan Nothing (Just spanEnd))])
|
||||
|
||||
cashFlow =
|
||||
|
||||
cashFlow =
|
||||
calculateCashFlow trans (And [ Not investmentsQuery
|
||||
, Not pnlQuery
|
||||
, Date (DateSpan (Just spanBegin) (Just spanEnd)) ] )
|
||||
|
||||
thisSpan = dbg3 "processing span" $
|
||||
|
||||
thisSpan = dbg3 "processing span" $
|
||||
OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow
|
||||
|
||||
|
||||
irr <- internalRateOfReturn showCashFlow prettyTables thisSpan
|
||||
twr <- timeWeightedReturn showCashFlow prettyTables investmentsQuery trans thisSpan
|
||||
let cashFlowAmt = negate $ sum $ map snd cashFlow
|
||||
@ -112,28 +112,28 @@ roi CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do
|
||||
, printf "%0.2f%%" $ smallIsZero irr
|
||||
, printf "%0.2f%%" $ smallIsZero twr ]
|
||||
|
||||
let table = Table
|
||||
(Tbl.Group NoLine (map (Header . show) (take (length tableBody) [1..])))
|
||||
(Tbl.Group DoubleLine
|
||||
let table = Table
|
||||
(Tbl.Group NoLine (map (Header . show) (take (length tableBody) [1..])))
|
||||
(Tbl.Group DoubleLine
|
||||
[ Tbl.Group SingleLine [Header "Begin", Header "End"]
|
||||
, Tbl.Group SingleLine [Header "Value (begin)", Header "Cashflow", Header "Value (end)", Header "PnL"]
|
||||
, Tbl.Group SingleLine [Header "IRR", Header "TWR"]])
|
||||
tableBody
|
||||
|
||||
|
||||
putStrLn $ Ascii.render prettyTables id id id table
|
||||
|
||||
timeWeightedReturn showCashFlow prettyTables investmentsQuery trans (OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow) = do
|
||||
let initialUnitPrice = 100
|
||||
let initialUnits = valueBefore / initialUnitPrice
|
||||
let cashflow =
|
||||
let cashflow =
|
||||
-- Aggregate all entries for a single day, assuming that intraday interest is negligible
|
||||
map (\date_cash -> let (dates, cash) = unzip date_cash in (head dates, sum cash))
|
||||
$ groupBy ((==) `on` fst)
|
||||
$ sortOn fst
|
||||
$ map (\(d,a) -> (d, negate a))
|
||||
$ sortOn fst
|
||||
$ map (\(d,a) -> (d, negate a))
|
||||
$ filter ((/=0).snd) cashFlow
|
||||
|
||||
let units =
|
||||
|
||||
let units =
|
||||
tail $
|
||||
scanl
|
||||
(\(_, _, _, unitBalance) (date, amt) ->
|
||||
@ -146,14 +146,14 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans (OneSpan spa
|
||||
in (valueOnDate, unitsBoughtOrSold, unitPrice, unitBalance + unitsBoughtOrSold))
|
||||
(0, 0, 0, initialUnits)
|
||||
cashflow
|
||||
|
||||
|
||||
let finalUnitBalance = if null units then initialUnits else let (_,_,_,u) = last units in u
|
||||
finalUnitPrice = valueAfter / finalUnitBalance
|
||||
totalTWR = roundTo 2 $ (finalUnitPrice - initialUnitPrice)
|
||||
years = fromIntegral (diffDays spanEnd spanBegin) / 365 :: Double
|
||||
annualizedTWR = 100*((1+(realToFrac totalTWR/100))**(1/years)-1) :: Double
|
||||
|
||||
let s d = show $ roundTo 2 d
|
||||
|
||||
let s d = show $ roundTo 2 d
|
||||
when showCashFlow $ do
|
||||
printf "\nTWR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd))
|
||||
let (dates', amounts') = unzip cashflow
|
||||
@ -165,27 +165,27 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans (OneSpan spa
|
||||
unitPrices = add initialUnitPrice unitPrices'
|
||||
unitBalances = add initialUnits unitBalances'
|
||||
valuesOnDate = add 0 valuesOnDate'
|
||||
|
||||
putStr $ Ascii.render prettyTables id id id
|
||||
(Table
|
||||
|
||||
putStr $ Ascii.render prettyTables id id id
|
||||
(Table
|
||||
(Tbl.Group NoLine (map (Header . showDate) dates))
|
||||
(Tbl.Group DoubleLine [ Tbl.Group SingleLine [Header "Portfolio value", Header "Unit balance"]
|
||||
(Tbl.Group DoubleLine [ Tbl.Group SingleLine [Header "Portfolio value", Header "Unit balance"]
|
||||
, Tbl.Group SingleLine [Header "Cash", Header "Unit price", Header "Units"]
|
||||
, Tbl.Group SingleLine [Header "New Unit Balance"]])
|
||||
[ [value, oldBalance, amt, prc, udelta, balance]
|
||||
[ [value, oldBalance, amt, prc, udelta, balance]
|
||||
| value <- map s valuesOnDate
|
||||
| oldBalance <- map s (0:unitBalances)
|
||||
| balance <- map s unitBalances
|
||||
| amt <- map s amounts
|
||||
| prc <- map s unitPrices
|
||||
| udelta <- map s unitsBoughtOrSold ])
|
||||
|
||||
printf "Final unit price: %s/%s=%s U.\nTotal TWR: %s%%.\nPeriod: %.2f years.\nAnnualized TWR: %.2f%%\n\n" (s valueAfter) (s finalUnitBalance) (s finalUnitPrice) (s totalTWR) years annualizedTWR
|
||||
|
||||
return annualizedTWR
|
||||
|
||||
|
||||
internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow) = do
|
||||
printf "Final unit price: %s/%s=%s U.\nTotal TWR: %s%%.\nPeriod: %.2f years.\nAnnualized TWR: %.2f%%\n\n" (s valueAfter) (s finalUnitBalance) (s finalUnitPrice) (s totalTWR) years annualizedTWR
|
||||
|
||||
return annualizedTWR
|
||||
|
||||
|
||||
internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueBefore valueAfter cashFlow) = do
|
||||
let prefix = (spanBegin, negate valueBefore)
|
||||
|
||||
postfix = (spanEnd, valueAfter)
|
||||
@ -193,18 +193,18 @@ internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueB
|
||||
totalCF = filter ((/=0) . snd) $ prefix : (sortOn fst cashFlow) ++ [postfix]
|
||||
|
||||
when showCashFlow $ do
|
||||
printf "\nIRR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd))
|
||||
printf "\nIRR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd))
|
||||
let (dates, amounts) = unzip totalCF
|
||||
putStrLn $ Ascii.render prettyTables id id id
|
||||
(Table
|
||||
putStrLn $ Ascii.render prettyTables id id id
|
||||
(Table
|
||||
(Tbl.Group NoLine (map (Header . showDate) dates))
|
||||
(Tbl.Group SingleLine [Header "Amount"])
|
||||
(map ((:[]) . show) amounts))
|
||||
|
||||
|
||||
-- 0% is always a solution, so require at least something here
|
||||
case ridders
|
||||
case ridders
|
||||
#if MIN_VERSION_math_functions(0,3,0)
|
||||
(RiddersParam 100 (AbsTol 0.00001))
|
||||
(RiddersParam 100 (AbsTol 0.00001))
|
||||
#else
|
||||
0.00001
|
||||
#endif
|
||||
@ -227,9 +227,9 @@ calculateCashFlow trans query = map go trans
|
||||
|
||||
total :: [Transaction] -> Query -> Quantity
|
||||
total trans query = unMix $ sumPostings $ filter (matchesPosting query) $ concatMap realPostings trans
|
||||
|
||||
unMix :: MixedAmount -> Quantity
|
||||
unMix a =
|
||||
|
||||
unMix :: MixedAmount -> Quantity
|
||||
unMix a =
|
||||
case (normaliseMixedAmount $ costOfMixedAmount a) of
|
||||
(Mixed [a]) -> aquantity a
|
||||
_ -> error "MixedAmount failed to normalize"
|
||||
|
@ -4,7 +4,7 @@
|
||||
module Hledger.Cli.Commands.Tags (
|
||||
tagsmode
|
||||
,tags
|
||||
)
|
||||
)
|
||||
where
|
||||
|
||||
import Data.List
|
||||
@ -15,7 +15,7 @@ import Hledger.Cli.CliOptions
|
||||
|
||||
tagsmode = hledgerCommandMode
|
||||
$(embedFileRelative "Hledger/Cli/Commands/Tags.txt")
|
||||
[] -- [flagNone ["strict"] (setboolopt "strict") "makes date comparing strict"] --
|
||||
[] -- [flagNone ["strict"] (setboolopt "strict") "makes date comparing strict"] --
|
||||
[generalflagsgroup1]
|
||||
hiddenflags
|
||||
([], Just $ argsFlag "[TAGREGEX [QUERY...]]")
|
||||
@ -26,10 +26,10 @@ tags CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
||||
args = listofstringopt "args" rawopts
|
||||
mtagpats = headMay args
|
||||
queryargs = drop 1 args
|
||||
q = queryFromOpts d $ ropts{query_ = unwords queryargs}
|
||||
q = queryFromOpts d $ ropts{query_ = unwords queryargs}
|
||||
txns = filter (q `matchesTransaction`) $ jtxns $ journalSelectingAmountFromOpts ropts j
|
||||
tags =
|
||||
nub $ sort $
|
||||
(maybe id (filter . regexMatchesCI) mtagpats) $
|
||||
tags =
|
||||
nub $ sort $
|
||||
(maybe id (filter . regexMatchesCI) mtagpats) $
|
||||
map (T.unpack . fst) $ concatMap transactionAllTags txns
|
||||
mapM_ putStrLn tags
|
||||
|
@ -1,7 +1,7 @@
|
||||
{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
|
||||
{-|
|
||||
|
||||
Common helpers for making multi-section balance report commands
|
||||
Common helpers for making multi-section balance report commands
|
||||
like balancesheet, cashflow, and incomestatement.
|
||||
|
||||
-}
|
||||
@ -27,16 +27,16 @@ import Hledger.Cli.Commands.Balance
|
||||
import Hledger.Cli.CliOptions
|
||||
import Hledger.Cli.Utils (writeOutput)
|
||||
|
||||
-- | Description of a compound balance report command,
|
||||
-- | Description of a compound balance report command,
|
||||
-- from which we generate the command's cmdargs mode and IO action.
|
||||
-- A compound balance report command shows one or more sections/subreports,
|
||||
-- each with its own title and subtotals row, in a certain order,
|
||||
-- A compound balance report command shows one or more sections/subreports,
|
||||
-- each with its own title and subtotals row, in a certain order,
|
||||
-- plus a grand totals row if there's more than one section.
|
||||
-- Examples are the balancesheet, cashflow and incomestatement commands.
|
||||
--
|
||||
-- Compound balance reports do sign normalisation: they show all account balances
|
||||
-- Compound balance reports do sign normalisation: they show all account balances
|
||||
-- as normally positive, unlike the ordinary BalanceReport and most hledger commands
|
||||
-- which show income/liability/equity balances as normally negative.
|
||||
-- which show income/liability/equity balances as normally negative.
|
||||
-- Each subreport specifies the normal sign of its amounts, and whether
|
||||
-- it should be added to or subtracted from the grand total.
|
||||
--
|
||||
@ -44,7 +44,7 @@ data CompoundBalanceCommandSpec = CompoundBalanceCommandSpec {
|
||||
cbcdoc :: CommandDoc, -- ^ the command's name(s) and documentation
|
||||
cbctitle :: String, -- ^ overall report title
|
||||
cbcqueries :: [CBCSubreportSpec], -- ^ subreport details
|
||||
cbctype :: BalanceType -- ^ the "balance" type (change, cumulative, historical)
|
||||
cbctype :: BalanceType -- ^ the "balance" type (change, cumulative, historical)
|
||||
-- this report shows (overrides command line flags)
|
||||
}
|
||||
|
||||
@ -62,15 +62,15 @@ data CBCSubreportSpec = CBCSubreportSpec {
|
||||
--
|
||||
-- * the period (date span) of each column
|
||||
--
|
||||
-- * one or more named, normal-positive multi balance reports,
|
||||
-- * one or more named, normal-positive multi balance reports,
|
||||
-- with columns corresponding to the above, and a flag indicating
|
||||
-- whether they increased or decreased the overall totals
|
||||
--
|
||||
-- * a list of overall totals for each column, and their grand total and average
|
||||
--
|
||||
-- It is used in compound balance report commands like balancesheet,
|
||||
-- It is used in compound balance report commands like balancesheet,
|
||||
-- cashflow and incomestatement.
|
||||
type CompoundBalanceReport =
|
||||
type CompoundBalanceReport =
|
||||
( String
|
||||
, [DateSpan]
|
||||
, [(String, MultiBalanceReport, Bool)]
|
||||
@ -78,7 +78,7 @@ type CompoundBalanceReport =
|
||||
)
|
||||
|
||||
|
||||
-- | Generate a cmdargs option-parsing mode from a compound balance command
|
||||
-- | Generate a cmdargs option-parsing mode from a compound balance command
|
||||
-- specification.
|
||||
compoundBalanceCommandMode :: CompoundBalanceCommandSpec -> Mode RawOpts
|
||||
compoundBalanceCommandMode CompoundBalanceCommandSpec{..} =
|
||||
@ -120,7 +120,7 @@ compoundBalanceCommand :: CompoundBalanceCommandSpec -> (CliOpts -> Journal -> I
|
||||
compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=ropts@ReportOpts{..}, rawopts_=rawopts} j = do
|
||||
d <- getCurrentDay
|
||||
let
|
||||
-- use the default balance type for this report, unless the user overrides
|
||||
-- use the default balance type for this report, unless the user overrides
|
||||
mBalanceTypeOverride =
|
||||
case reverse $ filter (`elem` ["change","cumulative","historical"]) $ map fst rawopts of
|
||||
"historical":_ -> Just HistoricalBalance
|
||||
@ -151,13 +151,13 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=r
|
||||
Nothing -> ""
|
||||
|
||||
-- Set balance type in the report options.
|
||||
-- Also, use tree mode (by default, at least?) if --cumulative/--historical
|
||||
-- are used in single column mode, since in that situation we will be using
|
||||
-- Also, use tree mode (by default, at least?) if --cumulative/--historical
|
||||
-- are used in single column mode, since in that situation we will be using
|
||||
-- balanceReportFromMultiBalanceReport which does not support eliding boring parents,
|
||||
-- and tree mode hides this.. or something.. XXX
|
||||
-- and tree mode hides this.. or something.. XXX
|
||||
ropts'
|
||||
| not (flat_ ropts) &&
|
||||
interval_==NoInterval &&
|
||||
| not (flat_ ropts) &&
|
||||
interval_==NoInterval &&
|
||||
balancetype `elem` [CumulativeChange, HistoricalBalance]
|
||||
= ropts{balancetype_=balancetype, accountlistmode_=ALTree}
|
||||
| otherwise
|
||||
@ -166,38 +166,38 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=r
|
||||
format = outputFormatFromOpts opts
|
||||
|
||||
-- make a CompoundBalanceReport
|
||||
subreports =
|
||||
map (\CBCSubreportSpec{..} ->
|
||||
subreports =
|
||||
map (\CBCSubreportSpec{..} ->
|
||||
(cbcsubreporttitle
|
||||
,mbrNormaliseSign cbcsubreportnormalsign $ -- <- convert normal-negative to normal-positive
|
||||
compoundBalanceSubreport ropts' userq j cbcsubreportquery cbcsubreportnormalsign
|
||||
,cbcsubreportincreasestotal
|
||||
))
|
||||
cbcqueries
|
||||
subtotalrows =
|
||||
[(coltotals, increasesoveralltotal)
|
||||
subtotalrows =
|
||||
[(coltotals, increasesoveralltotal)
|
||||
| (_, MultiBalanceReport (_,_,(coltotals,_,_)), increasesoveralltotal) <- subreports
|
||||
]
|
||||
-- Sum the subreport totals by column. Handle these cases:
|
||||
-- - no subreports
|
||||
-- - empty subreports, having no subtotals (#588)
|
||||
-- - subreports with a shorter subtotals row than the others
|
||||
-- - subreports with a shorter subtotals row than the others
|
||||
overalltotals = case subtotalrows of
|
||||
[] -> ([], nullmixedamt, nullmixedamt)
|
||||
rs ->
|
||||
let
|
||||
numcols = maximum $ map (length.fst) rs -- partial maximum is ok, rs is non-null
|
||||
paddedsignedsubtotalrows =
|
||||
paddedsignedsubtotalrows =
|
||||
[map (if increasesoveralltotal then id else negate) $ -- maybe flip the signs
|
||||
take numcols $ as ++ repeat nullmixedamt -- pad short rows with zeros
|
||||
take numcols $ as ++ repeat nullmixedamt -- pad short rows with zeros
|
||||
| (as,increasesoveralltotal) <- rs
|
||||
]
|
||||
coltotals = foldl' (zipWith (+)) zeros paddedsignedsubtotalrows -- sum the columns
|
||||
where zeros = replicate numcols nullmixedamt
|
||||
grandtotal = sum coltotals
|
||||
grandavg | null coltotals = nullmixedamt
|
||||
| otherwise = fromIntegral (length coltotals) `divideMixedAmount` grandtotal
|
||||
in
|
||||
| otherwise = fromIntegral (length coltotals) `divideMixedAmount` grandtotal
|
||||
in
|
||||
(coltotals, grandtotal, grandavg)
|
||||
colspans =
|
||||
case subreports of
|
||||
@ -230,12 +230,12 @@ compoundBalanceSubreport ropts@ReportOpts{..} userq j subreportqfn subreportnorm
|
||||
-- if user didn't specify --empty, now remove the all-zero rows, unless they have non-zero subaccounts
|
||||
-- in this report
|
||||
r' | empty_ = r
|
||||
| otherwise = MultiBalanceReport (dates, rows', totals)
|
||||
| otherwise = MultiBalanceReport (dates, rows', totals)
|
||||
where
|
||||
nonzeroaccounts =
|
||||
dbg1 "nonzeroaccounts" $
|
||||
catMaybes $ map (\(act,_,_,amts,_,_) ->
|
||||
if not (all isZeroMixedAmount amts) then Just act else Nothing) rows
|
||||
if not (all isZeroMixedAmount amts) then Just act else Nothing) rows
|
||||
rows' = filter (not . emptyRow) rows
|
||||
where
|
||||
emptyRow (act,_,_,amts,_,_) =
|
||||
@ -245,34 +245,34 @@ compoundBalanceSubreport ropts@ReportOpts{..} userq j subreportqfn subreportnorm
|
||||
{- Eg:
|
||||
Balance Sheet
|
||||
|
||||
|| 2017/12/31 Total Average
|
||||
|| 2017/12/31 Total Average
|
||||
=============++===============================
|
||||
Assets ||
|
||||
Assets ||
|
||||
-------------++-------------------------------
|
||||
assets:b || 1 1 1
|
||||
assets:b || 1 1 1
|
||||
-------------++-------------------------------
|
||||
|| 1 1 1
|
||||
|| 1 1 1
|
||||
=============++===============================
|
||||
Liabilities ||
|
||||
Liabilities ||
|
||||
-------------++-------------------------------
|
||||
-------------++-------------------------------
|
||||
||
|
||||
||
|
||||
=============++===============================
|
||||
Total || 1 1 1
|
||||
Total || 1 1 1
|
||||
|
||||
-}
|
||||
compoundBalanceReportAsText :: ReportOpts -> CompoundBalanceReport -> String
|
||||
compoundBalanceReportAsText ropts (title, _colspans, subreports, (coltotals, grandtotal, grandavg)) =
|
||||
title ++ "\n\n" ++
|
||||
title ++ "\n\n" ++
|
||||
balanceReportTableAsText ropts bigtable'
|
||||
where
|
||||
singlesubreport = length subreports == 1
|
||||
bigtable =
|
||||
bigtable =
|
||||
case map (subreportAsTable ropts singlesubreport) subreports of
|
||||
[] -> T.empty
|
||||
r:rs -> foldl' concatTables r rs
|
||||
bigtable'
|
||||
| no_total_ ropts || singlesubreport =
|
||||
| no_total_ ropts || singlesubreport =
|
||||
bigtable
|
||||
| otherwise =
|
||||
bigtable
|
||||
@ -332,11 +332,11 @@ compoundBalanceReportAsCsv ropts (title, colspans, subreports, (coltotals, grand
|
||||
(if row_total_ ropts then (1+) else id) $
|
||||
(if average_ ropts then (1+) else id) $
|
||||
maximum $ -- depends on non-null subreports
|
||||
map (\(MultiBalanceReport (amtcolheadings, _, _)) -> length amtcolheadings) $
|
||||
map (\(MultiBalanceReport (amtcolheadings, _, _)) -> length amtcolheadings) $
|
||||
map second3 subreports
|
||||
addtotals
|
||||
| no_total_ ropts || length subreports == 1 = id
|
||||
| otherwise = (++
|
||||
| otherwise = (++
|
||||
["Net:" :
|
||||
map showMixedAmountOneLineWithoutPrice (
|
||||
coltotals
|
||||
@ -350,7 +350,7 @@ compoundBalanceReportAsHtml :: ReportOpts -> CompoundBalanceReport -> Html ()
|
||||
compoundBalanceReportAsHtml ropts cbr =
|
||||
let
|
||||
(title, colspans, subreports, (coltotals, grandtotal, grandavg)) = cbr
|
||||
colspanattr = colspan_ $ TS.pack $ show $
|
||||
colspanattr = colspan_ $ TS.pack $ show $
|
||||
1 + length colspans + (if row_total_ ropts then 1 else 0) + (if average_ ropts then 1 else 0)
|
||||
leftattr = style_ "text-align:left"
|
||||
blankrow = tr_ $ td_ [colspanattr] $ toHtmlRaw (" "::String)
|
||||
@ -366,7 +366,7 @@ compoundBalanceReportAsHtml ropts cbr =
|
||||
|
||||
thRow :: [String] -> Html ()
|
||||
thRow = tr_ . mconcat . map (th_ . toHtml)
|
||||
|
||||
|
||||
-- Make rows for a subreport: its title row, not the headings row,
|
||||
-- the data rows, any totals row, and a blank row for whitespace.
|
||||
subreportrows :: (String, MultiBalanceReport, Bool) -> [Html ()]
|
||||
|
@ -60,7 +60,7 @@ import Hledger.Reports
|
||||
import Hledger.Utils
|
||||
|
||||
-- | Parse the user's specified journal file(s) as a Journal, maybe apply some
|
||||
-- transformations according to options, and run a hledger command with it.
|
||||
-- transformations according to options, and run a hledger command with it.
|
||||
-- Or, throw an error.
|
||||
withJournalDo :: CliOpts -> (Journal -> IO a) -> IO a
|
||||
withJournalDo opts cmd = do
|
||||
@ -149,10 +149,10 @@ journalAddForecast opts@CliOpts{inputopts_=iopts, reportopts_=ropts} j = do
|
||||
forecasttxns' = (if auto_ iopts then modifyTransactions (jtxnmodifiers j) else id) forecasttxns
|
||||
|
||||
return $
|
||||
if forecast_ ropts
|
||||
if forecast_ ropts
|
||||
then journalBalanceTransactions' opts j{ jtxns = concat [jtxns j, forecasttxns'] }
|
||||
else j
|
||||
where
|
||||
where
|
||||
journalBalanceTransactions' opts j =
|
||||
let assrt = not . ignore_assertions_ $ inputopts_ opts
|
||||
in
|
||||
@ -164,7 +164,7 @@ writeOutput :: CliOpts -> String -> IO ()
|
||||
writeOutput opts s = do
|
||||
f <- outputFileFromOpts opts
|
||||
(if f == "-" then putStr else writeFile f) s
|
||||
|
||||
|
||||
-- -- | Get a journal from the given string and options, or throw an error.
|
||||
-- readJournal :: CliOpts -> String -> IO Journal
|
||||
-- readJournal opts s = readJournal def Nothing s >>= either error' return
|
||||
|
Loading…
Reference in New Issue
Block a user