1 {-# LANGUAGE FlexibleInstances #-}
2 {-# LANGUAGE MultiParamTypeClasses #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 {-# LANGUAGE TupleSections #-}
7 module Hcompta.CLI.Command.Balance where
9 import Control.Applicative (Const(..))
10 import Prelude hiding (foldr)
11 import Control.Monad (liftM, forM_)
12 import Control.Monad.IO.Class (liftIO)
13 import Control.Monad.Trans.Except (runExceptT)
14 import qualified Data.Either
15 import qualified Data.Foldable
16 import Data.Foldable (foldr)
17 import qualified Data.Map.Strict as Data.Map
18 import Data.Monoid ((<>))
19 import qualified Data.Strict.Maybe as Strict
20 import qualified Data.Text.Lazy as TL
21 import System.Console.GetOpt
26 import System.Environment as Env (getProgName)
27 import System.Exit (exitSuccess)
28 import qualified System.IO as IO
30 import Hcompta.Account (Account)
31 import Hcompta.Amount (Amount)
32 import qualified Hcompta.Amount as Amount
33 import qualified Hcompta.Amount.Write as Amount.Write
34 import Hcompta.Amount.Unit (Unit)
35 import qualified Hcompta.Balance as Balance
36 import qualified Hcompta.CLI.Args as Args
37 import qualified Hcompta.CLI.Context as Context
38 import qualified Hcompta.CLI.Format.Ledger as CLI.Ledger
39 import qualified Hcompta.CLI.Lang as Lang
40 import qualified Hcompta.CLI.Lib.Leijen.Table as Table
41 import qualified Hcompta.CLI.Write as Write
42 import qualified Hcompta.Filter as Filter
43 import qualified Hcompta.Filter.Read as Filter.Read
44 import qualified Hcompta.Format.Ledger as Ledger
45 import qualified Hcompta.Format.Ledger.Journal as Ledger.Journal
46 import qualified Hcompta.Format.Ledger.Read as Ledger.Read
47 import qualified Hcompta.Format.Ledger.Write as Ledger.Write
48 import Hcompta.Lib.Leijen (toDoc, ToDoc(..))
49 import qualified Hcompta.Lib.Leijen as W
50 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
54 { ctx_filter_balance :: Filter.Simplified
56 (Filter.Filter_Balance
57 (Account, Amount.Sum Amount)))
58 , ctx_filter_posting :: Filter.Simplified
60 (Filter.Filter_Posting
62 , ctx_filter_transaction :: Filter.Simplified
64 (Filter.Filter_Transaction
66 , ctx_input :: [FilePath]
67 , ctx_reduce_date :: Bool
68 , ctx_redundant :: Bool
74 { ctx_filter_balance = mempty
75 , ctx_filter_posting = mempty
76 , ctx_filter_transaction = mempty
78 , ctx_reduce_date = True
79 , ctx_redundant = False
84 bin <- Env.getProgName
85 let pad = replicate (length bin) ' '
88 , " "++bin++" balance [-i JOURNAL_FILE]"
89 , " "++pad++" [-b BALANCE_FILTER]"
90 , " "++pad++" [-p POSTING_FILTER]"
91 , " "++pad++" [-t TRANSACTION_FILTER]"
92 , " "++pad++" [JOURNAL_FILE] [...]"
94 , usageInfo "OPTIONS" options
97 options :: Args.Options Ctx
99 [ Option "b" ["filter-balance"]
100 (ReqArg (\s context ctx -> do
101 ctx_filter_balance <-
102 liftM ((ctx_filter_balance ctx <>) . Filter.simplify) $
103 liftIO $ Filter.Read.read Filter.Read.filter_balance s
105 Left ko -> Write.fatal context $ ko
106 Right ok -> return ok
107 return $ ctx{ctx_filter_balance}) "FILTER")
108 "filter at balance level, multiple uses are merged with a logical AND"
109 , Option "p" ["filter-posting"]
110 (ReqArg (\s context ctx -> do
111 ctx_filter_posting <-
112 liftM ((ctx_filter_posting ctx <>) . Filter.simplify) $
113 liftIO $ Filter.Read.read Filter.Read.filter_posting s
115 Left ko -> Write.fatal context $ ko
116 Right ok -> return ok
117 return $ ctx{ctx_filter_posting}) "FILTER")
118 "filter at posting level, multiple uses are merged with a logical AND"
119 , Option "t" ["filter-transaction"]
120 (ReqArg (\s context ctx -> do
121 ctx_filter_transaction <-
122 liftM ((ctx_filter_transaction ctx <>) . Filter.simplify) $
123 liftIO $ Filter.Read.read Filter.Read.filter_transaction s
125 Left ko -> Write.fatal context $ ko
126 Right ok -> return ok
127 return $ ctx{ctx_filter_transaction}) "FILTER")
128 "filter at transaction level, multiple uses are merged with a logical AND"
129 , Option "h" ["help"]
130 (NoArg (\_context _ctx -> do
131 usage >>= IO.hPutStr IO.stderr
134 , Option "i" ["input"]
135 (ReqArg (\s _context ctx -> do
136 return $ ctx{ctx_input=s:ctx_input ctx}) "FILE")
137 "read data from given file, multiple uses merge the data as would a concatenation do"
138 {- NOTE: not used so far.
139 , Option "" ["reduce-date"]
140 (OptArg (\arg context ctx -> do
141 ctx_reduce_date <- case arg of
142 Nothing -> return $ True
143 Just "yes" -> return $ True
144 Just "no" -> return $ False
145 Just _ -> Write.fatal context $
146 W.text "--reduce-date option expects \"yes\", or \"no\" as value"
147 return $ ctx{ctx_reduce_date})
149 "use advanced date reducer to speed up filtering"
151 , Option "" ["redundant"]
152 (OptArg (\arg context ctx -> do
153 ctx_redundant <- case arg of
154 Nothing -> return $ True
155 Just "yes" -> return $ True
156 Just "no" -> return $ False
157 Just _ -> Write.fatal context $
158 W.text "--redundant option expects \"yes\", or \"no\" as value"
159 return $ ctx{ctx_redundant})
161 "also print accounts with zero amount or the same amounts than its ascending account"
164 run :: Context.Context -> [String] -> IO ()
165 run context args = do
166 (ctx, inputs) <- Args.parse context usage options (nil, args)
168 liftM Data.Either.partitionEithers $ do
169 CLI.Ledger.paths context $ ctx_input ctx ++ inputs
172 liftIO $ runExceptT $ Ledger.Read.file
173 (Ledger.Read.context ( ctx_filter_transaction ctx
174 , ctx_filter_posting ctx )
178 Left ko -> return $ Left (path, ko)
179 Right ok -> return $ Right ok
180 case read_journals of
181 (errs@(_:_), _journals) ->
182 forM_ errs $ \(_path, err) -> do
183 Write.fatal context $ err
185 Write.debug context $ "filter: transaction: " ++ show (ctx_filter_transaction ctx)
186 Write.debug context $ "filter: posting: " ++ show (ctx_filter_posting ctx)
187 Write.debug context $ "filter: balance: " ++ show (ctx_filter_balance ctx)
188 let (balance_by_account, Balance.Balance_by_Unit balance_by_unit) =
189 ledger_balances ctx journals
190 style_color <- Write.with_color context IO.stdout
191 W.displayIO IO.stdout $
192 W.renderPretty style_color 1.0 maxBound $ do
195 TL.toStrict . W.displayT .
196 W.renderCompact False .
197 toDoc (Context.lang context) in
199 [ Table.column (title Lang.Message_Debit) Table.Align_Right
200 , Table.column (title Lang.Message_Credit) Table.Align_Right
201 , Table.column (title Lang.Message_Balance) Table.Align_Right
202 , Table.column (title Lang.Message_Account) Table.Align_Left
204 write_by_accounts ctx balance_by_account $
206 [ Table.Cell_Line '=' 0
207 , Table.Cell_Line '=' 0
208 , Table.Cell_Line '=' 0
209 , Table.Cell_Line ' ' 0
211 flip write_by_amounts (repeat []) $
213 Balance.unit_sum_amount
218 -> [ Ledger.Journal (Const (Balance.Balance_by_Account (Amount.Sum Amount)) Ledger.Transaction) ]
219 -> ( Balance.Expanded (Amount.Sum Amount)
220 , Balance.Balance_by_Unit (Amount.Sum Amount) )
221 ledger_balances ctx journals =
222 let balance_by_account =
224 (flip $ Ledger.Journal.fold
225 (\Ledger.Journal{Ledger.journal_transactions=Const b} ->
228 let balance_expanded =
229 Lib.TreeMap.filter_with_Path_and_Node
230 (\node acct balance ->
231 let descendants = Lib.TreeMap.nodes
232 (Lib.TreeMap.node_descendants node) in
235 -- NOTE: worth if no descendant
236 -- but account inclusive
237 -- has at least a non-zero amount
238 || (Data.Map.null descendants && not
241 (not . Amount.is_zero . Amount.sum_balance)
242 (Balance.get_Account_Sum $ Balance.inclusive balance))))
243 -- NOTE: worth if account exclusive
244 -- has at least a non-zero amount
245 || not (Data.Map.null
247 (not . Amount.is_zero . Amount.sum_balance)
248 (Balance.get_Account_Sum $ Balance.exclusive balance)))
249 -- NOTE: worth if account has at least more than
250 -- one descendant account whose inclusive
251 -- has at least a non-zero amount
255 ( not . Data.Foldable.all
257 . Amount.sum_balance )
258 . Balance.get_Account_Sum
259 . Balance.inclusive )
260 . Lib.TreeMap.node_value )
265 (Filter.test (ctx_filter_balance ctx) . (acct,)) $
266 Balance.get_Account_Sum $
267 Balance.inclusive balance
269 Balance.expanded balance_by_account in
270 let balance_by_unit =
271 Balance.by_unit_of_expanded
280 -> Balance.Expanded (Amount.Sum Amount)
283 write_by_accounts _ctx =
284 let posting_type = Ledger.Posting_Type_Regular in
285 flip $ Lib.TreeMap.foldr_with_Path
286 (\account balance rows ->
288 (\(amount_positive, amount_negative, amount) ->
291 { Table.cell_content = maybe W.empty Amount.Write.amount amount_positive
292 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_positive
295 { Table.cell_content = maybe W.empty Amount.Write.amount amount_negative
296 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_negative
299 { Table.cell_content = Amount.Write.amount $ amount
300 , Table.cell_width = Amount.Write.amount_length $ amount
303 { Table.cell_content = Ledger.Write.account posting_type account
304 , Table.cell_width = Ledger.Write.account_length posting_type account
309 let bal = Balance.get_Account_Sum $ Balance.inclusive balance in
310 Data.Map.foldrWithKey
312 ( maybe Nothing Amount.sum_positive $ Data.Map.lookup unit $ bal
313 , maybe Nothing Amount.sum_negative $ Data.Map.lookup unit $ bal
314 , Amount.sum_balance amount
320 :: Data.Map.Map Unit (Amount.Sum Amount)
327 [ let amt = Amount.sum_positive amount_sum in
329 { Table.cell_content = maybe W.empty Amount.Write.amount amt
330 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
332 , let amt = Amount.sum_negative amount_sum in
334 { Table.cell_content = maybe W.empty Amount.Write.amount amt
335 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
337 , let amt = Amount.sum_balance amount_sum in
339 { Table.cell_content = Amount.Write.amount amt
340 , Table.cell_width = Amount.Write.amount_length amt
343 { Table.cell_content = W.empty
344 , Table.cell_width = 0