-
-
Save glguy/fe5ea89e8d447874b704326e889d7f4b to your computer and use it in GitHub Desktop.
Script for automatic reconciliation based on bank exports
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# Language ScopedTypeVariables, BlockArguments, DeriveTraversable #-} | |
{-# Options_GHC -Wno-unused-imports #-} | |
{-# Options_GHC -w #-} | |
module Main where | |
import Data.Map (Map) | |
import Hledger | |
import System.Console.ANSI | |
import Data.List (intercalate, intersperse, transpose) | |
import Data.Traversable | |
import Data.Foldable | |
import Text.Tabular | |
import Text.Tabular.AsciiWide | |
import qualified Data.Map as Map | |
import qualified Data.Set as Set | |
import qualified Data.Text as Text | |
import qualified Hledger.Cli as Cli | |
import qualified Hledger.Utils.Regex as Regex | |
mkMap :: [Account] -> Map AccountName Amount | |
mkMap accts = | |
Map.fromList | |
[ (aname a, getAmount (aibalance a)) | |
| a <- accts | |
] | |
look :: AccountName -> Map AccountName Amount -> Amount | |
look = Map.findWithDefault 0 | |
getAmount :: MixedAmount -> Amount | |
getAmount amt = | |
case amt of | |
Mixed [x] | |
| acommodity x == Text.singleton '$' -> x | |
Mixed [] -> 0 | |
_ -> error "Unexpected commodity in event report!" | |
nestAccount :: AccountName -> String | |
nestAccount a = go (Text.split (':'==) a) | |
where | |
go [x] = Text.unpack x | |
go [] = "" | |
go (_:xs) = " " <> go xs | |
cmdMode :: Cli.Mode RawOpts | |
cmdMode = | |
Cli.hledgerCommandMode | |
"events" | |
[] | |
[Cli.generalflagsgroup2] | |
[] | |
([], Nothing) | |
main :: IO () | |
main = | |
do copts <- Cli.getHledgerCliOpts cmdMode | |
Cli.withJournalDo copts \journal -> | |
do let table = report journal | |
putStrLn (render True id id prettyNumber table) | |
report :: Journal -> Table String String Amount | |
report journal | |
| Map.null saf_ledger = table | |
| otherwise = error ("Erroneous accounts: " ++ | |
show (Map.keys saf_ledger)) | |
where | |
Right prefixRE = Regex.toRegex "^Events:" | |
mk q = mkMap (drop 1 (laccounts (ledgerFromJournal q journal))) | |
Right flowTag = Tag <$> Regex.toRegex "^flow$" | |
Right inRE = Regex.toRegex "^in$" | |
Right outRE = Regex.toRegex "^out$" | |
Right inOutRE = Regex.toRegex "^(in|out)$" | |
in_ledger = mk (And [Acct prefixRE, flowTag (Just inRE)]) | |
out_ledger = mk (And [Acct prefixRE, flowTag (Just outRE)]) | |
bal_ledger = mk (And [Acct prefixRE]) | |
saf_ledger = mk (And [Acct prefixRE, Not (flowTag (Just inOutRE))]) | |
accounts = divideAccounts | |
$ Set.toList | |
$ Map.keysSet in_ledger <> | |
Map.keysSet out_ledger <> | |
Map.keysSet bal_ledger | |
rowHeaders = | |
Group SingleLine | |
(map (Group NoLine . map (Header . anPad)) accountNames) | |
where | |
accountNames = map (map nestAccount) accounts | |
anLen = maximum (map length (concat accountNames)) | |
anPad str = str ++ replicate (anLen - length str) '·' | |
table = Table | |
rowHeaders | |
columnHeaders | |
[ [-look a in_ledger, look a out_ledger, -look a bal_ledger] | |
| a <- concat accounts ] | |
divideAccounts :: [AccountName] -> [[AccountName]] | |
divideAccounts [] = [] | |
divideAccounts (x:xs) = | |
case span p xs of | |
(a,b) -> (x:a) : divideAccounts b | |
where | |
p y = Text.length (Text.filter (':'==) y) == 2 | |
columnHeaders :: Header String | |
columnHeaders = | |
Group DoubleLine | |
[Group SingleLine [Header "recvd", Header "paid"], | |
Header "held"] | |
prettyNumber :: Amount -> String | |
prettyNumber n = | |
case compare (aquantity n) 0 of | |
LT -> setSGRCode [SetColor Foreground Dull Red] <> | |
"(" <> showAmount (-n) <> ")" <> | |
setSGRCode [Reset] | |
GT -> showAmount n | |
EQ -> "-" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
if [ $# -ne 2 ]; then | |
echo "Usage: reconcile.sh LEDGER ACCOUNT" | |
fi | |
LEDGER=$1 | |
ACCOUNT=$2 | |
RED=$(tput setaf 1) | |
GREEN=$(tput setaf 2) | |
YELLOW=$(tput setaf 3) | |
MAGENTA=$(tput setaf 5) | |
RESET=$(tput sgr0) | |
PENDING=$(hledger -f "$LEDGER" register -w 80 --color=always "$ACCOUNT" not:tag:rec) | |
UNBALANCED=$(hledger -f "$LEDGER" bal --color=always -N "$ACCOUNT" --pivot rec tag:rec) | |
printf "${MAGENTA}%-30s " "$ACCOUNT" | |
if [ -n "$UNBALANCED" ]; then | |
printf "${RED}Unbalanced${RESET}\n" | |
elif [ -n "$PENDING" ]; then | |
printf "${YELLOW}Pending${RESET}\n" | |
else | |
printf "${GREEN}OK${RESET}\n" | |
fi | |
if [ -n "$UNBALANCED" ]; then | |
echo "$UNBALANCED" | |
fi | |
if [ -n "$PENDING" ]; then | |
echo "$PENDING" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment