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 Control.Arrow (first)
11 import Control.Monad (Monad(..), forM_, liftM, mapM)
12 import Control.Monad.IO.Class (liftIO)
13 import Control.Monad.Trans.Except (runExceptT)
15 import Data.Either (Either(..), partitionEithers)
16 import Data.Eq (Eq(..))
17 import Data.Foldable (Foldable(..), any)
18 import Data.Functor (Functor(..), (<$>))
19 import Data.List ((++), repeat)
20 import qualified Data.Map.Strict as Data.Map
21 import Data.Maybe (Maybe(..), maybe)
22 import Data.Monoid (Monoid(..), (<>))
23 import Data.Ord (Ord(..), Ordering(..))
24 import qualified Data.Strict.Maybe as Strict
25 import Data.String (String)
26 import Data.Tuple (fst, snd)
27 import qualified Data.Time.Clock as Time
28 import Prelude (($), (.), FilePath, IO, Num(..), const, id, flip, unlines, zipWith)
29 import qualified Text.Parsec
30 import Text.Show (Show(..))
31 import System.Console.GetOpt
36 import System.Environment as Env (getProgName)
37 import System.Exit (exitSuccess)
38 import qualified System.IO as IO
40 import Hcompta.Account (Account)
41 import qualified Hcompta.Account as Account
42 import qualified Hcompta.Account.Read as Account.Read
43 import Hcompta.Amount (Amount)
44 import qualified Hcompta.Amount as Amount
45 import Hcompta.Amount.Unit (Unit)
46 import qualified Hcompta.Amount.Write as Amount.Write
47 import qualified Hcompta.Balance as Balance
48 import Hcompta.Chart (Chart)
49 import qualified Hcompta.Chart as Chart
50 import qualified Hcompta.CLI.Args as Args
51 import qualified Hcompta.CLI.Context as C
52 import qualified Hcompta.CLI.Format.Ledger as CLI.Ledger
53 import qualified Hcompta.CLI.Lang as Lang
54 import qualified Hcompta.CLI.Lib.Leijen.Table as Table
55 import qualified Hcompta.CLI.Write as Write
56 import qualified Hcompta.Date as Date
57 import qualified Hcompta.Filter as Filter
58 import qualified Hcompta.Filter.Read as Filter.Read
59 import qualified Hcompta.Format.Ledger as Ledger
60 import qualified Hcompta.Format.Ledger.Journal as Ledger.Journal
61 import qualified Hcompta.Format.Ledger.Read as Ledger.Read
62 import qualified Hcompta.Format.Ledger.Write as Ledger.Write
63 import Hcompta.Lib.Leijen (toDoc, ToDoc(..))
64 import qualified Hcompta.Lib.Leijen as W
65 import Hcompta.Lib.TreeMap (TreeMap)
66 import qualified Hcompta.Lib.TreeMap as TreeMap
67 import qualified Hcompta.Posting as Posting
68 import qualified Hcompta.Tag as Tag
72 { ctx_filter_balance :: Filter.Simplified
74 (Filter.Filter_Balance
75 ((Account, Tag.Tags), Amount.Sum Amount)))
76 , ctx_filter_posting :: Filter.Simplified
78 (Filter.Filter_Posting
79 (Chart, Ledger.Posting)))
80 , ctx_filter_transaction :: Filter.Simplified
82 (Filter.Filter_Transaction
83 (Chart, Ledger.Transaction)))
84 , ctx_heritage :: Bool
85 , ctx_input :: [FilePath]
86 , ctx_output :: [(Write.Mode, FilePath)]
87 , ctx_reduce_date :: Bool
88 , ctx_redundant :: Bool
89 , ctx_total_by_unit :: Bool
90 , ctx_format_output :: Format_Output
91 , ctx_account_equilibrium :: (Account, Account)
96 | Format_Output_Transaction Lang.Exercise_OC
99 nil :: C.Context -> Ctx
102 { ctx_filter_balance = mempty
103 , ctx_filter_posting = mempty
104 , ctx_filter_transaction = mempty
105 , ctx_heritage = True
108 , ctx_reduce_date = True
109 , ctx_redundant = False
110 , ctx_total_by_unit = True
111 , ctx_format_output = Format_Output_Table
112 , ctx_account_equilibrium =
113 let e = C.translate c Lang.Account_Equilibrium
117 usage :: C.Context -> IO String
119 bin <- Env.getProgName
121 [ C.translate c Lang.Section_Description
122 , " "++C.translate c Lang.Help_Command_Balance
124 , C.translate c Lang.Section_Syntax
125 , " "++bin++" balance ["++C.translate c Lang.Type_Option++"] [...]"++
126 " ["++C.translate c Lang.Type_File_Journal++"] [...]"
128 , usageInfo (C.translate c Lang.Section_Options) (options c)
131 options :: C.Context -> Args.Options Ctx
133 [ Option "b" ["filter-balance"]
134 (ReqArg (\s ctx -> do
135 ctx_filter_balance <-
136 liftM ((ctx_filter_balance ctx <>) . Filter.simplify) $
137 liftIO $ Filter.Read.read Filter.Read.filter_balance s
139 Left ko -> Write.fatal c ko
140 Right ok -> return ok
141 return $ ctx{ctx_filter_balance}) $
142 C.translate c Lang.Type_Filter_Balance) $
143 C.translate c Lang.Help_Option_Filter_Balance
144 , Option "p" ["filter-posting"]
145 (ReqArg (\s ctx -> do
146 ctx_filter_posting <-
147 liftM ((ctx_filter_posting ctx <>) . Filter.simplify) $
148 liftIO $ Filter.Read.read Filter.Read.filter_posting s
150 Left ko -> Write.fatal c ko
151 Right ok -> return ok
152 return $ ctx{ctx_filter_posting}) $
153 C.translate c Lang.Type_Filter_Posting) $
154 C.translate c Lang.Help_Option_Filter_Posting
155 , Option "t" ["filter-transaction"]
156 (ReqArg (\s ctx -> do
157 ctx_filter_transaction <-
158 liftM ((ctx_filter_transaction ctx <>) . Filter.simplify) $
159 liftIO $ Filter.Read.read Filter.Read.filter_transaction s
161 Left ko -> Write.fatal c ko
162 Right ok -> return ok
163 return $ ctx{ctx_filter_transaction}) $
164 C.translate c Lang.Type_Filter_Transaction) $
165 C.translate c Lang.Help_Option_Filter_Transaction
166 , Option "h" ["help"]
168 usage c >>= IO.hPutStr IO.stderr
170 C.translate c Lang.Help_Option_Help
171 , Option "i" ["input"]
172 (ReqArg (\s ctx -> do
173 return $ ctx{ctx_input=s:ctx_input ctx}) $
174 C.translate c Lang.Type_File_Journal) $
175 C.translate c Lang.Help_Option_Input
176 , Option "o" ["output"]
177 (ReqArg (\s ctx -> do
178 return $ ctx{ctx_output=(Write.Mode_Append, s):ctx_output ctx}) $
179 C.translate c Lang.Type_File) $
180 C.translate c Lang.Help_Option_Output
181 , Option "O" ["overwrite"]
182 (ReqArg (\s ctx -> do
183 return $ ctx{ctx_output=(Write.Mode_Over, s):ctx_output ctx}) $
184 C.translate c Lang.Type_File) $
185 C.translate c Lang.Help_Option_Overwrite
186 {- NOTE: not used so far.
187 , Option "" ["reduce-date"]
188 (OptArg (\arg ctx -> do
189 ctx_reduce_date <- case arg of
190 Nothing -> return $ True
191 Just "yes" -> return $ True
192 Just "no" -> return $ False
193 Just _ -> Write.fatal c $
194 W.text "--reduce-date option expects \"yes\", or \"no\" as value"
195 return $ ctx{ctx_reduce_date})
197 "use advanced date reducer to speed up filtering"
199 , Option "" ["redundant"]
200 (OptArg (\arg ctx -> do
201 ctx_redundant <- case arg of
202 Nothing -> return $ True
203 Just "yes" -> return $ True
204 Just "no" -> return $ False
205 Just _ -> Write.fatal c Lang.Error_Option_Balance_Redundant
206 return $ ctx{ctx_redundant})
208 C.translate c Lang.Help_Option_Balance_Redundant
209 , Option "" ["heritage"]
210 (OptArg (\arg ctx -> do
211 ctx_heritage <- case arg of
212 Nothing -> return $ True
213 Just "yes" -> return $ True
214 Just "no" -> return $ False
215 Just _ -> Write.fatal c Lang.Error_Option_Balance_Heritage
216 return $ ctx{ctx_heritage})
218 C.translate c Lang.Help_Option_Balance_Heritage
219 , Option "" ["total"]
220 (OptArg (\arg ctx -> do
221 ctx_total_by_unit <- case arg of
222 Nothing -> return $ True
223 Just "yes" -> return $ True
224 Just "no" -> return $ False
225 Just _ -> Write.fatal c Lang.Error_Option_Balance_Total
226 return $ ctx{ctx_total_by_unit})
228 C.translate c Lang.Help_Option_Balance_Total
229 , Option "f" ["format"]
230 (ReqArg (\arg ctx -> do
231 ctx_format_output <- case arg of
232 "table" -> return $ Format_Output_Table
233 "open" -> return $ Format_Output_Transaction Lang.Exercise_Opening
234 "close" -> return $ Format_Output_Transaction Lang.Exercise_Closing
235 _ -> Write.fatal c Lang.Error_Option_Balance_Format
236 return $ ctx{ctx_format_output})
237 "[table|close|open]") $
238 C.translate c Lang.Help_Option_Balance_Format
240 (ReqArg (\arg ctx -> do
241 ctx_account_equilibrium <-
242 fmap (\e -> (e, e)) $
243 case Text.Parsec.runParser
244 (Account.Read.account <* Text.Parsec.eof)
246 Right acct -> return acct
247 _ -> Write.fatal c Lang.Error_Option_Equilibrium
248 return $ ctx{ctx_account_equilibrium}) $
249 C.translate c Lang.Type_Account) $
250 C.translate c Lang.Help_Option_Equilibrium
251 , Option "" ["eq-credit"]
252 (ReqArg (\arg ctx -> do
253 ctx_account_equilibrium <-
254 fmap (\e -> (fst $ ctx_account_equilibrium ctx, e)) $
255 case Text.Parsec.runParser
256 (Account.Read.account <* Text.Parsec.eof)
258 Right acct -> return acct
259 _ -> Write.fatal c Lang.Error_Option_Equilibrium_Credit
260 return $ ctx{ctx_account_equilibrium}) $
261 C.translate c Lang.Type_Account) $
262 C.translate c Lang.Help_Option_Equilibrium_Credit
263 , Option "" ["eq-debit"]
264 (ReqArg (\arg ctx -> do
265 ctx_account_equilibrium <-
266 fmap (\e -> (e, snd $ ctx_account_equilibrium ctx)) $
267 case Text.Parsec.runParser
268 (Account.Read.account <* Text.Parsec.eof)
270 Right acct -> return acct
271 _ -> Write.fatal c Lang.Error_Option_Equilibrium_Debit
272 return $ ctx{ctx_account_equilibrium}) $
273 C.translate c Lang.Type_Account) $
274 C.translate c Lang.Help_Option_Equilibrium_Debit
277 run :: C.Context -> [String] -> IO ()
282 [] -> x{ctx_output=[(Write.Mode_Append, "-")]}
284 Args.parse c usage options (nil c, args)
286 liftM Data.Either.partitionEithers $ do
287 CLI.Ledger.paths c $ ctx_input ctx ++ inputs
290 liftIO $ runExceptT $ Ledger.Read.file
291 (Ledger.Read.context ( ctx_filter_transaction ctx
292 , ctx_filter_posting ctx )
296 Left ko -> return $ Left (path, ko)
297 Right ok -> return $ Right ok
298 case read_journals of
299 (errs@(_:_), _journals) ->
300 forM_ errs $ \(_path, err) -> do
303 Write.debug c $ "filter: transaction: " ++ show (ctx_filter_transaction ctx)
304 Write.debug c $ "filter: posting: " ++ show (ctx_filter_posting ctx)
305 Write.debug c $ "filter: balance: " ++ show (ctx_filter_balance ctx)
307 case ctx_format_output ctx of
308 Format_Output_Transaction oc -> do
309 now <- liftM (\d -> d{Time.utctDayTime=0}) $ Date.now
310 let sty = Write.style
311 { Write.style_pretty = True -- ctx_align ctx
313 Write.write c sty (ctx_output ctx) $ do
314 Ledger.Write.transactions $ do
315 let (chart, bal) = ledger_balance_by_account ctx journals
316 let balance_by_account = ledger_balance_by_account_filter ctx (chart, bal)
317 let Balance.Balance_by_Unit balance_by_unit =
318 ledger_balance_by_unit ctx balance_by_account
319 let equilibrium_postings =
320 Data.Map.foldlWithKey
324 Lang.Exercise_Closing -> id
325 Lang.Exercise_Opening -> negate) $
327 Balance.unit_sum_amount bu in
328 case Amount.sign amt of
330 (Ledger.posting $ snd $ ctx_account_equilibrium ctx)
331 { Ledger.posting_amounts = Data.Map.singleton unit amt
332 , Ledger.posting_comments = [ Lang.translate (C.lang c) Lang.Comment_Equilibrium ]
336 (Ledger.posting $ fst $ ctx_account_equilibrium ctx)
337 { Ledger.posting_amounts = Data.Map.singleton unit amt
338 , Ledger.posting_comments = [ Lang.translate (C.lang c) Lang.Comment_Equilibrium ]
344 { Ledger.transaction_description=
345 Lang.translate lang (Lang.Description_Exercise oc)
346 , Ledger.transaction_dates=(now, [])
347 , Ledger.transaction_postings=
348 Data.Map.unionWith mappend
349 (Ledger.posting_by_Account equilibrium_postings)
350 (TreeMap.flatten_with_Path
351 (\posting_account (Balance.Account_Sum amount_by_unit) ->
352 [(Ledger.posting posting_account)
353 { Ledger.posting_amounts =
354 flip fmap amount_by_unit $
356 Lang.Exercise_Closing -> negate
357 Lang.Exercise_Opening -> id)
365 Format_Output_Table -> do
366 let ( table_balance_by_account
367 , Balance.Balance_by_Unit balance_by_unit
369 let (ch, bal) = ledger_balance_by_account ctx journals in
372 let balance_filtered = ledger_balance_by_account_expanded ctx ch bal in
373 ( table_by_account ctx Balance.inclusive balance_filtered
374 , ledger_balance_by_unit_expanded ctx balance_filtered
377 let balance_filtered = ledger_balance_by_account_filter ctx (ch, bal) in
378 ( table_by_account ctx id balance_filtered
379 , ledger_balance_by_unit ctx balance_filtered
381 let sty = Write.style { Write.style_pretty = True }
382 Write.write c sty (ctx_output ctx) $ do
385 [ Table.column (Lang.translate lang Lang.Title_Debit) Table.Align_Right
386 , Table.column (Lang.translate lang Lang.Title_Credit) Table.Align_Right
387 , Table.column (Lang.translate lang Lang.Title_Balance) Table.Align_Right
388 , Table.column (Lang.translate lang Lang.Title_Account) Table.Align_Left
390 table_balance_by_account $ do
391 case ctx_total_by_unit ctx of
395 [ Table.Cell_Line '=' 0
396 , Table.Cell_Line '=' 0
397 , Table.Cell_Line '=' 0
398 , Table.Cell_Line ' ' 0
400 flip table_by_unit (repeat []) $
402 Balance.unit_sum_amount
405 ledger_balance_by_account
407 -> [ Ledger.Journal (Const (Balance.Balance_by_Account (Amount.Sum Amount))
408 (Chart, Ledger.Transaction)) ]
409 -> (Chart, Balance.Balance_by_Account (Amount.Sum Amount))
410 ledger_balance_by_account _ctx =
414 (Ledger.journal_chart j,) $
417 { Ledger.journal_sections=Const b
423 ledger_balance_by_account_filter
425 -> (Chart, Balance.Balance_by_Account (Amount.Sum Amount))
426 -> Balance.Balance_by_Account (Amount.Sum Amount)
427 ledger_balance_by_account_filter ctx (chart, balance) =
428 case Filter.simplified $ ctx_filter_balance ctx of
432 else TreeMap.filter_with_Path_and_Node (const . is_worth) balance
433 Right False -> mempty
435 TreeMap.filter_with_Path_and_Node
437 (is_worth node bal &&) $
439 (Filter.test flt . ((acct, Chart.account_tags acct chart),)) $
440 Balance.get_Account_Sum bal)
444 let _descendants = TreeMap.nodes
445 (TreeMap.node_descendants node) in
447 -- NOTE: worth if no descendant
448 -- but Account's exclusive
449 -- has at least a non-zero Amount
451 (not . Amount.is_zero . Amount.sum_balance)
452 (Balance.get_Account_Sum bal)
454 ledger_balance_by_account_expanded
457 -> Balance.Balance_by_Account (Amount.Sum Amount)
458 -> Balance.Expanded (Amount.Sum Amount)
459 ledger_balance_by_account_expanded ctx chart =
460 case Filter.simplified $ ctx_filter_balance ctx of
464 else TreeMap.filter_with_Path_and_Node (const . is_worth)
465 Right False -> const mempty
467 TreeMap.filter_with_Path_and_Node
469 (is_worth node bal &&) $
471 (Filter.test flt . ((acct, Chart.account_tags acct chart),)) $
472 Balance.get_Account_Sum $
473 Balance.inclusive bal)
477 let descendants = TreeMap.nodes
478 (TreeMap.node_descendants node) in
480 -- NOTE: worth if no descendant
481 -- but Account's inclusive
482 -- has at least a non-zero Amount
483 || (Data.Map.null descendants
485 (not . Amount.is_zero . Amount.sum_balance)
486 (Balance.get_Account_Sum $ Balance.inclusive bal))
487 -- NOTE: worth if Account's exclusive
488 -- has at least a non-zero Amount
489 || (Data.Foldable.any
490 (not . Amount.is_zero . Amount.sum_balance)
491 (Balance.get_Account_Sum $ Balance.exclusive bal))
492 -- NOTE: worth if Account has at least more than
493 -- one descendant Account whose inclusive
494 -- has at least a non-zero Amount
499 (not . Amount.is_zero . Amount.sum_balance)
500 . Balance.get_Account_Sum
501 . Balance.inclusive )
502 . TreeMap.node_value )
506 ledger_balance_by_unit
508 -> Balance.Balance_by_Account (Amount.Sum Amount)
509 -> Balance.Balance_by_Unit (Amount.Sum Amount)
510 ledger_balance_by_unit _ctx =
511 flip Balance.by_unit_of_by_account mempty
513 ledger_balance_by_unit_expanded
515 -> Balance.Expanded (Amount.Sum Amount)
516 -> Balance.Balance_by_Unit (Amount.Sum Amount)
517 ledger_balance_by_unit_expanded _ctx =
518 flip Balance.by_unit_of_expanded mempty
522 -> (amount -> Balance.Account_Sum (Amount.Sum Amount))
523 -> TreeMap Account.Account_Section amount
526 table_by_account _ctx get_Account_Sum =
527 let posting_type = Posting.Posting_Type_Regular in
528 flip $ TreeMap.foldr_with_Path
529 (\account balance rows ->
531 (\(amount_positive, amount_negative, amount) ->
534 { Table.cell_content = maybe W.empty Amount.Write.amount amount_positive
535 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_positive
538 { Table.cell_content = maybe W.empty Amount.Write.amount amount_negative
539 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_negative
542 { Table.cell_content = Amount.Write.amount $ amount
543 , Table.cell_width = Amount.Write.amount_length $ amount
546 { Table.cell_content = Ledger.Write.account posting_type account
547 , Table.cell_width = Ledger.Write.account_length posting_type account
552 let bal = Balance.get_Account_Sum $ get_Account_Sum balance in
553 Data.Map.foldrWithKey
555 ( maybe Nothing Amount.sum_positive $ Data.Map.lookup unit $ bal
556 , maybe Nothing Amount.sum_negative $ Data.Map.lookup unit $ bal
557 , Amount.sum_balance amount
563 :: Data.Map.Map Unit (Amount.Sum Amount)
570 [ let amt = Amount.sum_positive amount_sum in
572 { Table.cell_content = maybe W.empty Amount.Write.amount amt
573 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
575 , let amt = Amount.sum_negative amount_sum in
577 { Table.cell_content = maybe W.empty Amount.Write.amount amt
578 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
580 , let amt = Amount.sum_balance amount_sum in
582 { Table.cell_content = Amount.Write.amount amt
583 , Table.cell_width = Amount.Write.amount_length amt
586 { Table.cell_content = W.empty
587 , Table.cell_width = 0