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 qualified Data.Time.Clock as Time
22 import System.Console.GetOpt
27 import System.Environment as Env (getProgName)
28 import System.Exit (exitSuccess)
29 import qualified System.IO as IO
30 import qualified Text.Parsec
32 import Hcompta.Account (Account)
33 import qualified Hcompta.Account as Account
34 import qualified Hcompta.Account.Read as Account.Read
35 import Hcompta.Amount (Amount)
36 import qualified Hcompta.Amount as Amount
37 import qualified Hcompta.Amount.Write as Amount.Write
38 import Hcompta.Amount.Unit (Unit)
39 import qualified Hcompta.Balance as Balance
40 import qualified Hcompta.CLI.Args as Args
41 import Hcompta.CLI.Context (Context)
42 import qualified Hcompta.CLI.Context as Context
43 import qualified Hcompta.CLI.Format.Ledger as CLI.Ledger
44 import qualified Hcompta.CLI.Lang as Lang
45 import qualified Hcompta.CLI.Lib.Leijen.Table as Table
46 import qualified Hcompta.CLI.Write as Write
47 import qualified Hcompta.Date as Date
48 import qualified Hcompta.Filter as Filter
49 import qualified Hcompta.Filter.Read as Filter.Read
50 import qualified Hcompta.Format.Ledger as Ledger
51 import qualified Hcompta.Format.Ledger.Journal as Ledger.Journal
52 import qualified Hcompta.Format.Ledger.Read as Ledger.Read
53 import qualified Hcompta.Format.Ledger.Write as Ledger.Write
54 import Hcompta.Lib.Leijen (toDoc, ToDoc(..))
55 import qualified Hcompta.Lib.Leijen as W
56 import Hcompta.Lib.TreeMap (TreeMap)
57 import qualified Hcompta.Lib.TreeMap as TreeMap
58 import qualified Hcompta.Posting as Posting
62 { ctx_filter_balance :: Filter.Simplified
64 (Filter.Filter_Balance
65 (Account, Amount.Sum Amount)))
66 , ctx_filter_posting :: Filter.Simplified
68 (Filter.Filter_Posting
70 , ctx_filter_transaction :: Filter.Simplified
72 (Filter.Filter_Transaction
74 , ctx_heritage :: Bool
75 , ctx_input :: [FilePath]
76 , ctx_reduce_date :: Bool
77 , ctx_redundant :: Bool
78 , ctx_total_by_unit :: Bool
79 , ctx_format_output :: Format_Output
80 , ctx_account_equilibrium :: Account
85 | Format_Output_Transaction { negate_transaction :: Bool }
91 { ctx_filter_balance = mempty
92 , ctx_filter_posting = mempty
93 , ctx_filter_transaction = mempty
96 , ctx_reduce_date = True
97 , ctx_redundant = False
98 , ctx_total_by_unit = True
99 , ctx_format_output = Format_Output_Table
100 , ctx_account_equilibrium = Account.account
101 (TL.toStrict $ W.displayT $ W.renderOneLine False $
102 toDoc (Context.lang context) Lang.Message_Equilibrium)
108 bin <- Env.getProgName
109 let pad = replicate (length bin) ' '
112 , " "++bin++" balance [-i JOURNAL_FILE]"
113 , " "++pad++" [-b BALANCE_FILTER]"
114 , " "++pad++" [-p POSTING_FILTER]"
115 , " "++pad++" [-t TRANSACTION_FILTER]"
116 , " "++pad++" [JOURNAL_FILE] [...]"
118 , usageInfo "OPTIONS" options
121 options :: Args.Options Ctx
123 [ Option "b" ["filter-balance"]
124 (ReqArg (\s context ctx -> do
125 ctx_filter_balance <-
126 liftM ((ctx_filter_balance ctx <>) . Filter.simplify) $
127 liftIO $ Filter.Read.read Filter.Read.filter_balance s
129 Left ko -> Write.fatal context $ ko
130 Right ok -> return ok
131 return $ ctx{ctx_filter_balance}) "FILTER")
132 "filter at balance level, multiple uses are merged with a logical AND"
133 , Option "p" ["filter-posting"]
134 (ReqArg (\s context ctx -> do
135 ctx_filter_posting <-
136 liftM ((ctx_filter_posting ctx <>) . Filter.simplify) $
137 liftIO $ Filter.Read.read Filter.Read.filter_posting s
139 Left ko -> Write.fatal context $ ko
140 Right ok -> return ok
141 return $ ctx{ctx_filter_posting}) "FILTER")
142 "filter at posting level, multiple uses are merged with a logical AND"
143 , Option "t" ["filter-transaction"]
144 (ReqArg (\s context ctx -> do
145 ctx_filter_transaction <-
146 liftM ((ctx_filter_transaction ctx <>) . Filter.simplify) $
147 liftIO $ Filter.Read.read Filter.Read.filter_transaction s
149 Left ko -> Write.fatal context $ ko
150 Right ok -> return ok
151 return $ ctx{ctx_filter_transaction}) "FILTER")
152 "filter at transaction level, multiple uses are merged with a logical AND"
153 , Option "h" ["help"]
154 (NoArg (\_context _ctx -> do
155 usage >>= IO.hPutStr IO.stderr
158 , Option "i" ["input"]
159 (ReqArg (\s _context ctx -> do
160 return $ ctx{ctx_input=s:ctx_input ctx}) "FILE")
161 "read data from given file, multiple uses merge the data as would a concatenation do"
162 {- NOTE: not used so far.
163 , Option "" ["reduce-date"]
164 (OptArg (\arg context ctx -> do
165 ctx_reduce_date <- case arg of
166 Nothing -> return $ True
167 Just "yes" -> return $ True
168 Just "no" -> return $ False
169 Just _ -> Write.fatal context $
170 W.text "--reduce-date option expects \"yes\", or \"no\" as value"
171 return $ ctx{ctx_reduce_date})
173 "use advanced date reducer to speed up filtering"
175 , Option "" ["redundant"]
176 (OptArg (\arg context ctx -> do
177 ctx_redundant <- case arg of
178 Nothing -> return $ True
179 Just "yes" -> return $ True
180 Just "no" -> return $ False
181 Just _ -> Write.fatal context $
182 W.text "--redundant option expects \"yes\", or \"no\" as value"
183 return $ ctx{ctx_redundant})
185 "also print accounts with zero amount or the same amounts than its ascending account"
186 , Option "" ["heritage"]
187 (OptArg (\arg context ctx -> do
188 ctx_heritage <- case arg of
189 Nothing -> return $ True
190 Just "yes" -> return $ True
191 Just "no" -> return $ False
192 Just _ -> Write.fatal context $
193 W.text "--heritage option expects \"yes\", or \"no\" as value"
194 return $ ctx{ctx_heritage})
196 "propagate amounts to ascending accounts"
197 , Option "" ["total"]
198 (OptArg (\arg context ctx -> do
199 ctx_total_by_unit <- case arg of
200 Nothing -> return $ True
201 Just "yes" -> return $ True
202 Just "no" -> return $ False
203 Just _ -> Write.fatal context $
204 W.text "--total option expects \"yes\", or \"no\" as value"
205 return $ ctx{ctx_total_by_unit})
207 "calculate totals by unit"
208 , Option "f" ["format"]
209 (ReqArg (\arg context ctx -> do
210 ctx_format_output <- case arg of
211 "table" -> return $ Format_Output_Table
212 "open" -> return $ Format_Output_Transaction False
213 "close" -> return $ Format_Output_Transaction True
214 _ -> Write.fatal context $
215 W.text "--format option expects \"close\", \"open\", or \"table\" as value"
216 return $ ctx{ctx_format_output})
217 "[close|open|table]")
218 "select output format"
219 , Option "" ["equilibrium"]
220 (ReqArg (\arg context ctx -> do
221 ctx_account_equilibrium <-
222 case Text.Parsec.runParser
223 (Account.Read.account <* Text.Parsec.eof)
225 Right acct -> return acct
226 _ -> Write.fatal context $
227 W.text "--equilibrium option expects a valid account name"
228 return $ ctx{ctx_account_equilibrium})
230 "specify account equilibrating a close or open balance"
233 run :: Context.Context -> [String] -> IO ()
234 run context args = do
235 (ctx, inputs) <- Args.parse context usage options (nil context, args)
237 liftM Data.Either.partitionEithers $ do
238 CLI.Ledger.paths context $ ctx_input ctx ++ inputs
241 liftIO $ runExceptT $ Ledger.Read.file
242 (Ledger.Read.context ( ctx_filter_transaction ctx
243 , ctx_filter_posting ctx )
247 Left ko -> return $ Left (path, ko)
248 Right ok -> return $ Right ok
249 case read_journals of
250 (errs@(_:_), _journals) ->
251 forM_ errs $ \(_path, err) -> do
252 Write.fatal context $ err
254 Write.debug context $ "filter: transaction: " ++ show (ctx_filter_transaction ctx)
255 Write.debug context $ "filter: posting: " ++ show (ctx_filter_posting ctx)
256 Write.debug context $ "filter: balance: " ++ show (ctx_filter_balance ctx)
257 style_color <- Write.with_color context IO.stdout
258 case ctx_format_output ctx of
259 Format_Output_Transaction nt -> do
260 let balance_by_account =
261 ledger_balance_by_account_filter ctx $
262 ledger_balance_by_account ctx journals
263 let Balance.Balance_by_Unit balance_by_unit =
264 ledger_balance_by_unit ctx $
265 ledger_balance_by_account_filter ctx balance_by_account
266 let posting_equilibrium =
267 (Ledger.posting $ ctx_account_equilibrium ctx)
268 { Ledger.posting_amounts =
269 flip Data.Map.map balance_by_unit $
270 (if nt then id else negate)
272 . Balance.unit_sum_amount
273 , Ledger.posting_comments=
274 [ TL.toStrict $ W.displayT $ W.renderOneLine False $
275 toDoc (Context.lang context) $
276 Lang.Message_Equilibrium_posting
279 now <- liftM (\d -> d{Time.utctDayTime=0}) $ Date.now
282 { Ledger.transaction_description=
283 TL.toStrict $ W.displayT $ W.renderOneLine False $
284 toDoc (Context.lang context) $
285 Lang.Message_Balance_Description nt
286 , Ledger.transaction_dates=(now, [])
287 , Ledger.transaction_postings=
288 (if null $ Ledger.posting_amounts posting_equilibrium
291 Data.Map.insertWith (++)
292 (ctx_account_equilibrium ctx)
293 [posting_equilibrium]) $
294 TreeMap.flatten_with_Path
295 (\posting_account (Balance.Account_Sum amount_by_unit) ->
296 [(Ledger.posting posting_account)
297 { Ledger.posting_amounts =
298 flip fmap amount_by_unit $
299 (if nt then negate else id)
306 let sty = Ledger.Write.Style
307 { Ledger.Write.style_align = True -- ctx_align ctx
308 , Ledger.Write.style_color
310 Ledger.Write.put sty IO.stdout $ do
311 Ledger.Write.transaction transaction
312 Format_Output_Table -> do
313 let ( table_balance_by_account
314 , Balance.Balance_by_Unit balance_by_unit
316 case ledger_balance_by_account ctx journals of
317 b | ctx_heritage ctx ->
318 let bb = ledger_balance_by_account_expanded ctx b in
319 ( table_by_account ctx Balance.inclusive bb
320 , ledger_balance_by_unit_expanded ctx bb
323 let bb = ledger_balance_by_account_filter ctx b in
324 ( table_by_account ctx id bb
325 , ledger_balance_by_unit ctx bb
327 W.displayIO IO.stdout $ do
328 W.renderPretty style_color 1.0 maxBound $ do
331 TL.toStrict . W.displayT .
332 W.renderCompact False .
333 toDoc (Context.lang context)
335 [ Table.column (title Lang.Message_Debit) Table.Align_Right
336 , Table.column (title Lang.Message_Credit) Table.Align_Right
337 , Table.column (title Lang.Message_Balance) Table.Align_Right
338 , Table.column (title Lang.Message_Account) Table.Align_Left
340 table_balance_by_account $ do
341 case ctx_total_by_unit ctx of
345 [ Table.Cell_Line '=' 0
346 , Table.Cell_Line '=' 0
347 , Table.Cell_Line '=' 0
348 , Table.Cell_Line ' ' 0
350 flip table_by_unit (repeat []) $
352 Balance.unit_sum_amount
355 ledger_balance_by_account
357 -> [ Ledger.Journal (Const (Balance.Balance_by_Account (Amount.Sum Amount)) Ledger.Transaction) ]
358 -> Balance.Balance_by_Account (Amount.Sum Amount)
359 ledger_balance_by_account _ctx =
361 (flip $ Ledger.Journal.fold
362 (\Ledger.Journal{Ledger.journal_transactions=Const b} ->
366 ledger_balance_by_account_filter
368 -> Balance.Balance_by_Account (Amount.Sum Amount)
369 -> Balance.Balance_by_Account (Amount.Sum Amount)
370 ledger_balance_by_account_filter ctx =
371 case Filter.simplified $ ctx_filter_balance ctx of
373 Right False -> const mempty
375 TreeMap.filter_with_Path $ \acct ->
376 Data.Foldable.any (Filter.test flt . (acct,)) .
377 Balance.get_Account_Sum
379 ledger_balance_by_account_expanded
381 -> Balance.Balance_by_Account (Amount.Sum Amount)
382 -> Balance.Expanded (Amount.Sum Amount)
383 ledger_balance_by_account_expanded ctx =
384 case Filter.simplified $ ctx_filter_balance ctx of
386 Right False -> const mempty
388 TreeMap.filter_with_Path_and_Node
389 (\node acct balance ->
390 let descendants = TreeMap.nodes
391 (TreeMap.node_descendants node) in
394 -- NOTE: worth if no descendant
395 -- but Account's inclusive
396 -- has at least a non-zero Amount
397 || (Data.Map.null descendants &&
399 (not . Amount.is_zero . Amount.sum_balance)
400 (Balance.get_Account_Sum $ Balance.inclusive balance)))
401 -- NOTE: worth if Account's exclusive
402 -- has at least a non-zero Amount
403 || (Data.Foldable.any
404 (not . Amount.is_zero . Amount.sum_balance)
405 (Balance.get_Account_Sum $ Balance.exclusive balance))
406 -- NOTE: worth if Account has at least more than
407 -- one descendant Account whose inclusive
408 -- has at least a non-zero Amount
413 (not . Amount.is_zero . Amount.sum_balance)
414 . Balance.get_Account_Sum
415 . Balance.inclusive )
416 . TreeMap.node_value )
421 Data.Foldable.any (Filter.test flt . (acct,)) $
422 Balance.get_Account_Sum $
423 Balance.inclusive balance
427 ledger_balance_by_unit
429 -> Balance.Balance_by_Account (Amount.Sum Amount)
430 -> Balance.Balance_by_Unit (Amount.Sum Amount)
431 ledger_balance_by_unit _ctx =
432 flip Balance.by_unit_of_by_account mempty
434 ledger_balance_by_unit_expanded
436 -> Balance.Expanded (Amount.Sum Amount)
437 -> Balance.Balance_by_Unit (Amount.Sum Amount)
438 ledger_balance_by_unit_expanded _ctx =
439 flip Balance.by_unit_of_expanded mempty
443 -> (amount -> Balance.Account_Sum (Amount.Sum Amount))
444 -> TreeMap Account.Name amount
447 table_by_account _ctx get_Account_Sum =
448 let posting_type = Posting.Posting_Type_Regular in
449 flip $ TreeMap.foldr_with_Path
450 (\account balance rows ->
452 (\(amount_positive, amount_negative, amount) ->
455 { Table.cell_content = maybe W.empty Amount.Write.amount amount_positive
456 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_positive
459 { Table.cell_content = maybe W.empty Amount.Write.amount amount_negative
460 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_negative
463 { Table.cell_content = Amount.Write.amount $ amount
464 , Table.cell_width = Amount.Write.amount_length $ amount
467 { Table.cell_content = Ledger.Write.account posting_type account
468 , Table.cell_width = Ledger.Write.account_length posting_type account
473 let bal = Balance.get_Account_Sum $ get_Account_Sum balance in
474 Data.Map.foldrWithKey
476 ( maybe Nothing Amount.sum_positive $ Data.Map.lookup unit $ bal
477 , maybe Nothing Amount.sum_negative $ Data.Map.lookup unit $ bal
478 , Amount.sum_balance amount
484 :: Data.Map.Map Unit (Amount.Sum Amount)
491 [ let amt = Amount.sum_positive amount_sum in
493 { Table.cell_content = maybe W.empty Amount.Write.amount amt
494 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
496 , let amt = Amount.sum_negative amount_sum in
498 { Table.cell_content = maybe W.empty Amount.Write.amount amt
499 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
501 , let amt = Amount.sum_balance amount_sum in
503 { Table.cell_content = Amount.Write.amount amt
504 , Table.cell_width = Amount.Write.amount_length amt
507 { Table.cell_content = W.empty
508 , Table.cell_width = 0