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.Monad (Monad(..), forM_, liftM, mapM)
11 import Control.Monad.IO.Class (liftIO)
12 import Control.Monad.Trans.Except (runExceptT)
14 import Data.Either (Either(..), partitionEithers)
15 import Data.Eq (Eq(..))
16 import Data.Foldable (Foldable(..), any)
17 import Data.Functor (Functor(..))
18 import Data.List ((++), repeat, replicate)
19 import qualified Data.Map.Strict as Data.Map
20 import Data.Maybe (Maybe(..), maybe)
21 import Data.Monoid (Monoid(..), (<>))
22 import Data.Ord (Ord(..))
23 import qualified Data.Strict.Maybe as Strict
24 import Data.String (String)
25 import qualified Data.Text.Lazy as TL
26 import qualified Data.Time.Clock as Time
27 import Prelude (($), (.), Bounded(..), FilePath, IO, Num(..), const, id, flip, unlines, zipWith)
28 import qualified Text.Parsec
29 import Text.Show (Show(..))
30 import System.Console.GetOpt
35 import System.Environment as Env (getProgName)
36 import System.Exit (exitSuccess)
37 import qualified System.IO as IO
39 import Hcompta.Account (Account)
40 import qualified Hcompta.Account as Account
41 import qualified Hcompta.Account.Read as Account.Read
42 import Hcompta.Amount (Amount)
43 import qualified Hcompta.Amount as Amount
44 import Hcompta.Amount.Unit (Unit)
45 import qualified Hcompta.Amount.Write as Amount.Write
46 import qualified Hcompta.Balance as Balance
47 import Hcompta.Chart (Chart)
48 import qualified Hcompta.Chart as Chart
49 import qualified Hcompta.CLI.Args as Args
50 import Hcompta.CLI.Context (Context)
51 import qualified Hcompta.CLI.Context as Context
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_reduce_date :: Bool
87 , ctx_redundant :: Bool
88 , ctx_total_by_unit :: Bool
89 , ctx_format_output :: Format_Output
90 , ctx_account_equilibrium :: Account
95 | Format_Output_Transaction { negate_transaction :: Bool }
101 { ctx_filter_balance = mempty
102 , ctx_filter_posting = mempty
103 , ctx_filter_transaction = mempty
104 , ctx_heritage = True
106 , ctx_reduce_date = True
107 , ctx_redundant = False
108 , ctx_total_by_unit = True
109 , ctx_format_output = Format_Output_Table
110 , ctx_account_equilibrium = Account.account
111 (TL.toStrict $ W.displayT $ W.renderOneLine False $
112 toDoc (Context.lang context) Lang.Message_Equilibrium)
118 bin <- Env.getProgName
119 let pad = replicate (length bin) ' '
122 , " "++bin++" balance [-i FILE_JOURNAL]"
123 , " "++pad++" [-b FILTER_BALANCE]"
124 , " "++pad++" [-p FILTER_POSTING]"
125 , " "++pad++" [-t FILTER_TRANSACTION]"
126 , " "++pad++" [FILE_JOURNAL] [...]"
128 , usageInfo "OPTIONS" options
131 options :: Args.Options Ctx
133 [ Option "b" ["filter-balance"]
134 (ReqArg (\s context 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 context $ ko
140 Right ok -> return ok
141 return $ ctx{ctx_filter_balance}) "FILTER_BALANCE")
142 "filter at balance level, multiple uses are merged with a logical AND"
143 , Option "p" ["filter-posting"]
144 (ReqArg (\s context ctx -> do
145 ctx_filter_posting <-
146 liftM ((ctx_filter_posting ctx <>) . Filter.simplify) $
147 liftIO $ Filter.Read.read Filter.Read.filter_posting s
149 Left ko -> Write.fatal context $ ko
150 Right ok -> return ok
151 return $ ctx{ctx_filter_posting}) "FILTER_POSTING")
152 "filter at posting level, multiple uses are merged with a logical AND"
153 , Option "t" ["filter-transaction"]
154 (ReqArg (\s context ctx -> do
155 ctx_filter_transaction <-
156 liftM ((ctx_filter_transaction ctx <>) . Filter.simplify) $
157 liftIO $ Filter.Read.read Filter.Read.filter_transaction s
159 Left ko -> Write.fatal context $ ko
160 Right ok -> return ok
161 return $ ctx{ctx_filter_transaction}) "FILTER_TRANSACTION")
162 "filter at transaction level, multiple uses are merged with a logical AND"
163 , Option "h" ["help"]
164 (NoArg (\_context _ctx -> do
165 usage >>= IO.hPutStr IO.stderr
168 , Option "i" ["input"]
169 (ReqArg (\s _context ctx -> do
170 return $ ctx{ctx_input=s:ctx_input ctx}) "FILE_JOURNAL")
171 "read data from given file, multiple uses merge the data as would a concatenation do"
172 {- NOTE: not used so far.
173 , Option "" ["reduce-date"]
174 (OptArg (\arg context ctx -> do
175 ctx_reduce_date <- case arg of
176 Nothing -> return $ True
177 Just "yes" -> return $ True
178 Just "no" -> return $ False
179 Just _ -> Write.fatal context $
180 W.text "--reduce-date option expects \"yes\", or \"no\" as value"
181 return $ ctx{ctx_reduce_date})
183 "use advanced date reducer to speed up filtering"
185 , Option "" ["redundant"]
186 (OptArg (\arg context ctx -> do
187 ctx_redundant <- case arg of
188 Nothing -> return $ True
189 Just "yes" -> return $ True
190 Just "no" -> return $ False
191 Just _ -> Write.fatal context $
192 W.text "--redundant option expects \"yes\", or \"no\" as value"
193 return $ ctx{ctx_redundant})
195 "also print accounts with zero amount or the same amounts than its ascending account"
196 , Option "" ["heritage"]
197 (OptArg (\arg context ctx -> do
198 ctx_heritage <- case arg of
199 Nothing -> return $ True
200 Just "yes" -> return $ True
201 Just "no" -> return $ False
202 Just _ -> Write.fatal context $
203 W.text "--heritage option expects \"yes\", or \"no\" as value"
204 return $ ctx{ctx_heritage})
206 "propagate amounts to ascending accounts"
207 , Option "" ["total"]
208 (OptArg (\arg context ctx -> do
209 ctx_total_by_unit <- case arg of
210 Nothing -> return $ True
211 Just "yes" -> return $ True
212 Just "no" -> return $ False
213 Just _ -> Write.fatal context $
214 W.text "--total option expects \"yes\", or \"no\" as value"
215 return $ ctx{ctx_total_by_unit})
217 "calculate totals by unit"
218 , Option "f" ["format"]
219 (ReqArg (\arg context ctx -> do
220 ctx_format_output <- case arg of
221 "table" -> return $ Format_Output_Table
222 "open" -> return $ Format_Output_Transaction False
223 "close" -> return $ Format_Output_Transaction True
224 _ -> Write.fatal context $
225 W.text "--format option expects \"close\", \"open\", or \"table\" as value"
226 return $ ctx{ctx_format_output})
227 "[close|open|table]")
228 "select output format"
229 , Option "" ["equilibrium"]
230 (ReqArg (\arg context ctx -> do
231 ctx_account_equilibrium <-
232 case Text.Parsec.runParser
233 (Account.Read.account <* Text.Parsec.eof)
235 Right acct -> return acct
236 _ -> Write.fatal context $
237 W.text "--equilibrium option expects a valid account name"
238 return $ ctx{ctx_account_equilibrium})
240 "specify account equilibrating a close or open balance"
243 run :: Context.Context -> [String] -> IO ()
244 run context args = do
245 (ctx, inputs) <- Args.parse context usage options (nil context, args)
247 liftM Data.Either.partitionEithers $ do
248 CLI.Ledger.paths context $ ctx_input ctx ++ inputs
251 liftIO $ runExceptT $ Ledger.Read.file
252 (Ledger.Read.context ( ctx_filter_transaction ctx
253 , ctx_filter_posting ctx )
257 Left ko -> return $ Left (path, ko)
258 Right ok -> return $ Right ok
259 case read_journals of
260 (errs@(_:_), _journals) ->
261 forM_ errs $ \(_path, err) -> do
262 Write.fatal context $ err
264 Write.debug context $ "filter: transaction: " ++ show (ctx_filter_transaction ctx)
265 Write.debug context $ "filter: posting: " ++ show (ctx_filter_posting ctx)
266 Write.debug context $ "filter: balance: " ++ show (ctx_filter_balance ctx)
267 style_color <- Write.with_color context IO.stdout
268 case ctx_format_output ctx of
269 Format_Output_Transaction nt -> do
270 let balance_by_account =
271 ledger_balance_by_account_filter ctx $
272 ledger_balance_by_account ctx journals
273 let Balance.Balance_by_Unit balance_by_unit =
274 ledger_balance_by_unit ctx balance_by_account
275 let posting_equilibrium =
276 (Ledger.posting $ ctx_account_equilibrium ctx)
277 { Ledger.posting_amounts =
278 flip Data.Map.map balance_by_unit $
279 (if nt then id else negate)
281 . Balance.unit_sum_amount
282 , Ledger.posting_comments=
283 [ TL.toStrict $ W.displayT $ W.renderOneLine False $
284 toDoc (Context.lang context) $
285 Lang.Message_Equilibrium_posting
288 now <- liftM (\d -> d{Time.utctDayTime=0}) $ Date.now
291 { Ledger.transaction_description=
292 TL.toStrict $ W.displayT $ W.renderOneLine False $
293 toDoc (Context.lang context) $
294 Lang.Message_Balance_Description nt
295 , Ledger.transaction_dates=(now, [])
296 , Ledger.transaction_postings=
297 (if Data.Map.null $ Ledger.posting_amounts posting_equilibrium
300 Data.Map.insertWith (++)
301 (ctx_account_equilibrium ctx)
302 [posting_equilibrium]) $
303 TreeMap.flatten_with_Path
304 (\posting_account (Balance.Account_Sum amount_by_unit) ->
305 [(Ledger.posting posting_account)
306 { Ledger.posting_amounts =
307 flip fmap amount_by_unit $
308 (if nt then negate else id)
315 let sty = Ledger.Write.Style
316 { Ledger.Write.style_align = True -- ctx_align ctx
317 , Ledger.Write.style_color
319 Ledger.Write.put sty IO.stdout $ do
320 Ledger.Write.transaction transaction
321 Format_Output_Table -> do
322 let ( table_balance_by_account
323 , Balance.Balance_by_Unit balance_by_unit
325 let (ch, bal) = ledger_balance_by_account ctx journals in
328 let balance_filtered = ledger_balance_by_account_expanded ctx ch bal in
329 ( table_by_account ctx Balance.inclusive balance_filtered
330 , ledger_balance_by_unit_expanded ctx balance_filtered
333 let balance_filtered = ledger_balance_by_account_filter ctx (ch, bal) in
334 ( table_by_account ctx id balance_filtered
335 , ledger_balance_by_unit ctx balance_filtered
337 W.displayIO IO.stdout $ do
338 W.renderPretty style_color 1.0 maxBound $ do
341 TL.toStrict . W.displayT .
342 W.renderCompact False .
343 toDoc (Context.lang context)
345 [ Table.column (title Lang.Message_Debit) Table.Align_Right
346 , Table.column (title Lang.Message_Credit) Table.Align_Right
347 , Table.column (title Lang.Message_Balance) Table.Align_Right
348 , Table.column (title Lang.Message_Account) Table.Align_Left
350 table_balance_by_account $ do
351 case ctx_total_by_unit ctx of
355 [ Table.Cell_Line '=' 0
356 , Table.Cell_Line '=' 0
357 , Table.Cell_Line '=' 0
358 , Table.Cell_Line ' ' 0
360 flip table_by_unit (repeat []) $
362 Balance.unit_sum_amount
365 ledger_balance_by_account
367 -> [ Ledger.Journal (Const (Balance.Balance_by_Account (Amount.Sum Amount))
368 (Chart, Ledger.Transaction)) ]
369 -> (Chart, Balance.Balance_by_Account (Amount.Sum Amount))
370 ledger_balance_by_account _ctx =
374 (Ledger.journal_chart j,) $
377 { Ledger.journal_sections=Const b
383 ledger_balance_by_account_filter
385 -> (Chart, Balance.Balance_by_Account (Amount.Sum Amount))
386 -> Balance.Balance_by_Account (Amount.Sum Amount)
387 ledger_balance_by_account_filter ctx (chart, balance) =
388 case Filter.simplified $ ctx_filter_balance ctx of
392 else TreeMap.filter_with_Path_and_Node (const . is_worth) balance
393 Right False -> mempty
395 TreeMap.filter_with_Path_and_Node
397 (is_worth node bal &&) $
399 (Filter.test flt . ((acct, Chart.account_tags acct chart),)) $
400 Balance.get_Account_Sum bal)
404 let _descendants = TreeMap.nodes
405 (TreeMap.node_descendants node) in
407 -- NOTE: worth if no descendant
408 -- but Account's exclusive
409 -- has at least a non-zero Amount
411 (not . Amount.is_zero . Amount.sum_balance)
412 (Balance.get_Account_Sum bal)
414 ledger_balance_by_account_expanded
417 -> Balance.Balance_by_Account (Amount.Sum Amount)
418 -> Balance.Expanded (Amount.Sum Amount)
419 ledger_balance_by_account_expanded ctx chart =
420 case Filter.simplified $ ctx_filter_balance ctx of
424 else TreeMap.filter_with_Path_and_Node (const . is_worth)
425 Right False -> const mempty
427 TreeMap.filter_with_Path_and_Node
429 (is_worth node bal &&) $
431 (Filter.test flt . ((acct, Chart.account_tags acct chart),)) $
432 Balance.get_Account_Sum $
433 Balance.inclusive bal)
437 let descendants = TreeMap.nodes
438 (TreeMap.node_descendants node) in
440 -- NOTE: worth if no descendant
441 -- but Account's inclusive
442 -- has at least a non-zero Amount
443 || (Data.Map.null descendants
445 (not . Amount.is_zero . Amount.sum_balance)
446 (Balance.get_Account_Sum $ Balance.inclusive bal))
447 -- NOTE: worth if Account's exclusive
448 -- has at least a non-zero Amount
449 || (Data.Foldable.any
450 (not . Amount.is_zero . Amount.sum_balance)
451 (Balance.get_Account_Sum $ Balance.exclusive bal))
452 -- NOTE: worth if Account has at least more than
453 -- one descendant Account whose inclusive
454 -- has at least a non-zero Amount
459 (not . Amount.is_zero . Amount.sum_balance)
460 . Balance.get_Account_Sum
461 . Balance.inclusive )
462 . TreeMap.node_value )
466 ledger_balance_by_unit
468 -> Balance.Balance_by_Account (Amount.Sum Amount)
469 -> Balance.Balance_by_Unit (Amount.Sum Amount)
470 ledger_balance_by_unit _ctx =
471 flip Balance.by_unit_of_by_account mempty
473 ledger_balance_by_unit_expanded
475 -> Balance.Expanded (Amount.Sum Amount)
476 -> Balance.Balance_by_Unit (Amount.Sum Amount)
477 ledger_balance_by_unit_expanded _ctx =
478 flip Balance.by_unit_of_expanded mempty
482 -> (amount -> Balance.Account_Sum (Amount.Sum Amount))
483 -> TreeMap Account.Account_Section amount
486 table_by_account _ctx get_Account_Sum =
487 let posting_type = Posting.Posting_Type_Regular in
488 flip $ TreeMap.foldr_with_Path
489 (\account balance rows ->
491 (\(amount_positive, amount_negative, amount) ->
494 { Table.cell_content = maybe W.empty Amount.Write.amount amount_positive
495 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_positive
498 { Table.cell_content = maybe W.empty Amount.Write.amount amount_negative
499 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_negative
502 { Table.cell_content = Amount.Write.amount $ amount
503 , Table.cell_width = Amount.Write.amount_length $ amount
506 { Table.cell_content = Ledger.Write.account posting_type account
507 , Table.cell_width = Ledger.Write.account_length posting_type account
512 let bal = Balance.get_Account_Sum $ get_Account_Sum balance in
513 Data.Map.foldrWithKey
515 ( maybe Nothing Amount.sum_positive $ Data.Map.lookup unit $ bal
516 , maybe Nothing Amount.sum_negative $ Data.Map.lookup unit $ bal
517 , Amount.sum_balance amount
523 :: Data.Map.Map Unit (Amount.Sum Amount)
530 [ let amt = Amount.sum_positive amount_sum in
532 { Table.cell_content = maybe W.empty Amount.Write.amount amt
533 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
535 , let amt = Amount.sum_negative amount_sum in
537 { Table.cell_content = maybe W.empty Amount.Write.amount amt
538 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
540 , let amt = Amount.sum_balance amount_sum in
542 { Table.cell_content = Amount.Write.amount amt
543 , Table.cell_width = Amount.Write.amount_length amt
546 { Table.cell_content = W.empty
547 , Table.cell_width = 0