Modif : Balance : inutile de mettre amount_sum_balance dans Amount_Sum.
[comptalang.git] / cli / Hcompta / CLI / Command / Balance.hs
index d5ef16137d0eee2f54e8942b26d1efb60c1befb0..1cf915c446f766f715db5c67b701bc5fe0b035fc 100644 (file)
@@ -1,14 +1,20 @@
 {-# LANGUAGE NamedFieldPuns #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections #-}
 module Hcompta.CLI.Command.Balance where
 
+import           Prelude hiding (foldr)
+-- import           Control.Monad ((>=>))
+import           Control.Applicative ((<$>))
 import           Control.Monad.IO.Class (liftIO)
 import           Control.Monad.Trans.Except (runExceptT)
 import qualified Data.Either
 import qualified Data.Foldable
+import           Data.Foldable (foldr)
 import qualified Data.List
-import qualified Data.Map
+import qualified Data.Map.Strict as Data.Map
+-- import           Data.Map.Strict (Map)
 import qualified Data.Text.Lazy as TL
 import           System.Console.GetOpt
                  ( ArgDescr(..)
@@ -18,34 +24,46 @@ import           System.Console.GetOpt
 import           System.Environment as Env (getProgName)
 import           System.Exit (exitWith, ExitCode(..))
 import qualified System.IO as IO
-import           Text.Show.Pretty (ppShow) -- TODO: may be not necessary
+-- import           Text.Show.Pretty (ppShow)
 
+import qualified Hcompta.Balance as Balance
 import qualified Hcompta.CLI.Args as Args
 import qualified Hcompta.CLI.Context as Context
 import qualified Hcompta.CLI.Format.Ledger as CLI.Ledger
+import qualified Hcompta.CLI.Lang as Lang
+import qualified Hcompta.CLI.Lib.Leijen.Table as Table
 import qualified Hcompta.CLI.Write as Write
-import qualified Hcompta.Calc.Balance as Balance
-import qualified Hcompta.Format.Ledger.Journal
+import qualified Hcompta.Format.Ledger as Ledger
+import qualified Hcompta.Format.Ledger.Journal as Ledger.Journal
 import qualified Hcompta.Format.Ledger.Read as Ledger.Read
 import qualified Hcompta.Format.Ledger.Write as Ledger.Write
 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
+-- import qualified Hcompta.Lib.Foldable as Lib.Foldable
 import qualified Hcompta.Lib.Leijen as W
-import           Hcompta.Lib.Leijen ((<>))
-import qualified Hcompta.Model.Amount as Amount
-import qualified Hcompta.Model.Transaction.Posting as Posting
--- import qualified Hcompta.Format.Ledger.Write
+import           Hcompta.Lib.Leijen (toDoc, ToDoc(..))
+-- import qualified Hcompta.Account as Account
+import           Hcompta.Account (Account)
+import qualified Hcompta.Amount as Amount
+import           Hcompta.Amount (Amount)
+import           Hcompta.Amount.Unit (Unit)
+import qualified Hcompta.Filter as Filter
+import qualified Hcompta.Filter.Read as Filter.Read
 
 data Ctx
  =   Ctx
- { ctx_input     :: [FilePath]
- , ctx_redundant :: Bool
- } deriving (Eq, Show)
+ { ctx_input              :: [FilePath]
+ , ctx_redundant          :: Bool
+ , ctx_transaction_filter :: Filter.Test_Bool (Filter.Test_Transaction Ledger.Transaction)
+ , ctx_posting_filter     :: Filter.Test_Bool (Filter.Test_Posting     Ledger.Posting)
+ } deriving (Show)
 
 nil :: Ctx
 nil =
        Ctx
-        { ctx_input     = []
-        , ctx_redundant = False
+        { ctx_input              = []
+        , ctx_redundant          = False
+        , ctx_transaction_filter = Filter.Any
+        , ctx_posting_filter     = Filter.Any
         }
 
 usage :: IO String
@@ -69,14 +87,32 @@ options =
         (ReqArg (\s _context ctx -> do
                return $ ctx{ctx_input=s:ctx_input ctx}) "FILE")
         "read data from given file, can be use multiple times"
+       , Option "t" ["transaction-filter"]
+        (ReqArg (\s context ctx -> do
+               ctx_transaction_filter <-
+                       liftIO $ Filter.Read.read Filter.Read.test_transaction s
+                       >>= \f -> case f of
+                        Left  ko -> Write.fatal context $ ko
+                        Right ok -> return ok
+               return $ ctx{ctx_transaction_filter}) "FILTER")
+        "filter on posting"
+       , Option "p" ["posting-filter"]
+        (ReqArg (\s context ctx -> do
+               ctx_posting_filter <-
+                       liftIO $ Filter.Read.read Filter.Read.test_posting s
+                       >>= \f -> case f of
+                        Left  ko -> Write.fatal context $ ko
+                        Right ok -> return ok
+               return $ ctx{ctx_posting_filter}) "FILTER")
+        "filter on balance"
        , Option "" ["redundant"]
         (OptArg (\arg context ctx -> do
                redundant <- case arg of
                 Nothing    -> return $ True
                 Just "yes" -> return $ True
                 Just "no"  -> return $ False
-                Just _     -> Write.fatal context
-                        "--redundant option expects \"yes\", or \"no\" as value"
+                Just _     -> Write.fatal context $
+                       W.text "--redundant option expects \"yes\", or \"no\" as value"
                return $ ctx{ctx_redundant=redundant})
          "[yes|no]")
         "also print accounts with zero amount or the same amounts than its ascending account"
@@ -84,116 +120,217 @@ options =
 
 run :: Context.Context -> [String] -> IO ()
 run context args = do
-       (ctx, _) <- Args.parse context usage options (nil, args)
-       koks <- do
+       (ctx, text_filters) <- Args.parse context usage options (nil, args)
+       read_journals <- do
                CLI.Ledger.paths context $ ctx_input ctx
-               >>= do mapM $ \path -> do
-                       liftIO $ runExceptT $
-                               Ledger.Read.file path
-                       >>= \x -> case x of
-                        Left  ko -> return $ Left (path, ko)
-                        Right ok -> return $ Right ok
+               >>= do
+                       mapM $ \path -> do
+                               liftIO $ runExceptT $ Ledger.Read.file path
+                               >>= \x -> case x of
+                                Left  ko -> return $ Left (path, ko)
+                                Right ok -> return $ Right ok
                >>= return . Data.Either.partitionEithers
-       case koks of
-        (kos@(_:_), _oks) ->
-               (flip mapM_) kos $ \(_path, ko) ->
-                       Write.fatal context $ ko
+       case read_journals of
+        (errs@(_:_), _journals) ->
+               (flip mapM_) errs $ \(_path, err) -> do
+                       Write.fatal context $ err
         ([], journals) -> do
-               let balance =
-                       Data.List.foldl
-                        (\b j -> Balance.journal_with_virtual
-                                (Hcompta.Format.Ledger.Journal.to_Model j) b)
-                        Balance.nil
+               balance_filter <-
+                       foldr Filter.And Filter.Any <$> do
+                       (flip mapM) text_filters $ \s ->
+                               liftIO $ Filter.Read.read Filter.Read.test_balance s
+                               >>= \f -> case f of
+                                Left  ko -> Write.fatal context $ ko
+                                Right ok -> return ok
+               Write.debug context $ "balance_filter: " ++ show balance_filter
+               Write.debug context $ "transaction_filter: " ++ show (ctx_transaction_filter ctx)
+               Write.debug context $ "posting_filter: " ++ show (ctx_posting_filter ctx)
+               let (balance_by_account, balance_by_unit) =
+                       ledger_balances
+                        (ctx_transaction_filter ctx)
+                        (ctx_posting_filter ctx)
+                        balance_filter
                         journals
-               Write.debug context $ ppShow $ balance
-               Write.debug context $ ppShow $
-                       Lib.TreeMap.flatten (const ()) (Balance.by_account balance)
-               let expanded = Balance.expanded $ Balance.by_account balance
-               Write.debug context $ ppShow $ expanded
-               with_color <- Write.with_color context IO.stdout
-               Ledger.Write.put with_color IO.stdout $ do
-                       let (max_amount_length, accounts) = write_accounts ctx expanded
-                       accounts <> do
-                       (if W.is_empty accounts
-                               then W.empty
-                               else (W.bold $ W.dullblack $
-                                       W.text (TL.pack $ replicate max_amount_length '-') <>
-                                       (if max_amount_length <= 0 then W.empty else W.line))) <> do
-                       write_amounts max_amount_length $
-                               Data.Map.map Balance.amount $
-                               (Balance.by_unit balance)
+               style_color <- Write.with_color context IO.stdout
+               W.displayIO IO.stdout $
+                W.renderPretty style_color 1.0 maxBound $ do
+                       toDoc () $
+                               let title =
+                                       TL.toStrict . W.displayT .
+                                       W.renderCompact False .
+                                       toDoc (Context.lang context) in
+                               zipWith id
+                               [ Table.column (title Lang.Message_Balance_debit)  Table.Align_Right
+                               , Table.column (title Lang.Message_Balance_credit) Table.Align_Right
+                               , Table.column (title Lang.Message_Balance_total)  Table.Align_Right
+                               , Table.column (title Lang.Message_Account)        Table.Align_Left
+                               ] $
+                               flip (write_by_accounts ctx) balance_by_account $
+                               zipWith (:)
+                                       [ Table.Cell_Line '=' 0
+                                       , Table.Cell_Line '=' 0
+                                       , Table.Cell_Line '=' 0
+                                       , Table.Cell_Line ' ' 0
+                                       ] $
+                               write_by_amounts (repeat []) $
+                                       Data.Map.map
+                                        Balance.unit_sum_amount
+                                        balance_by_unit
 
-write_accounts :: Ctx -> Balance.Expanded -> (Int, W.Doc)
-write_accounts ctx accounts = do
-       let max_amount_length =
-               uncurry (+) $
-               Data.Foldable.foldl
-                (\(len, plus) Balance.Account_Sum_Expanded{Balance.inclusive=amounts} ->
-                       let amounts_ = (if ctx_redundant ctx then amounts else Data.Map.filter (not . Amount.is_zero) amounts) in
-                       ( Data.Map.foldr (max . Ledger.Write.amount_length) len amounts
-                       , (if Data.Map.size amounts_ > 1
-                               then 2 -- NOTE: length "+ "
-                               else plus)
-                       )
-                )
-                (0, 0) accounts
-       (max_amount_length,) $ do
-       Lib.TreeMap.foldl_with_Path_and_Node
-        (\doc account node amounts ->
-               let descendants = Lib.TreeMap.nodes (Lib.TreeMap.node_descendants node) in
-               if not (ctx_redundant ctx) && (
-                       Data.Map.size
-                        (Data.Map.filter
-                                (not . Amount.is_zero)
-                                (Balance.exclusive amounts)) == 0 &&
-                       Data.Map.size
-                        (Data.Map.filter
-                                ( maybe False (not . Amount.are_zero . Balance.inclusive)
-                                . Lib.TreeMap.node_value
-                                ) descendants) == 1
-                )
-               then doc
-               else
-                       doc <> Data.Map.foldl
-                        (\doc_ amount ->
-                               if not (ctx_redundant ctx) && Amount.is_zero amount
-                               then doc_
-                               else
-                                       doc_ <>
-                                       (if W.is_empty doc_
-                                       then do
-                                               W.fill (max_amount_length - Ledger.Write.amount_length amount) W.empty <> do
-                                               Ledger.Write.amount amount <> do
-                                               W.space <> W.space <> do
-                                               Ledger.Write.account Posting.Type_Regular account
-                                       else do
-                                               (W.bold $ W.dullblack $ W.text "+" <> W.space) <> do
-                                               W.fill (max_amount_length - Ledger.Write.amount_length amount - 2) W.empty <> do
-                                               Ledger.Write.amount amount) <> do
-                                       W.line
-                        ) W.empty (Balance.inclusive amounts)
+ledger_balances
+ :: Filter.Test_Bool (Filter.Test_Transaction Ledger.Transaction)
+ -> Filter.Test_Bool (Filter.Test_Posting     Ledger.Posting)
+ -> Filter.Test_Bool (Filter.Test_Balance     (Account, Balance.Amount_Sum Amount))
+ -> [Ledger.Journal]
+ -> ( Balance.Expanded (Balance.Amount_Sum Amount)
+    , Balance.Balance_by_Unit (Balance.Amount_Sum Amount) Unit )
+ledger_balances
+ transaction_filter
+ posting_filter
+ balance_filter
+ journals =
+       let balance_by_account =
+               foldr
+                (Ledger.Journal.fold
+                        (flip (foldr
+                                (flip (foldr
+                                        (\tr ->
+                                               case Filter.test transaction_filter tr of
+                                                False -> id
+                                                True ->
+                                                       let filter_postings =
+                                                               Data.Foldable.concatMap $
+                                                               Data.List.filter $
+                                                               (Filter.test posting_filter) in
+                                                       let balance =
+                                                               flip (foldr Balance.by_account) .
+                                                               map (\p ->
+                                                                       ( Ledger.posting_account p
+                                                                       , Data.Map.map Balance.amount_sum (Ledger.posting_amounts p)
+                                                                       )
+                                                                ) .
+                                                               filter_postings in
+                                                       balance (Ledger.transaction_postings tr) .
+                                                       balance (Ledger.transaction_virtual_postings tr) .
+                                                       balance (Ledger.transaction_balanced_virtual_postings tr)
+                                        ))))
+                        . Ledger.journal_transactions))
+                (Balance.balance_by_account Balance.nil)
+                journals in
+       let balance_expanded =
+               Lib.TreeMap.filter_with_Path (\acct ->
+                       Data.Foldable.any
+                        (Filter.test balance_filter . (acct,)) .
+                       Balance.inclusive) $
+               Balance.expanded balance_by_account in
+       let balance_by_unit =
+               Balance.by_unit_of_expanded
+                balance_expanded
+                (Balance.balance_by_unit Balance.nil) in
+       ( balance_expanded
+       , balance_by_unit
+       )
+
+write_by_accounts
+ :: Ctx
+ -> [[Table.Cell]]
+ -> Balance.Expanded (Balance.Amount_Sum Amount)
+ -> [[Table.Cell]]
+write_by_accounts ctx =
+       let posting_type = Ledger.Posting_Type_Regular in
+       Lib.TreeMap.foldr_with_Path_and_Node
+        (\account node balance rows -> do
+               let descendants = Lib.TreeMap.nodes
+                        (Lib.TreeMap.node_descendants node)
+               let is_worth =
+                       ctx_redundant ctx
+                       -- NOTE: worth if no descendant
+                       -- but account inclusive
+                       -- has at least a non-zero amount
+                       || (Data.Map.null descendants && not
+                                (Data.Map.null
+                                (Data.Map.filter
+                                        (not . Amount.is_zero . Balance.amount_sum_balance)
+                                        (Balance.inclusive balance))))
+                       -- NOTE: worth if account exclusive
+                       -- has at least a non-zero amount
+                       || not (Data.Map.null
+                                (Data.Map.filter
+                                        (not . Amount.is_zero . Balance.amount_sum_balance)
+                                        (Balance.exclusive balance)))
+                       -- NOTE: worth if account has at least more than
+                       -- one descendant account whose inclusive
+                       -- has at least a non-zero amount
+                       || Data.Map.size
+                                (Data.Map.filter
+                                        ( maybe False
+                                                ( not . Data.Foldable.all
+                                                        ( Amount.is_zero
+                                                        . Balance.amount_sum_balance )
+                                                . Balance.inclusive )
+                                        . Lib.TreeMap.node_value )
+                                        descendants) > 1
+               case is_worth of
+                False -> rows
+                True ->
+                       foldr
+                        (\(amount_positive, amount_negative, amount) ->
+                               zipWith (:)
+                                       [ Table.cell
+                                        { Table.cell_content = maybe W.empty Ledger.Write.amount  amount_positive
+                                        , Table.cell_width   = maybe 0 Ledger.Write.amount_length amount_positive
+                                        }
+                                       , Table.cell
+                                        { Table.cell_content = maybe W.empty Ledger.Write.amount  amount_negative
+                                        , Table.cell_width   = maybe 0 Ledger.Write.amount_length amount_negative
+                                        }
+                                       , Table.cell
+                                        { Table.cell_content = Ledger.Write.amount        $ amount
+                                        , Table.cell_width   = Ledger.Write.amount_length $ amount
+                                        }
+                                       , Table.cell
+                                        { Table.cell_content = Ledger.Write.account        posting_type account
+                                        , Table.cell_width   = Ledger.Write.account_length posting_type account
+                                        }
+                                       ]
+                        )
+                        rows $
+                       let bal = Balance.inclusive balance in
+                       Data.Map.foldrWithKey
+                        (\unit amount acc ->
+                               ( maybe Nothing Balance.amount_sum_positive $ Data.Map.lookup unit $ bal
+                               , maybe Nothing Balance.amount_sum_negative $ Data.Map.lookup unit $ bal
+                               , Balance.amount_sum_balance amount
+                               ) : acc
+                        ) [] $ bal
         )
-        W.empty accounts
 
-write_amounts :: Int -> Amount.By_Unit -> W.Doc
-write_amounts max_amount_length_ amounts = do
-       let max_amount_length =
-               Data.Map.foldr
-                (max . Ledger.Write.amount_length)
-                max_amount_length_ amounts
-       (if Data.Map.size amounts > 1
-               then W.space <> W.space
-               else W.empty) <> do
-       W.intercalate
-        (W.line <> (W.bold $ W.dullblack $ W.text "+") <> W.space)
-        (\amount ->
-               let len =
-                       max_amount_length
-                       - Ledger.Write.amount_length amount
-                       - (if Data.Map.size amounts > 1
-                               then 2 -- NOTE: length "+ "
-                               else 0) in
-               W.fill len W.empty <> do
-               Ledger.Write.amount amount)
-        amounts <> do
-       (if Data.Map.null amounts then W.empty else W.line)
+write_by_amounts
+ :: [[Table.Cell]]
+ -> Data.Map.Map Unit (Balance.Amount_Sum Amount)
+ -> [[Table.Cell]]
+write_by_amounts =
+       foldr
+        (\amount_sum ->
+               zipWith (:)
+                       [ let amt = Balance.amount_sum_positive amount_sum in
+                               Table.cell
+                                { Table.cell_content = maybe W.empty Ledger.Write.amount  amt
+                                , Table.cell_width   = maybe 0 Ledger.Write.amount_length amt
+                                }
+                       , let amt = Balance.amount_sum_negative amount_sum in
+                               Table.cell
+                                { Table.cell_content = maybe W.empty Ledger.Write.amount  amt
+                                , Table.cell_width   = maybe 0 Ledger.Write.amount_length amt
+                                }
+                       , let amt = Balance.amount_sum_balance amount_sum in
+                               Table.cell
+                                { Table.cell_content = Ledger.Write.amount  amt
+                                , Table.cell_width   = Ledger.Write.amount_length amt
+                                }
+                       , Table.cell
+                                { Table.cell_content = W.empty
+                                , Table.cell_width   = 0
+                                }
+                       ]
+        )