]> Git — Sourcephile - comptalang.git/blob - cli/Hcompta/CLI/Command/Balance.hs
Ajout : Hcompta.Chart.
[comptalang.git] / cli / Hcompta / CLI / Command / Balance.hs
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
8
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)
13 import Data.Bool
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
31 ( ArgDescr(..)
32 , OptDescr(..)
33 , usageInfo
34 )
35 import System.Environment as Env (getProgName)
36 import System.Exit (exitSuccess)
37 import qualified System.IO as IO
38
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
69
70 data Ctx
71 = Ctx
72 { ctx_filter_balance :: Filter.Simplified
73 (Filter.Filter_Bool
74 (Filter.Filter_Balance
75 ((Account, Tag.Tags), Amount.Sum Amount)))
76 , ctx_filter_posting :: Filter.Simplified
77 (Filter.Filter_Bool
78 (Filter.Filter_Posting
79 (Chart, Ledger.Posting)))
80 , ctx_filter_transaction :: Filter.Simplified
81 (Filter.Filter_Bool
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
91 } deriving (Show)
92
93 data Format_Output
94 = Format_Output_Table
95 | Format_Output_Transaction { negate_transaction :: Bool }
96 deriving (Eq, Show)
97
98 nil :: Context -> Ctx
99 nil context =
100 Ctx
101 { ctx_filter_balance = mempty
102 , ctx_filter_posting = mempty
103 , ctx_filter_transaction = mempty
104 , ctx_heritage = True
105 , ctx_input = []
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)
113 []
114 }
115
116 usage :: IO String
117 usage = do
118 bin <- Env.getProgName
119 let pad = replicate (length bin) ' '
120 return $ unlines $
121 [ "SYNTAX "
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] [...]"
127 , ""
128 , usageInfo "OPTIONS" options
129 ]
130
131 options :: Args.Options Ctx
132 options =
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
138 >>= \f -> case f of
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
148 >>= \f -> case f of
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
158 >>= \f -> case f of
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
166 exitSuccess))
167 "show this help"
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})
182 "[yes|no]")
183 "use advanced date reducer to speed up filtering"
184 -}
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})
194 "[yes|no]")
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})
205 "[yes|no]")
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})
216 "[yes|no]")
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)
234 () "" arg of
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})
239 "ACCOUNT")
240 "specify account equilibrating a close or open balance"
241 ]
242
243 run :: Context.Context -> [String] -> IO ()
244 run context args = do
245 (ctx, inputs) <- Args.parse context usage options (nil context, args)
246 read_journals <-
247 liftM Data.Either.partitionEithers $ do
248 CLI.Ledger.paths context $ ctx_input ctx ++ inputs
249 >>= do
250 mapM $ \path -> do
251 liftIO $ runExceptT $ Ledger.Read.file
252 (Ledger.Read.context ( ctx_filter_transaction ctx
253 , ctx_filter_posting ctx )
254 Ledger.journal)
255 path
256 >>= \x -> case x of
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
263 ([], journals) -> do
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)
280 . Amount.sum_balance
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
286 ]
287 }
288 now <- liftM (\d -> d{Time.utctDayTime=0}) $ Date.now
289 let transaction =
290 Ledger.transaction
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
298 then id
299 else
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)
309 . Amount.sum_balance
310 }
311 ]
312 )
313 balance_by_account
314 }
315 let sty = Ledger.Write.Style
316 { Ledger.Write.style_align = True -- ctx_align ctx
317 , Ledger.Write.style_color
318 }
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
324 ) =
325 let (ch, bal) = ledger_balance_by_account ctx journals in
326 if ctx_heritage ctx
327 then
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
331 )
332 else
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
336 )
337 W.displayIO IO.stdout $ do
338 W.renderPretty style_color 1.0 maxBound $ do
339 toDoc () $ do
340 let title =
341 TL.toStrict . W.displayT .
342 W.renderCompact False .
343 toDoc (Context.lang context)
344 zipWith id
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
349 ] $ do
350 table_balance_by_account $ do
351 case ctx_total_by_unit ctx of
352 False -> repeat []
353 True -> do
354 zipWith (:)
355 [ Table.Cell_Line '=' 0
356 , Table.Cell_Line '=' 0
357 , Table.Cell_Line '=' 0
358 , Table.Cell_Line ' ' 0
359 ] $ do
360 flip table_by_unit (repeat []) $
361 Data.Map.map
362 Balance.unit_sum_amount
363 balance_by_unit
364
365 ledger_balance_by_account
366 :: Ctx
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 =
371 Data.Foldable.foldl'
372 (flip (\j ->
373 flip mappend $
374 (Ledger.journal_chart j,) $
375 Ledger.Journal.fold
376 (\Ledger.Journal
377 { Ledger.journal_sections=Const b
378 } -> mappend b
379 ) j mempty
380 ))
381 mempty
382
383 ledger_balance_by_account_filter
384 :: Ctx
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
389 Right True ->
390 if ctx_redundant ctx
391 then balance
392 else TreeMap.filter_with_Path_and_Node (const . is_worth) balance
393 Right False -> mempty
394 Left flt ->
395 TreeMap.filter_with_Path_and_Node
396 (\node acct bal ->
397 (is_worth node bal &&) $
398 Data.Foldable.any
399 (Filter.test flt . ((acct, Chart.account_tags acct chart),)) $
400 Balance.get_Account_Sum bal)
401 balance
402 where
403 is_worth node bal =
404 let _descendants = TreeMap.nodes
405 (TreeMap.node_descendants node) in
406 ctx_redundant ctx
407 -- NOTE: worth if no descendant
408 -- but Account's exclusive
409 -- has at least a non-zero Amount
410 || Data.Foldable.any
411 (not . Amount.is_zero . Amount.sum_balance)
412 (Balance.get_Account_Sum bal)
413
414 ledger_balance_by_account_expanded
415 :: Ctx
416 -> Chart
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
421 Right True ->
422 if ctx_redundant ctx
423 then id
424 else TreeMap.filter_with_Path_and_Node (const . is_worth)
425 Right False -> const mempty
426 Left flt ->
427 TreeMap.filter_with_Path_and_Node
428 (\node acct bal ->
429 (is_worth node bal &&) $
430 Data.Foldable.any
431 (Filter.test flt . ((acct, Chart.account_tags acct chart),)) $
432 Balance.get_Account_Sum $
433 Balance.inclusive bal)
434 . Balance.expanded
435 where
436 is_worth node bal =
437 let descendants = TreeMap.nodes
438 (TreeMap.node_descendants node) in
439 ctx_redundant ctx
440 -- NOTE: worth if no descendant
441 -- but Account's inclusive
442 -- has at least a non-zero Amount
443 || (Data.Map.null descendants
444 && Data.Foldable.any
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
455 || Data.Map.size
456 ( Data.Map.filter
457 ( Strict.maybe False
458 ( Data.Foldable.any
459 (not . Amount.is_zero . Amount.sum_balance)
460 . Balance.get_Account_Sum
461 . Balance.inclusive )
462 . TreeMap.node_value )
463 descendants
464 ) > 1
465
466 ledger_balance_by_unit
467 :: Ctx
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
472
473 ledger_balance_by_unit_expanded
474 :: Ctx
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
479
480 table_by_account
481 :: Ctx
482 -> (amount -> Balance.Account_Sum (Amount.Sum Amount))
483 -> TreeMap Account.Account_Section amount
484 -> [[Table.Cell]]
485 -> [[Table.Cell]]
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 ->
490 foldr
491 (\(amount_positive, amount_negative, amount) ->
492 zipWith (:)
493 [ Table.cell
494 { Table.cell_content = maybe W.empty Amount.Write.amount amount_positive
495 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_positive
496 }
497 , Table.cell
498 { Table.cell_content = maybe W.empty Amount.Write.amount amount_negative
499 , Table.cell_width = maybe 0 Amount.Write.amount_length amount_negative
500 }
501 , Table.cell
502 { Table.cell_content = Amount.Write.amount $ amount
503 , Table.cell_width = Amount.Write.amount_length $ amount
504 }
505 , Table.cell
506 { Table.cell_content = Ledger.Write.account posting_type account
507 , Table.cell_width = Ledger.Write.account_length posting_type account
508 }
509 ]
510 )
511 rows $
512 let bal = Balance.get_Account_Sum $ get_Account_Sum balance in
513 Data.Map.foldrWithKey
514 (\unit amount acc ->
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
518 ) : acc
519 ) [] $ bal
520 )
521
522 table_by_unit
523 :: Data.Map.Map Unit (Amount.Sum Amount)
524 -> [[Table.Cell]]
525 -> [[Table.Cell]]
526 table_by_unit =
527 flip $ foldr
528 (\amount_sum ->
529 zipWith (:)
530 [ let amt = Amount.sum_positive amount_sum in
531 Table.cell
532 { Table.cell_content = maybe W.empty Amount.Write.amount amt
533 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
534 }
535 , let amt = Amount.sum_negative amount_sum in
536 Table.cell
537 { Table.cell_content = maybe W.empty Amount.Write.amount amt
538 , Table.cell_width = maybe 0 Amount.Write.amount_length amt
539 }
540 , let amt = Amount.sum_balance amount_sum in
541 Table.cell
542 { Table.cell_content = Amount.Write.amount amt
543 , Table.cell_width = Amount.Write.amount_length amt
544 }
545 , Table.cell
546 { Table.cell_content = W.empty
547 , Table.cell_width = 0
548 }
549 ]
550 )