haspara 0.0.0.0 → 0.0.0.1
raw patch · 10 files changed
+468/−6 lines, 10 filesdep +deriving-aesondep ~refineddep ~unordered-containersPVP: minor bump suggested
API additions: PVP suggests at least a minor version bump
Dependencies added: deriving-aeson
Dependency ranges changed: refined, unordered-containers
API changes (from Hackage documentation)
+ Haspara.Accounting: Account :: !AccountKind -> !o -> Account o
+ Haspara.Accounting: AccountKindAsset :: AccountKind
+ Haspara.Accounting: AccountKindEquity :: AccountKind
+ Haspara.Accounting: AccountKindExpense :: AccountKind
+ Haspara.Accounting: AccountKindLiability :: AccountKind
+ Haspara.Accounting: AccountKindRevenue :: AccountKind
+ Haspara.Accounting: EntryCredit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting: EntryDebit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting: EventDecrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting: EventIncrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting: Ledger :: !Account a -> !Quantity s -> !Quantity s -> ![LedgerItem o s] -> Ledger a o (s :: Nat)
+ Haspara.Accounting: LedgerItem :: !Entry o s -> !Quantity s -> LedgerItem o (s :: Nat)
+ Haspara.Accounting: Posting :: NonEmpty (Event o s, Account a) -> Posting a o (s :: Nat)
+ Haspara.Accounting: [accountKind] :: Account o -> !AccountKind
+ Haspara.Accounting: [accountObject] :: Account o -> !o
+ Haspara.Accounting: [ledgerAccount] :: Ledger a o (s :: Nat) -> !Account a
+ Haspara.Accounting: [ledgerClosing] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting: [ledgerItemBalance] :: LedgerItem o (s :: Nat) -> !Quantity s
+ Haspara.Accounting: [ledgerItemEntry] :: LedgerItem o (s :: Nat) -> !Entry o s
+ Haspara.Accounting: [ledgerOpening] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting: [ledgerRunning] :: Ledger a o (s :: Nat) -> ![LedgerItem o s]
+ Haspara.Accounting: accountKindText :: AccountKind -> Text
+ Haspara.Accounting: addEntry :: KnownNat s => Ledger a o s -> Entry o s -> Ledger a o s
+ Haspara.Accounting: buildEntry :: KnownNat s => Event o s -> AccountKind -> Entry o s
+ Haspara.Accounting: data Account o
+ Haspara.Accounting: data AccountKind
+ Haspara.Accounting: data Entry o (s :: Nat)
+ Haspara.Accounting: data Event o (s :: Nat)
+ Haspara.Accounting: data Ledger a o (s :: Nat)
+ Haspara.Accounting: data LedgerItem o (s :: Nat)
+ Haspara.Accounting: entryCredit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting: entryDate :: KnownNat s => Entry o s -> Date
+ Haspara.Accounting: entryDebit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting: entryObject :: KnownNat s => Entry o s -> o
+ Haspara.Accounting: entryQuantity :: KnownNat s => Entry o s -> Quantity s
+ Haspara.Accounting: eventDate :: KnownNat s => Event o s -> Date
+ Haspara.Accounting: eventObject :: KnownNat s => Event o s -> o
+ Haspara.Accounting: mkEvent :: (MonadError String m, KnownNat s) => Date -> o -> Quantity s -> m (Event o s)
+ Haspara.Accounting: mkLedger :: KnownNat s => Account a -> Quantity s -> [Entry o s] -> Ledger a o s
+ Haspara.Accounting: negateEvent :: KnownNat s => Event o s -> Event o s
+ Haspara.Accounting: newtype Posting a o (s :: Nat)
+ Haspara.Accounting: post :: KnownNat s => Posting a o s -> [(Account a, Entry o s)]
+ Haspara.Accounting: postingEvents :: KnownNat s => Posting a o s -> [o]
+ Haspara.Accounting: type UnsignedQuantity s = Refined NonNegative (Quantity s)
+ Haspara.Accounting.Account: Account :: !AccountKind -> !o -> Account o
+ Haspara.Accounting.Account: [accountKind] :: Account o -> !AccountKind
+ Haspara.Accounting.Account: [accountObject] :: Account o -> !o
+ Haspara.Accounting.Account: data Account o
+ Haspara.Accounting.Account: instance Data.Aeson.Types.FromJSON.FromJSON o => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance Data.Aeson.Types.ToJSON.ToJSON o => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance Data.Hashable.Class.Hashable o => Data.Hashable.Class.Hashable (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Generics.Generic (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Show.Show o => GHC.Show.Show (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.AccountKind: AccountKindAsset :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindEquity :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindExpense :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindLiability :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindRevenue :: AccountKind
+ Haspara.Accounting.AccountKind: accountKindText :: AccountKind -> Text
+ Haspara.Accounting.AccountKind: data AccountKind
+ Haspara.Accounting.AccountKind: instance Data.Aeson.Types.FromJSON.FromJSON Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance Data.Aeson.Types.ToJSON.ToJSON Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance Data.Hashable.Class.Hashable Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Classes.Eq Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Classes.Ord Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Enum.Enum Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Generics.Generic Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Show.Show Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.Entry: EntryCredit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting.Entry: EntryDebit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting.Entry: buildEntry :: KnownNat s => Event o s -> AccountKind -> Entry o s
+ Haspara.Accounting.Entry: data Entry o (s :: Nat)
+ Haspara.Accounting.Entry: entryCredit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting.Entry: entryDate :: KnownNat s => Entry o s -> Date
+ Haspara.Accounting.Entry: entryDebit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting.Entry: entryObject :: KnownNat s => Entry o s -> o
+ Haspara.Accounting.Entry: entryQuantity :: KnownNat s => Entry o s -> Quantity s
+ Haspara.Accounting.Entry: instance (Data.Aeson.Types.FromJSON.FromJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance (Data.Aeson.Types.ToJSON.ToJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance (GHC.Show.Show o, GHC.TypeNats.KnownNat s) => GHC.Show.Show (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Event: EventDecrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting.Event: EventIncrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting.Event: data Event o (s :: Nat)
+ Haspara.Accounting.Event: eventDate :: KnownNat s => Event o s -> Date
+ Haspara.Accounting.Event: eventObject :: KnownNat s => Event o s -> o
+ Haspara.Accounting.Event: instance (Data.Aeson.Types.FromJSON.FromJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance (Data.Aeson.Types.ToJSON.ToJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance (GHC.Show.Show o, GHC.TypeNats.KnownNat s) => GHC.Show.Show (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: mkEvent :: (MonadError String m, KnownNat s) => Date -> o -> Quantity s -> m (Event o s)
+ Haspara.Accounting.Event: negateEvent :: KnownNat s => Event o s -> Event o s
+ Haspara.Accounting.Ledger: Ledger :: !Account a -> !Quantity s -> !Quantity s -> ![LedgerItem o s] -> Ledger a o (s :: Nat)
+ Haspara.Accounting.Ledger: LedgerItem :: !Entry o s -> !Quantity s -> LedgerItem o (s :: Nat)
+ Haspara.Accounting.Ledger: [ledgerAccount] :: Ledger a o (s :: Nat) -> !Account a
+ Haspara.Accounting.Ledger: [ledgerClosing] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting.Ledger: [ledgerItemBalance] :: LedgerItem o (s :: Nat) -> !Quantity s
+ Haspara.Accounting.Ledger: [ledgerItemEntry] :: LedgerItem o (s :: Nat) -> !Entry o s
+ Haspara.Accounting.Ledger: [ledgerOpening] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting.Ledger: [ledgerRunning] :: Ledger a o (s :: Nat) -> ![LedgerItem o s]
+ Haspara.Accounting.Ledger: addEntry :: KnownNat s => Ledger a o s -> Entry o s -> Ledger a o s
+ Haspara.Accounting.Ledger: data Ledger a o (s :: Nat)
+ Haspara.Accounting.Ledger: data LedgerItem o (s :: Nat)
+ Haspara.Accounting.Ledger: instance (Data.Aeson.Types.FromJSON.FromJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance (Data.Aeson.Types.ToJSON.ToJSON o, Data.Aeson.Types.ToJSON.ToJSON a, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.Classes.Eq a, GHC.Classes.Eq o) => GHC.Classes.Eq (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.Classes.Ord a, GHC.Classes.Ord o) => GHC.Classes.Ord (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.Show.Show o, GHC.TypeNats.KnownNat s) => GHC.Show.Show (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance (GHC.TypeNats.KnownNat s, Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.FromJSON.FromJSON o) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.TypeNats.KnownNat s, Data.Aeson.Types.ToJSON.ToJSON o) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance (GHC.TypeNats.KnownNat s, GHC.Show.Show a, GHC.Show.Show o) => GHC.Show.Show (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance GHC.Generics.Generic (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance GHC.Generics.Generic (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: mkLedger :: KnownNat s => Account a -> Quantity s -> [Entry o s] -> Ledger a o s
+ Haspara.Accounting.Posting: Posting :: NonEmpty (Event o s, Account a) -> Posting a o (s :: Nat)
+ Haspara.Accounting.Posting: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.Classes.Eq o, GHC.Classes.Eq a) => GHC.Classes.Eq (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.Classes.Ord o, GHC.Classes.Ord a) => GHC.Classes.Ord (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.TypeNats.KnownNat s, Data.Aeson.Types.FromJSON.FromJSON o, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.TypeNats.KnownNat s, GHC.Show.Show o, GHC.Show.Show a) => GHC.Show.Show (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance GHC.Generics.Generic (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: newtype Posting a o (s :: Nat)
+ Haspara.Accounting.Posting: post :: KnownNat s => Posting a o s -> [(Account a, Entry o s)]
+ Haspara.Accounting.Posting: postingEvents :: KnownNat s => Posting a o s -> [o]
+ Haspara.Accounting.Types: type UnsignedQuantity s = Refined NonNegative (Quantity s)
Files
- CHANGELOG.md +17/−1
- haspara.cabal +15/−5
- src/Haspara/Accounting.hs +42/−0
- src/Haspara/Accounting/Account.hs +32/−0
- src/Haspara/Accounting/AccountKind.hs +75/−0
- src/Haspara/Accounting/Entry.hs +109/−0
- src/Haspara/Accounting/Event.hs +76/−0
- src/Haspara/Accounting/Ledger.hs +43/−0
- src/Haspara/Accounting/Posting.hs +52/−0
- src/Haspara/Accounting/Types.hs +7/−0
CHANGELOG.md view
@@ -2,6 +2,20 @@ ## [Unreleased] +<a name="0.0.0.1"></a>+## [0.0.0.1] - 2022-03-01+### Chore+- bump development version to 0.0.0.1+- **deps:** upgrade to lts-18.27++### Feat+- add rudimentary accounting functionality++### Pull Requests+- Merge pull request [#4](https://github.com/telostat/haspara/issues/4) from telostat/upgrade-stack-lts+- Merge pull request [#3](https://github.com/telostat/haspara/issues/3) from telostat/rudimentary-accounting++ <a name="0.0.0.0"></a> ## 0.0.0.0 - 2022-03-01 ### Chore@@ -13,6 +27,7 @@ - init codebase - **deps:** upgrade to lts-18.17 - **docs:** update README+- **release:** 0.0.0.0 ### Feat - add Aeson instances to Money data type@@ -22,4 +37,5 @@ - Merge pull request [#1](https://github.com/telostat/haspara/issues/1) from telostat/init -[Unreleased]: https://github.com/telostat/haspara/compare/0.0.0.0...HEAD+[Unreleased]: https://github.com/telostat/haspara/compare/0.0.0.1...HEAD+[0.0.0.1]: https://github.com/telostat/haspara/compare/0.0.0.0...0.0.0.1
haspara.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.12 name: haspara-version: 0.0.0.0+version: 0.0.0.1 license: MIT license-file: LICENSE copyright: Copyright (c) 2021-2022 Telostat Pte Ltd@@ -27,6 +27,14 @@ library exposed-modules: Haspara+ Haspara.Accounting+ Haspara.Accounting.Account+ Haspara.Accounting.AccountKind+ Haspara.Accounting.Entry+ Haspara.Accounting.Event+ Haspara.Accounting.Ledger+ Haspara.Accounting.Posting+ Haspara.Accounting.Types Haspara.Currency Haspara.Date Haspara.FXQuote@@ -50,16 +58,17 @@ build-depends: aeson >=1.5.6.0 && <1.6, base >=4.11 && <5,+ deriving-aeson >=0.2.8 && <0.3, hashable >=1.3.0.0 && <1.4, megaparsec >=9.0.1 && <9.1, mtl >=2.2.2 && <2.3,- refined >=0.6.2 && <0.7,+ refined >=0.6.3 && <0.7, safe-decimal >=0.2.1.0 && <0.3, scientific >=0.3.7.0 && <0.4, template-haskell >=2.16.0.0 && <2.17, text >=1.2.4.1 && <1.3, time >=1.9.3 && <1.10,- unordered-containers >=0.2.15.0 && <0.3+ unordered-containers >=0.2.16.0 && <0.3 test-suite haspara-doctest type: exitcode-stdio-1.0@@ -71,15 +80,16 @@ build-depends: aeson >=1.5.6.0 && <1.6, base >=4.11 && <5,+ deriving-aeson >=0.2.8 && <0.3, doctest ==0.17.*, hashable >=1.3.0.0 && <1.4, haspara -any, megaparsec >=9.0.1 && <9.1, mtl >=2.2.2 && <2.3,- refined >=0.6.2 && <0.7,+ refined >=0.6.3 && <0.7, safe-decimal >=0.2.1.0 && <0.3, scientific >=0.3.7.0 && <0.4, template-haskell >=2.16.0.0 && <2.17, text >=1.2.4.1 && <1.3, time >=1.9.3 && <1.10,- unordered-containers >=0.2.15.0 && <0.3+ unordered-containers >=0.2.16.0 && <0.3
+ src/Haspara/Accounting.hs view
@@ -0,0 +1,42 @@+module Haspara.Accounting+ ( Account(..)+ , AccountKind(..)+ , accountKindText+ , Entry(..)+ , buildEntry+ , Event(..)+ , eventDate+ , eventObject+ , negateEvent+ , mkEvent+ , Posting(..)+ , postingEvents+ , post+ , UnsignedQuantity+ , Ledger(..)+ , LedgerItem(..)+ , mkLedger+ , addEntry+ , entryDate+ , entryObject+ , entryQuantity+ , entryDebit+ , entryCredit+ ) where+++import Haspara.Accounting.Account (Account(..))+import Haspara.Accounting.AccountKind (AccountKind(..), accountKindText)+import Haspara.Accounting.Entry+ ( Entry(..)+ , buildEntry+ , entryCredit+ , entryDate+ , entryDebit+ , entryObject+ , entryQuantity+ )+import Haspara.Accounting.Event (Event(..), eventDate, eventObject, mkEvent, negateEvent)+import Haspara.Accounting.Ledger (Ledger(..), LedgerItem(..), addEntry, mkLedger)+import Haspara.Accounting.Posting (Posting(..), post, postingEvents)+import Haspara.Accounting.Types (UnsignedQuantity)
+ src/Haspara/Accounting/Account.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}++module Haspara.Accounting.Account where++import Data.Hashable (Hashable)+import Deriving.Aeson (CustomJSON(CustomJSON), FromJSON, Generic, ToJSON)+import Deriving.Aeson.Stock (PrefixedSnake)+import Haspara.Accounting.AccountKind (AccountKind)+++-- | Account model.+--+-- >>> import Haspara.Accounting.AccountKind (AccountKind(..))+-- >>> import qualified Data.Aeson as Aeson+-- >>> let acc = Account AccountKindAsset (1 ::Int)+-- >>> Aeson.encode acc+-- "{\"kind\":\"ASSET\",\"object\":1}"+-- >>> Aeson.decode (Aeson.encode acc) :: Maybe (Account Int)+-- Just (Account {accountKind = AccountKindAsset, accountObject = 1})+-- >>> Aeson.decode (Aeson.encode acc) == Just acc+-- True+data Account o = Account+ { accountKind :: !AccountKind+ , accountObject :: !o+ } deriving (Eq, Generic, Ord, Show)+ deriving (FromJSON, ToJSON)+ via PrefixedSnake "account" (Account o)+++instance Hashable o => Hashable (Account o)
+ src/Haspara/Accounting/AccountKind.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE DeriveGeneric #-}++module Haspara.Accounting.AccountKind where++import qualified Data.Aeson as Aeson+import qualified Data.Char as C+import Data.Hashable (Hashable)+import qualified Data.Text as T+import GHC.Generics (Generic)+++data AccountKind =+ AccountKindAsset+ | AccountKindLiability+ | AccountKindEquity+ | AccountKindRevenue+ | AccountKindExpense+ deriving (Enum, Eq, Generic, Ord, Show)+++instance Hashable AccountKind+++-- | 'Aeson.FromJSON' instance for 'AccountKind'.+--+-- >>> Aeson.decode "\"Asset\"" :: Maybe AccountKind+-- Just AccountKindAsset+-- >>> Aeson.decode "\"aSSET\"" :: Maybe AccountKind+-- Just AccountKindAsset+-- >>> Aeson.decode "\"ASSET\"" :: Maybe AccountKind+-- Just AccountKindAsset+-- >>> Aeson.decode "\"LIABILITY\"" :: Maybe AccountKind+-- Just AccountKindLiability+-- >>> Aeson.decode "\"EQUITY\"" :: Maybe AccountKind+-- Just AccountKindEquity+-- >>> Aeson.decode "\"REVENUE\"" :: Maybe AccountKind+-- Just AccountKindRevenue+-- >>> Aeson.decode "\"EXPENSE\"" :: Maybe AccountKind+-- Just AccountKindExpense+instance Aeson.FromJSON AccountKind where+ parseJSON = Aeson.withText "AccountKind" $ \t -> case T.map C.toUpper t of+ "ASSET" -> pure AccountKindAsset+ "LIABILITY" -> pure AccountKindLiability+ "EQUITY" -> pure AccountKindEquity+ "REVENUE" -> pure AccountKindRevenue+ "EXPENSE" -> pure AccountKindExpense+ _ -> fail $ "Unknown account kind: " <> show t+++-- | 'Aeson.ToJSON' instance for 'AccountKind'.+--+-- >>> Aeson.encode AccountKindAsset+-- "\"ASSET\""+-- >>> Aeson.encode AccountKindLiability+-- "\"LIABILITY\""+-- >>> Aeson.encode AccountKindEquity+-- "\"EQUITY\""+-- >>> Aeson.encode AccountKindRevenue+-- "\"REVENUE\""+-- >>> Aeson.encode AccountKindExpense+-- "\"EXPENSE\""+instance Aeson.ToJSON AccountKind where+ toJSON AccountKindAsset = Aeson.String "ASSET"+ toJSON AccountKindLiability = Aeson.String "LIABILITY"+ toJSON AccountKindEquity = Aeson.String "EQUITY"+ toJSON AccountKindRevenue = Aeson.String "REVENUE"+ toJSON AccountKindExpense = Aeson.String "EXPENSE"+++accountKindText :: AccountKind -> T.Text+accountKindText AccountKindAsset = "Asset"+accountKindText AccountKindLiability = "Liability"+accountKindText AccountKindEquity = "Equity"+accountKindText AccountKindRevenue = "Revenue"+accountKindText AccountKindExpense = "Expense"
+ src/Haspara/Accounting/Entry.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}++module Haspara.Accounting.Entry where++import Data.Aeson ((.:), (.=))+import qualified Data.Aeson as Aeson+import qualified Data.Char as C+import qualified Data.Text as T+import GHC.TypeLits (KnownNat, Nat)+import qualified Haspara as H+import Haspara.Accounting.AccountKind (AccountKind(..))+import Haspara.Accounting.Event (Event(..))+import Haspara.Accounting.Types (UnsignedQuantity)+import Refined (unrefine)+++-- | Encoding of a posting entry.+--+-- >>> :set -XDataKinds+-- >>> import Refined+-- >>> let date = read "2021-01-01"+-- >>> let oid = 1 :: Int+-- >>> let qty = $$(refineTH 42) :: UnsignedQuantity 2+-- >>> let entry = EntryDebit date oid qty+-- >>> let json = Aeson.encode entry+-- >>> json+-- "{\"qty\":42.0,\"obj\":1,\"date\":\"2021-01-01\",\"type\":\"DEBIT\"}"+-- >>> Aeson.decode json :: Maybe (Entry Int 2)+-- Just (EntryDebit 2021-01-01 1 (Refined 42.00))+-- >>> Aeson.decode json == Just entry+-- True+data Entry o (s :: Nat) =+ EntryDebit H.Date o (UnsignedQuantity s)+ | EntryCredit H.Date o (UnsignedQuantity s)+ deriving (Eq, Ord, Show)+++instance (Aeson.FromJSON o, KnownNat s) => Aeson.FromJSON (Entry o s) where+ parseJSON = Aeson.withObject "Entry" $ \o -> do+ dorc <- o .: "type"+ cons <- case T.map C.toUpper dorc of+ "DEBIT" -> pure EntryDebit+ "CREDIT" -> pure EntryCredit+ x -> fail ("Unknown entry type: " <> T.unpack x)+ date <- o .: "date"+ obj <- o .: "obj"+ qty <- o .: "qty"+ pure (cons date obj qty)+++instance (Aeson.ToJSON o, KnownNat s) => Aeson.ToJSON (Entry o s) where+ toJSON x = case x of+ EntryDebit d o q -> Aeson.object ["type" .= ("DEBIT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]+ EntryCredit d o q -> Aeson.object ["type" .= ("CREDIT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]+++entryDate :: KnownNat s => Entry o s -> H.Date+entryDate (EntryDebit d _ _) = d+entryDate (EntryCredit d _ _) = d+++entryQuantity :: KnownNat s => Entry o s -> H.Quantity s+entryQuantity (EntryDebit _ _ q) = unrefine q+entryQuantity (EntryCredit _ _ q) = -(unrefine q)+++entryObject :: KnownNat s => Entry o s -> o+entryObject (EntryDebit _ o _) = o+entryObject (EntryCredit _ o _) = o+++entryDebit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)+entryDebit (EntryDebit _ _ x) = Just x+entryDebit EntryCredit {} = Nothing+++entryCredit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)+entryCredit EntryDebit {} = Nothing+entryCredit (EntryCredit _ _ x) = Just x+++-- |+--+-- +-----------------------+----------+----------++-- | Kind of account | Debit | Credit |+-- +-----------------------+----------+----------++-- | Asset | Increase | Decrease |+-- +-----------------------+----------+----------++-- | Liability | Decrease | Increase |+-- +-----------------------+----------+----------++-- | Equity/Capital | Decrease | Increase |+-- +-----------------------+----------+----------++-- | Income/Revenue | Decrease | Increase |+-- +-----------------------+----------+----------++-- | Expense/Cost/Dividend | Increase | Decrease |+-- +-----------------------+----------+----------++--+buildEntry :: (KnownNat s) => Event o s -> AccountKind -> Entry o s+buildEntry (EventDecrement d o x) AccountKindAsset = EntryCredit d o x+buildEntry (EventIncrement d o x) AccountKindAsset = EntryDebit d o x+buildEntry (EventDecrement d o x) AccountKindLiability = EntryDebit d o x+buildEntry (EventIncrement d o x) AccountKindLiability = EntryCredit d o x+buildEntry (EventDecrement d o x) AccountKindEquity = EntryDebit d o x+buildEntry (EventIncrement d o x) AccountKindEquity = EntryCredit d o x+buildEntry (EventDecrement d o x) AccountKindRevenue = EntryDebit d o x+buildEntry (EventIncrement d o x) AccountKindRevenue = EntryCredit d o x+buildEntry (EventDecrement d o x) AccountKindExpense = EntryCredit d o x+buildEntry (EventIncrement d o x) AccountKindExpense = EntryDebit d o x
+ src/Haspara/Accounting/Event.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE KindSignatures #-}++module Haspara.Accounting.Event where++import Control.Monad.Except (MonadError(throwError))+import Data.Aeson ((.:), (.=))+import qualified Data.Aeson as Aeson+import qualified Data.Char as C+import qualified Data.Text as T+import GHC.TypeLits (KnownNat, Nat)+import qualified Haspara as H+import Haspara.Accounting.Types (UnsignedQuantity)+import Refined (refine)+++-- | Encoding of an increment/decrement event.+--+-- >>> :set -XDataKinds+-- >>> import Refined+-- >>> let date = read "2021-01-01"+-- >>> let oid = 1 :: Int+-- >>> let qty = $$(refineTH 42) :: UnsignedQuantity 2+-- >>> let event = EventDecrement date oid qty+-- >>> let json = Aeson.encode event+-- >>> json+-- "{\"qty\":42.0,\"obj\":1,\"date\":\"2021-01-01\",\"type\":\"DECREMENT\"}"+-- >>> Aeson.decode json :: Maybe (Event Int 2)+-- Just (EventDecrement 2021-01-01 1 (Refined 42.00))+-- >>> Aeson.decode json == Just event+-- True+data Event o (s :: Nat) =+ EventDecrement H.Date o (UnsignedQuantity s)+ | EventIncrement H.Date o (UnsignedQuantity s)+ deriving (Eq, Ord, Show)+++instance (Aeson.FromJSON o, KnownNat s) => Aeson.FromJSON (Event o s) where+ parseJSON = Aeson.withObject "Event" $ \o -> do+ dorc <- o .: "type"+ cons <- case T.map C.toUpper dorc of+ "DECREMENT" -> pure EventDecrement+ "INCREMENT" -> pure EventIncrement+ x -> fail ("Unknown event type: " <> T.unpack x)+ date <- o .: "date"+ obj <- o .: "obj"+ qty <- o .: "qty"+ pure (cons date obj qty)+++instance (Aeson.ToJSON o, KnownNat s) => Aeson.ToJSON (Event o s) where+ toJSON x = case x of+ EventDecrement d o q -> Aeson.object ["type" .= ("DECREMENT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]+ EventIncrement d o q -> Aeson.object ["type" .= ("INCREMENT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]+++eventDate :: (KnownNat s) => Event o s -> H.Date+eventDate (EventDecrement d _ _) = d+eventDate (EventIncrement d _ _) = d+++eventObject :: (KnownNat s) => Event o s -> o+eventObject (EventDecrement _ o _) = o+eventObject (EventIncrement _ o _) = o+++negateEvent :: (KnownNat s) => Event o s -> Event o s+negateEvent (EventDecrement d o x) = EventIncrement d o x+negateEvent (EventIncrement d o x) = EventDecrement d o x+++mkEvent :: (MonadError String m, KnownNat s) => H.Date -> o -> H.Quantity s -> m (Event o s)+mkEvent d o x+ | x < 0 = either (throwError . show) pure $ EventDecrement d o <$> refine (abs x)+ | otherwise = either (throwError . show) pure $ EventIncrement d o <$> refine (abs x)
+ src/Haspara/Accounting/Ledger.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE KindSignatures #-}++module Haspara.Accounting.Ledger where++import Deriving.Aeson (CustomJSON(CustomJSON), FromJSON, Generic, ToJSON)+import Deriving.Aeson.Stock (PrefixedSnake)+import GHC.TypeLits (KnownNat, Nat)+import Haspara (Quantity)+import Haspara.Accounting.Account (Account)+import Haspara.Accounting.Entry (Entry(..), entryQuantity)+++data Ledger a o (s :: Nat) = Ledger+ { ledgerAccount :: !(Account a)+ , ledgerOpening :: !(Quantity s)+ , ledgerClosing :: !(Quantity s)+ , ledgerRunning :: ![LedgerItem o s]+ } deriving (Eq, Generic, Ord, Show)+ deriving (FromJSON, ToJSON)+ via PrefixedSnake "ledger" (Ledger a o s)+++data LedgerItem o (s :: Nat) = LedgerItem+ { ledgerItemEntry :: !(Entry o s)+ , ledgerItemBalance :: !(Quantity s)+ } deriving (Eq, Generic, Ord, Show)+ deriving (FromJSON, ToJSON)+ via PrefixedSnake "ledgerItem" (LedgerItem o s)+++mkLedger :: KnownNat s => Account a -> Quantity s -> [Entry o s] -> Ledger a o s+mkLedger a o = foldl addEntry (Ledger a o o [])+++addEntry :: KnownNat s => Ledger a o s -> Entry o s -> Ledger a o s+addEntry l@(Ledger _ _ c r) e = l { ledgerClosing = balance, ledgerRunning = r <> [item]}+ where+ balance = c + entryQuantity e+ item = LedgerItem e balance
+ src/Haspara/Accounting/Posting.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE TemplateHaskell #-}++module Haspara.Accounting.Posting where++import qualified Data.List.NonEmpty as NE+import Deriving.Aeson (CustomJSON(CustomJSON), FromJSON, Generic, ToJSON)+import Deriving.Aeson.Stock (Vanilla)+import GHC.TypeLits (KnownNat, Nat)+import Haspara.Accounting.Account (Account(accountKind))+import Haspara.Accounting.Entry (Entry, buildEntry)+import Haspara.Accounting.Event (Event, eventObject)+++-- | Type encoding for a posting.+--+-- >>> :set -XDataKinds+-- >>> import Haspara.Accounting+-- >>> import Refined+-- >>> import qualified Data.Aeson as Aeson+-- >>> import qualified Data.List.NonEmpty as NE+-- >>> let date = read "2021-01-01"+-- >>> let oid = 1 :: Int+-- >>> let qty = $$(refineTH 42) :: UnsignedQuantity 2+-- >>> let event = EventDecrement date oid qty+-- >>> let account = Account AccountKindAsset ("Cash" :: String, 1 ::Int)+-- >>> let posting = Posting . NE.fromList $ [(event, account)]+-- >>> let json = Aeson.encode posting+-- >>> json+-- "[[{\"qty\":42.0,\"obj\":1,\"date\":\"2021-01-01\",\"type\":\"DECREMENT\"},{\"kind\":\"ASSET\",\"object\":[\"Cash\",1]}]]"+-- >>> Aeson.decode json :: Maybe (Posting (String, Int) Int 2)+-- Just (Posting ((EventDecrement 2021-01-01 1 (Refined 42.00),Account {accountKind = AccountKindAsset, accountObject = ("Cash",1)}) :| []))+-- >>> Aeson.decode json == Just posting+-- True+newtype Posting a o (s :: Nat) = Posting (NE.NonEmpty (Event o s, Account a))+ deriving (Eq, Generic, Ord, Show)+ deriving (FromJSON, ToJSON)+ via Vanilla (Posting a o s)+++postingEvents :: (KnownNat s) => Posting a o s -> [o]+postingEvents (Posting es) = eventObject . fst <$> NE.toList es+++post :: (KnownNat s) => Posting a o s -> [(Account a, Entry o s)]+post (Posting xs) = go (NE.toList xs)+ where+ go [] = []+ go ((ev, ac) : ys) = (ac, buildEntry ev (accountKind ac)) : go ys
+ src/Haspara/Accounting/Types.hs view
@@ -0,0 +1,7 @@+module Haspara.Accounting.Types where++import Haspara.Quantity (Quantity)+import Refined (NonNegative, Refined)+++type UnsignedQuantity s = Refined NonNegative (Quantity s)