Skip to content

Instantly share code, notes, and snippets.

@glguy
Last active September 13, 2021 16:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save glguy/fe5ea89e8d447874b704326e889d7f4b to your computer and use it in GitHub Desktop.
Save glguy/fe5ea89e8d447874b704326e889d7f4b to your computer and use it in GitHub Desktop.
Script for automatic reconciliation based on bank exports
{-# 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 -> "-"
#!/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