823 lines
49 KiB
Clojure
823 lines
49 KiB
Clojure
(ns auto-ap.graphql.ledger
|
|
(:require
|
|
[auto-ap.datomic :refer [audit-transact-batch conn remove-nils uri]]
|
|
[auto-ap.datomic.accounts :as a]
|
|
[auto-ap.datomic.clients :as d-clients]
|
|
[auto-ap.datomic.ledger :as l]
|
|
[auto-ap.ledger.reports :as l-reports]
|
|
[auto-ap.datomic.vendors :as d-vendors]
|
|
[auto-ap.graphql.utils
|
|
:refer [->graphql <-graphql assert-admin assert-can-see-client result->page]]
|
|
[auto-ap.parse.util :as parse]
|
|
[auto-ap.pdf.ledger :refer [print-balance-sheet print-pnl print-journal-detail-report]]
|
|
[auto-ap.utils :refer [by dollars= heartbeat]]
|
|
[clj-time.coerce :as coerce]
|
|
[clj-time.core :as t]
|
|
[clojure.tools.logging :as log]
|
|
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
|
|
[datomic.api :as d]
|
|
[mount.core :as mount]
|
|
[unilog.context :as lc]
|
|
[yang.scheduler :as scheduler]))
|
|
|
|
(mount/defstate running-balance-cache
|
|
:start (atom {}))
|
|
|
|
(defn get-ledger-page [context args _]
|
|
(let [args (assoc args :id (:id context))
|
|
[journal-entries journal-entries-count] (l/get-graphql (assoc (<-graphql (:filters args))
|
|
:id (:id context)))
|
|
|
|
journal-entries (mapv
|
|
(fn [je]
|
|
(-> je
|
|
(update :journal-entry/original-entity :db/id)
|
|
(update :journal-entry/line-items
|
|
(fn [jels]
|
|
(mapv
|
|
(fn [jel]
|
|
(assoc jel :running-balance (get-in @running-balance-cache [(:db/id (:journal-entry/client je))
|
|
(:db/id jel)])))
|
|
|
|
jels)))))
|
|
journal-entries)]
|
|
(result->page journal-entries journal-entries-count :journal_entries (:filters args))))
|
|
|
|
|
|
(defn roll-up-until
|
|
([lookup-account all-ledger-entries end-date]
|
|
(roll-up-until lookup-account all-ledger-entries end-date nil))
|
|
([lookup-account all-ledger-entries end-date start-date]
|
|
(->> all-ledger-entries
|
|
(filter (fn [[d]]
|
|
(if start-date
|
|
(and
|
|
(>= (compare d start-date) 0)
|
|
(<= (compare d end-date) 0))
|
|
(<= (compare d end-date) 0))))
|
|
(reduce
|
|
(fn [acc [_ _ account location debit credit]]
|
|
(-> acc
|
|
(update-in [[location account] :debit] (fnil + 0.0) debit)
|
|
(update-in [[location account] :credit] (fnil + 0.0) credit)
|
|
(update-in [[location account] :count] (fnil + 0) 1))
|
|
)
|
|
{})
|
|
(reduce-kv
|
|
(fn [acc [location account-id] {:keys [debit credit count]}]
|
|
(let [account (lookup-account account-id)
|
|
account-type (:account_type account)]
|
|
|
|
(conj acc (merge {:id (str account-id "-" location)
|
|
:location (or location "")
|
|
:count count
|
|
:amount (if account-type (if (#{:account-type/asset
|
|
:account-type/dividend
|
|
:account-type/expense} account-type)
|
|
(- debit credit)
|
|
(- credit debit))
|
|
0.0)}
|
|
account)))
|
|
)
|
|
|
|
[]))))
|
|
|
|
(defn build-account-lookup [client-id]
|
|
(let [accounts (by :db/id (map first (d/query {:query {:find ['(pull ?e [:db/id :account/name
|
|
:account/numeric-code
|
|
{:account/type [:db/ident]
|
|
:account/client-overrides [:account-client-override/client :account-client-override/name]}
|
|
])]
|
|
:in ['$]
|
|
:where ['[?e :account/name]]}
|
|
:args [(d/db (d/connect uri) )]})))
|
|
|
|
bank-accounts (by :db/id (map first (d/query {:query {:find ['(pull ?e [:db/id :bank-account/name :bank-account/numeric-code {:bank-account/type [:db/ident]}])]
|
|
:in ['$]
|
|
:where ['[?e :bank-account/name]]}
|
|
:args [(d/db (d/connect uri))]})))
|
|
overrides-by-client (->> accounts
|
|
vals
|
|
(mapcat (fn [a]
|
|
(map (fn [o]
|
|
[[(:db/id a) (:db/id (:account-client-override/client o))]
|
|
(:account-client-override/name o)])
|
|
(:account/client-overrides a))
|
|
) )
|
|
(into {} ))]
|
|
(fn [a]
|
|
{:name (or (:bank-account/name (bank-accounts a))
|
|
(overrides-by-client [a client-id])
|
|
(:account/name (accounts a)))
|
|
:account_type (or (:db/ident (:account/type (accounts a)))
|
|
({:bank-account-type/check :account-type/asset
|
|
:bank-account-type/cash :account-type/asset
|
|
:bank-account-type/credit :account-type/liability}
|
|
(:db/ident (:bank-account/type (bank-accounts a)))))
|
|
:numeric_code (or (:account/numeric-code (accounts a))
|
|
(:bank-account/numeric-code (bank-accounts a)))
|
|
:client_id client-id})))
|
|
|
|
(defn full-ledger-for-client [client-id]
|
|
(->> (d/query
|
|
{:query {:find ['?d '?jel '?account '?location '?debit '?credit]
|
|
:in ['$ '?client-id]
|
|
:where '[[?e :journal-entry/client ?client-id]
|
|
[?e :journal-entry/date ?d]
|
|
[?e :journal-entry/line-items ?jel]
|
|
(or-join [?e]
|
|
(and [?e :journal-entry/original-entity ?i]
|
|
(or-join [?e ?i]
|
|
(and
|
|
[?i :transaction/bank-account ?b]
|
|
(or [?b :bank-account/include-in-reports true]
|
|
(not [?b :bank-account/include-in-reports])))
|
|
(not [?i :transaction/bank-account])))
|
|
(not [?e :journal-entry/original-entity ]))
|
|
[(get-else $ ?jel :journal-entry-line/account :account/unknown) ?account]
|
|
[(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit ]
|
|
[(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit]
|
|
[(get-else $ ?jel :journal-entry-line/location "") ?location]]
|
|
}
|
|
:args [(d/db (d/connect uri)) client-id]})
|
|
(sort-by first)))
|
|
|
|
(defn get-balance-sheet [context args _]
|
|
(let [client-id (:client_id args)
|
|
_ (assert-can-see-client (:id context) client-id)
|
|
end-date (coerce/to-date (:date args))
|
|
comparable-date (coerce/to-date (:comparison_date args))
|
|
all-ledger-entries (full-ledger-for-client client-id)
|
|
lookup-account (build-account-lookup client-id)]
|
|
(log/info "Running balance sheet with " args)
|
|
|
|
(cond-> {:balance-sheet-accounts (roll-up-until lookup-account all-ledger-entries end-date)}
|
|
(:include_comparison args) (assoc :comparable-balance-sheet-accounts (roll-up-until lookup-account all-ledger-entries comparable-date))
|
|
true ->graphql)))
|
|
|
|
(defn get-profit-and-loss [context args _]
|
|
(let [client-id (:client_id args)
|
|
client-ids (or (some-> client-id vector)
|
|
(filter identity (:client_ids args)))
|
|
_ (when (not (seq client-ids))
|
|
(throw (ex-info "Please select a client." {:validation-error "Please select a client."})))
|
|
_ (doseq [client-id client-ids]
|
|
(assert-can-see-client (:id context) client-id))
|
|
_ (when (and (:include_deltas args)
|
|
(:column_per_location args))
|
|
(throw (ex-info "Please select one of 'Include deltas' or 'Column per location'" {:validation-error "Please select one of 'Include deltas' or 'Column per location'"})))
|
|
all-ledger-entries (->> client-ids
|
|
(map (fn [client-id]
|
|
[client-id (full-ledger-for-client client-id)]))
|
|
(into {}))
|
|
lookup-account (->> client-ids
|
|
(map (fn [client-id]
|
|
[client-id (build-account-lookup client-id)]))
|
|
(into {}))]
|
|
(->graphql
|
|
{:periods
|
|
(->> (:periods args)
|
|
(mapv (fn [{:keys [start end]}]
|
|
{:accounts (mapcat
|
|
#(roll-up-until (lookup-account %) (all-ledger-entries %) (coerce/to-date end) (coerce/to-date start) )
|
|
client-ids)})))})))
|
|
|
|
(defn profit-and-loss-pdf [context args value]
|
|
(let [data (get-profit-and-loss context args value)
|
|
result (print-pnl (:id context) args data)]
|
|
|
|
(->graphql result)))
|
|
|
|
(defn balance-sheet-pdf [context args value]
|
|
(let [data (get-balance-sheet context args value)
|
|
result (print-balance-sheet (:id context) args data)]
|
|
|
|
(->graphql result)))
|
|
|
|
|
|
(defn assoc-error [f]
|
|
(fn [entry]
|
|
(try
|
|
(f entry)
|
|
(catch Exception e
|
|
(println e)
|
|
(assoc entry :error (.getMessage e)
|
|
:status (or (:status (ex-data e))
|
|
:error))))))
|
|
|
|
(defn all-ids-not-locked [all-ids]
|
|
(->> all-ids
|
|
(d/q '[:find [?t ...]
|
|
:in $ [?t ...]
|
|
:where
|
|
[?t :journal-entry/client ?c]
|
|
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
|
|
[?t :journal-entry/date ?d]
|
|
[(>= ?d ?lu)]]
|
|
(d/db conn))))
|
|
|
|
(defn delete-external-ledger [context args _]
|
|
(let [_ (assert-admin (:id context))
|
|
args (assoc args :id (:id context))
|
|
ids (some-> (:filters args)
|
|
(assoc :only-external true)
|
|
(<-graphql)
|
|
(assoc :per-page Integer/MAX_VALUE)
|
|
(#(l/raw-graphql-ids (d/db conn) %))
|
|
:ids)
|
|
specific-ids (l/filter-ids (:ids args))
|
|
all-ids (all-ids-not-locked (into (set ids) specific-ids))]
|
|
(if (> (count all-ids) 1000)
|
|
{:message (str "You can only delete 1000 ledger entries at a time.")}
|
|
(do
|
|
(log/info "Deleting " (count all-ids) args)
|
|
(audit-transact-batch
|
|
(map (fn [i]
|
|
[:db/retractEntity i])
|
|
all-ids)
|
|
(:id context))
|
|
{:message (str "Succesfully deleted " (count all-ids) " ledger entries.")}))))
|
|
|
|
(defn import-ledger [context args _]
|
|
(assert-admin (:id context))
|
|
(lc/with-context {:area "import ledger"}
|
|
(let [all-vendors (->> (d/q '[:find [?e ...]
|
|
:in $
|
|
:where [?e :vendor/name]]
|
|
(d/db conn))
|
|
(d/pull-many (d/db conn) d-vendors/default-read)
|
|
(by :vendor/name))
|
|
all-clients (by :client/code (d-clients/get-all ))
|
|
all-client-bank-accounts (reduce
|
|
(fn [acc client]
|
|
(assoc acc (:client/code client)
|
|
(set (->> (:client/bank-accounts client)
|
|
(map :bank-account/code)
|
|
))))
|
|
{}
|
|
(d-clients/get-all))
|
|
all-client-locations (reduce
|
|
(fn [acc client]
|
|
(assoc acc (:client/code client)
|
|
(-> (set (:client/locations client))
|
|
(conj "HQ")
|
|
(conj "A"))))
|
|
{}
|
|
(d-clients/get-all))
|
|
new-hidden-vendors (reduce
|
|
(fn [new-vendors {:keys [vendor_name]}]
|
|
(if (or (all-vendors vendor_name)
|
|
(new-vendors vendor_name))
|
|
new-vendors
|
|
(assoc new-vendors vendor_name
|
|
{:vendor/name vendor_name
|
|
:vendor/hidden true
|
|
:db/id vendor_name})))
|
|
{}
|
|
(:entries args))
|
|
_ (audit-transact-batch (vec (vals new-hidden-vendors)) (:id context))
|
|
all-vendors (->> (d/q '[:find [?e ...]
|
|
:in $
|
|
:where [?e :vendor/name]]
|
|
(d/db conn))
|
|
(d/pull-many (d/db conn) d-vendors/default-read)
|
|
(by :vendor/name))
|
|
all-accounts (transduce (map (comp str :account/numeric-code)) conj #{} (a/get-accounts))
|
|
transaction (doall (map
|
|
(assoc-error (fn [entry]
|
|
(let [vendor (all-vendors (:vendor_name entry))]
|
|
(when-not (all-clients (:client_code entry))
|
|
(throw (ex-info (str "Client '" (:client_code entry )"' not found.") {:status :error}) ))
|
|
(when-not vendor
|
|
(throw (ex-info (str "Vendor '" (:vendor_name entry) "' not found.") {:status :error})))
|
|
(when-not (re-find #"\d{1,2}/\d{1,2}/\d{4}" (:date entry))
|
|
(throw (ex-info (str "Date must be MM/dd/yyyy") {:status :error})))
|
|
(when-let [locked-until (:client/locked-until (all-clients (:client_code entry)))]
|
|
(when (and (not (t/after? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))))
|
|
(coerce/to-date-time locked-until)))
|
|
(not (t/equal? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))))
|
|
(coerce/to-date-time locked-until))))
|
|
(throw (ex-info (str "Client's data is locked until " locked-until) {:status :error}))))
|
|
|
|
(when-not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry)))
|
|
(reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line_items entry))))
|
|
(throw (ex-info (str "Debits '"
|
|
(reduce (fnil + 0.0 0.0) 0 (map :debit (:line_items entry)))
|
|
"' and credits '"
|
|
(reduce (fnil + 0.0 0.0) 0 (map :credit (:line_items entry)))
|
|
"' do not add up.")
|
|
{:status :error})))
|
|
(when (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry)))
|
|
0.0)
|
|
(throw (ex-info (str "Cannot have ledger entries that total $0.00")
|
|
{:status :ignored})))
|
|
(assoc entry
|
|
:status :success
|
|
:tx
|
|
(remove-nils
|
|
{:journal-entry/source (:source entry)
|
|
:journal-entry/client [:client/code (:client_code entry)]
|
|
:journal-entry/date (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry)))
|
|
:journal-entry/external-id (:external_id entry)
|
|
:journal-entry/vendor (:db/id (all-vendors (:vendor_name entry)))
|
|
:journal-entry/amount (:amount entry)
|
|
:journal-entry/note (:note entry)
|
|
:journal-entry/cleared-against (:cleared_against entry)
|
|
|
|
:journal-entry/line-items
|
|
(mapv (fn [ea]
|
|
(let [debit (or (:debit ea) 0.0)
|
|
credit (or (:credit ea) 0.0)]
|
|
(when (and (not (get
|
|
(get all-client-locations (:client_code entry))
|
|
(:location ea)))
|
|
(not= "A" (:location ea)))
|
|
(throw (ex-info (str "Location '" (:location ea) "' not found.")
|
|
{:status :error})))
|
|
(when (and (<= debit 0.0)
|
|
(<= credit 0.0))
|
|
(throw (ex-info (str "Line item amount " (or debit credit) " must be greater than 0.")
|
|
{:status :error})))
|
|
(when (and (not (all-accounts (:account_identifier ea)))
|
|
(not (get
|
|
(get all-client-bank-accounts (:client_code entry))
|
|
(:account_identifier ea))))
|
|
(throw (ex-info (str "Account '" (:account_identifier ea) "' not found.")
|
|
{:status :error})))
|
|
(let [matching-account (when (re-matches #"^[0-9]+$" (:account_identifier ea))
|
|
(a/get-account-by-numeric-code-and-sets (Integer/parseInt (:account_identifier ea)) ["default"]))]
|
|
(when (and matching-account
|
|
(:account/location matching-account)
|
|
(not= (:account/location matching-account)
|
|
(:location ea)))
|
|
(throw (ex-info (str "Account '"
|
|
(:account/numeric-code matching-account)
|
|
"' requires location '"
|
|
(:account/location matching-account)
|
|
"' but got '"
|
|
(:location ea)
|
|
"'")
|
|
{:status :error})))
|
|
|
|
(when (and matching-account
|
|
(not (:account/location matching-account))
|
|
(= "A" (:location ea)))
|
|
(throw (ex-info (str "Account '"
|
|
(:account/numeric-code matching-account)
|
|
"' cannot use location '"
|
|
(:location ea)
|
|
"'")
|
|
{:status :error})))
|
|
(remove-nils (cond-> {:journal-entry-line/location (:location ea)
|
|
:journal-entry-line/debit (when (> debit 0)
|
|
debit)
|
|
:journal-entry-line/credit (when (> credit 0)
|
|
credit)}
|
|
matching-account (assoc :journal-entry-line/account (:db/id matching-account))
|
|
(not matching-account) (assoc :journal-entry-line/account [:bank-account/code (:account_identifier ea)]))))))
|
|
(:line_items entry))
|
|
|
|
:journal-entry/cleared true})))))
|
|
(:entries args)))
|
|
errors (filter #(= (:status %) :error) transaction)
|
|
ignored (filter #(= (:status %) :ignored) transaction)
|
|
success (filter #(= (:status %) :success) transaction)
|
|
retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:external_id x)]])
|
|
success)
|
|
ignore-retraction (->> ignored
|
|
(map :external_id )
|
|
(d/q '[:find [?je ...]
|
|
:in $ [?ei ...]
|
|
:where [?je :journal-entry/external-id ?ei]]
|
|
(d/db conn)
|
|
)
|
|
(map (fn [je] [:db/retractEntity je])))]
|
|
(log/info "manual ledger import has " (count success) " new rows")
|
|
(log/info errors)
|
|
|
|
|
|
(audit-transact-batch retraction (:id context))
|
|
(when (seq ignore-retraction)
|
|
(audit-transact-batch ignore-retraction (:id context)))
|
|
#_(log/info (map :tx success))
|
|
(audit-transact-batch (map :tx success) (:id context))
|
|
|
|
{:successful (map (fn [x] {:external_id (:external_id x)}) success)
|
|
:ignored (map (fn [x]
|
|
{:external_id (:external_id x)})
|
|
ignored)
|
|
:existing []
|
|
:errors (map (fn [x] {:external_id (:external_id x)
|
|
:error (:error x)}) errors)})))
|
|
|
|
(defn build-running-balance
|
|
([lookup-account all-ledger-entries]
|
|
(->> all-ledger-entries
|
|
(reduce
|
|
(fn [[rollup cache] [_ _ jel account location debit credit]]
|
|
(let [rollup (-> rollup
|
|
(update-in [[location account] :debit] (fnil + 0.0) debit)
|
|
(update-in [[location account] :credit] (fnil + 0.0) credit)
|
|
(update-in [[location account] :count] (fnil + 0) 1))]
|
|
[rollup
|
|
(assoc cache jel (assoc (get rollup [location account]) :account-id account))]))
|
|
[{} {}])
|
|
(second)
|
|
(reduce-kv
|
|
(fn [acc jel {:keys [debit credit account-id]}]
|
|
(let [account (lookup-account account-id)
|
|
account-type (:account_type account)]
|
|
(assoc acc jel
|
|
(if account-type (if (#{:account-type/asset
|
|
:account-type/dividend
|
|
:account-type/expense} account-type)
|
|
(- debit credit)
|
|
(- credit debit))
|
|
0.0))))
|
|
|
|
{}))))
|
|
|
|
(defn running-balance-for [client-id]
|
|
(let [lookup-account (build-account-lookup client-id)]
|
|
(->> (d/query
|
|
{:query {:find ['?d '?e '?jel '?account '?location '?debit '?credit]
|
|
:in ['$ '?client-id]
|
|
:where '[[?e :journal-entry/client ?client-id]
|
|
[?e :journal-entry/date ?d]
|
|
[?e :journal-entry/line-items ?jel]
|
|
(or-join [?e]
|
|
(and [?e :journal-entry/original-entity ?i]
|
|
(or-join [?e ?i]
|
|
(and
|
|
[?i :transaction/bank-account ?b]
|
|
(or [?b :bank-account/include-in-reports true]
|
|
(not [?b :bank-account/include-in-reports])))
|
|
(not [?i :transaction/bank-account])))
|
|
(not [?e :journal-entry/original-entity ]))
|
|
[(get-else $ ?jel :journal-entry-line/account :account/unknown) ?account]
|
|
[(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit ]
|
|
[(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit]
|
|
[(get-else $ ?jel :journal-entry-line/location "") ?location]]
|
|
}
|
|
:args [(d/db conn) client-id]})
|
|
(sort-by (juxt first second))
|
|
(build-running-balance lookup-account))))
|
|
|
|
(def last-run-running-balance (atom nil))
|
|
|
|
(defn build-running-balance-cache []
|
|
(let [clients-needing-refresh (if-let [last-run @last-run-running-balance]
|
|
(->> (d/query
|
|
{:query {:find ['?v]
|
|
:in ['$ '?log '?since '?till]
|
|
:where ['[(tx-ids ?log ?since ?till) [?tx ...]]
|
|
'[$ _ :journal-entry/client ?v ?tx]]}
|
|
:args [(d/history (d/db conn))
|
|
(d/log conn)
|
|
last-run
|
|
(java.util.Date.)]})
|
|
(map first)
|
|
(into #{}))
|
|
(into #{} (map :db/id (d-clients/get-all))))
|
|
starting (java.util.Date.)]
|
|
(log/info (count clients-needing-refresh) "Clients need their balance cache refreshed.")
|
|
(swap! running-balance-cache
|
|
merge
|
|
(reduce
|
|
(fn [acc client]
|
|
(log/info "Computing running balance cache for " (:client/code (d/entity (d/db conn) client)))
|
|
(assoc acc client (running-balance-for client)))
|
|
{}
|
|
clients-needing-refresh))
|
|
(log/info "Done refreshing " (count clients-needing-refresh) " client caches")
|
|
(reset! last-run-running-balance starting)))
|
|
|
|
|
|
|
|
(defn refresh-running-balance-cache []
|
|
(build-running-balance-cache))
|
|
|
|
|
|
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
|
(mount/defstate running-balance-cache-worker
|
|
:start (scheduler/every (* 15 60 1000) (heartbeat refresh-running-balance-cache "running-balance-cache"))
|
|
:stop (scheduler/stop running-balance-cache-worker))
|
|
|
|
|
|
(defn get-journal-detail-report [context input _]
|
|
(let [category-totals (atom {})
|
|
base-categories (into []
|
|
(for [client-id (:client_ids input)
|
|
:let [_ (assert-can-see-client (:id context) client-id)
|
|
account-lookup (build-account-lookup client-id)
|
|
c (d/pull (d/db conn) '[:client/locations] client-id)]
|
|
location (:client/locations c)
|
|
category (:categories input)
|
|
:let [category (<-graphql category)
|
|
all-journal-entries (->> (get-ledger-page context
|
|
{:filters {:client_id client-id
|
|
:location location
|
|
:date_range (:date_range input)
|
|
:from_numeric_code (get-in l-reports/ranges [category 0])
|
|
:to_numeric_code (get-in l-reports/ranges [category 1])
|
|
:per_page Integer/MAX_VALUE}}
|
|
nil)
|
|
:journal_entries
|
|
(mapcat (fn [je]
|
|
(->> (je :line_items)
|
|
(filter (fn [jel]
|
|
(when-let [account (account-lookup (:id (:account jel)))]
|
|
(and
|
|
(<= (get-in l-reports/ranges [category 0])
|
|
(:numeric_code account)
|
|
(get-in l-reports/ranges [category 1]))
|
|
(= location (:location jel)))))
|
|
)
|
|
(map (fn [jel ]
|
|
{:date (:date je)
|
|
:debit (:debit jel)
|
|
:credit (:credit jel)
|
|
:description (or (:name (:vendor je))
|
|
(:alternate_description je))
|
|
:account (:account jel)
|
|
:location (:location jel)})))))
|
|
(into []))
|
|
_ (swap! category-totals assoc-in [client-id location category]
|
|
(- (or (reduce + 0.0 (map #(or (:credit %) 0.0) all-journal-entries)) 0.0)
|
|
(or (reduce + 0.0 (map #(or (:debit %) 0.0) all-journal-entries)) 0.0)) )
|
|
journal-entries-by-account (group-by #(account-lookup (get-in % [:account :id])) all-journal-entries)]
|
|
[account journal-entries] (conj (vec journal-entries-by-account) [nil all-journal-entries])
|
|
:let [journal-entries (first (reduce
|
|
(fn [[acc last-je] je]
|
|
(let [next-je (assoc je :running_balance
|
|
(- (+ (or (:running_balance last-je 0.0) 0.0)
|
|
(or (:credit je 0.0) 0.0))
|
|
(or (:debit je 0.0) 0.0)))]
|
|
[(conj acc next-je) next-je]))
|
|
[]
|
|
(sort-by :date journal-entries)))]]
|
|
{:category (->graphql category)
|
|
:client_id client-id
|
|
:location location
|
|
:account (or account
|
|
{:name (str "All " (name category))})
|
|
:journal_entries (when account (sort-by :date journal-entries))
|
|
:total (- (or (reduce + 0.0 (map #(or (:credit %) 0.0) journal-entries)) 0.0)
|
|
(or (reduce + 0.0 (map #(or (:debit %) 0.0) journal-entries)) 0.0))}))
|
|
result {:categories
|
|
(into base-categories
|
|
(for [client-id (:client_ids input)
|
|
:let [_ (assert-can-see-client (:id context) client-id)
|
|
account-lookup (build-account-lookup client-id)
|
|
c (d/pull (d/db conn) '[:client/locations] client-id)]
|
|
location (:client/locations c)
|
|
line [{:client_id client-id
|
|
:location location
|
|
:account {:name "Gross Profit"}
|
|
:journal_entries nil
|
|
:total (+ (get-in @category-totals [client-id location :sales] 0.0)
|
|
(get-in @category-totals [client-id location :cogs] 0.0)
|
|
(get-in @category-totals [client-id location :payroll] 0.0))}
|
|
{:client_id client-id
|
|
:location location
|
|
:account {:name "Overhead"}
|
|
:journal_entries nil
|
|
:total (+ (get-in @category-totals [client-id location :controllable] 0.0)
|
|
(get-in @category-totals [client-id location :fixed-overhead] 0.0)
|
|
(get-in @category-totals [client-id location :ownership-controllable] 0.0))}
|
|
{:client_id client-id
|
|
:location location
|
|
:account {:name "Net Profit"}
|
|
:journal_entries nil
|
|
:total (+ (get-in @category-totals [client-id location :sales] 0.0)
|
|
(get-in @category-totals [client-id location :cogs] 0.0)
|
|
(get-in @category-totals [client-id location :payroll] 0.0)
|
|
(get-in @category-totals [client-id location :controllable] 0.0)
|
|
(get-in @category-totals [client-id location :fixed-overhead] 0.0)
|
|
(get-in @category-totals [client-id location :ownership-controllable] 0.0))}]]
|
|
line))}]
|
|
result))
|
|
|
|
|
|
|
|
(defn journal-detail-report-pdf [context args value]
|
|
(let [data (get-journal-detail-report context args value)
|
|
result (print-journal-detail-report (:id context) args data)]
|
|
|
|
(->graphql result)))
|
|
|
|
|
|
(def objects
|
|
{:balance_sheet_account
|
|
{:fields {:id {:type 'String}
|
|
:amount {:type 'String}
|
|
:location {:type 'String}
|
|
:client_id {:type :id}
|
|
:count {:type 'Int}
|
|
:numeric_code {:type 'Int}
|
|
:account_type {:type :account_type}
|
|
:name {:type 'String}}}
|
|
|
|
:report_pdf
|
|
{:fields {:url {:type 'String}
|
|
:name {:type 'String}}}
|
|
|
|
:balance_sheet
|
|
{:fields {:balance_sheet_accounts {:type '(list :balance_sheet_account)}
|
|
:comparable_balance_sheet_accounts {:type '(list :balance_sheet_account)}}}
|
|
|
|
:profit_and_loss_report_period
|
|
{:fields {:accounts {:type '(list :balance_sheet_account)}}}
|
|
|
|
:profit_and_loss_report
|
|
{:fields {:periods {:type '(list :profit_and_loss_report_period)}}}
|
|
|
|
:journal_entry_line
|
|
{:fields {:id {:type :id}
|
|
:account {:type :account}
|
|
:location {:type 'String}
|
|
:debit {:type 'String}
|
|
:credit {:type 'String}
|
|
:running_balance {:type :money}}}
|
|
|
|
:journal_entry
|
|
{:fields {:id {:type :id}
|
|
:source {:type 'String}
|
|
:external_id {:type 'String}
|
|
:original_entity {:type :id}
|
|
:amount {:type 'String}
|
|
:note {:type 'String}
|
|
:cleared_against {:type 'String}
|
|
:client {:type :client}
|
|
:vendor {:type :vendor}
|
|
:alternate_description {:type 'String}
|
|
:date {:type 'String}
|
|
:line_items {:type '(list :journal_entry_line)}}}
|
|
|
|
:journal_detail_report_row
|
|
{:fields {:client {:type :client}
|
|
:description {:type 'String}
|
|
:date {:type 'String}
|
|
:account {:type :account}
|
|
:location {:type 'String}
|
|
:debit {:type 'String}
|
|
:credit {:type 'String}
|
|
:running_balance {:type :money}}}
|
|
|
|
:ledger_page
|
|
{:fields {:journal_entries {:type '(list :journal_entry)}
|
|
:count {:type 'Int}
|
|
:total {:type 'Int}
|
|
:start {:type 'Int}
|
|
:end {:type 'Int}}}
|
|
|
|
:import_ledger_entry_result
|
|
{:fields {:external_id {:type 'String}
|
|
:error {:type 'String}
|
|
:status {:type 'String}}}
|
|
|
|
:import_ledger_result
|
|
{:fields {:successful {:type '(list :import_ledger_entry_result)}
|
|
:existing {:type '(list :import_ledger_entry_result)}
|
|
:ignored {:type '(list :import_ledger_entry_result)}
|
|
:errors {:type '(list :import_ledger_entry_result)}}}
|
|
|
|
:journal_detail_report_category
|
|
{:fields {:category {:type :ledger_category}
|
|
:account {:type :account}
|
|
:total {:type :money}
|
|
:client_id {:type :id}
|
|
:location {:type 'String}
|
|
:journal_entries {:type '(list :journal_detail_report_row)}}}
|
|
:journal_detail_report
|
|
{:fields {:categories {:type '(list :journal_detail_report_category)}}}})
|
|
|
|
(def queries
|
|
{:balance_sheet {:type :balance_sheet
|
|
:args {:client_id {:type :id}
|
|
:include_comparison {:type 'Boolean}
|
|
:date {:type :iso_date}
|
|
:comparison_date {:type :iso_date}}
|
|
:resolve :get-balance-sheet}
|
|
|
|
:profit_and_loss {:type :profit_and_loss_report
|
|
:args {:client_id {:type :id}
|
|
:client_ids {:type '(list :id)}
|
|
:periods {:type '(list :date_range)}
|
|
:include_deltas {:type 'Boolean}
|
|
:column_per_location {:type 'Boolean}}
|
|
:resolve :get-profit-and-loss}
|
|
|
|
:journal_detail_report {:type :journal_detail_report
|
|
:args {:client_id {:type :id}
|
|
:client_ids {:type '(list :id)}
|
|
:date_range {:type :date_range}
|
|
:categories {:type '(list :ledger_category)}}
|
|
:resolve :get-journal-detail-report}
|
|
|
|
:journal_detail_report_pdf {:type :report_pdf
|
|
:args {:client_id {:type :id}
|
|
:client_ids {:type '(list :id)}
|
|
:date_range {:type :date_range}
|
|
:categories {:type '(list :ledger_category)}}
|
|
:resolve :journal-detail-report-pdf}
|
|
|
|
:profit_and_loss_pdf {:type :report_pdf
|
|
:args {:client_id {:type :id}
|
|
:client_ids {:type '(list :id)}
|
|
:periods {:type '(list :date_range)}
|
|
:include_deltas {:type 'Boolean}
|
|
:column_per_location {:type 'Boolean}}
|
|
:resolve :profit-and-loss-pdf}
|
|
|
|
:balance_sheet_pdf {:type :report_pdf
|
|
:args {:client_id {:type :id}
|
|
:include_comparison {:type 'Boolean}
|
|
:date {:type :iso_date}
|
|
:comparison_date {:type :iso_date}}
|
|
:resolve :balance-sheet-pdf}
|
|
|
|
:ledger_page {:type :ledger_page
|
|
:args {:filters {:type :ledger_filters}}
|
|
:resolve :get-ledger-page}})
|
|
|
|
(def mutations
|
|
{:import_ledger
|
|
{:type :import_ledger_result
|
|
:args {:entries {:type '(list :import_ledger_entry)}}
|
|
:resolve :mutation/import-ledger}
|
|
|
|
:delete_external_ledger
|
|
{:type :message
|
|
:args {:filters {:type :ledger_filters}
|
|
:ids {:type '(list :id)}}
|
|
:resolve :mutation/delete-external-ledger}})
|
|
|
|
(def input-objects
|
|
{:numeric_code_range
|
|
{:fields {:from {:type 'Int}
|
|
:to {:type 'Int}}}
|
|
:ledger_filters
|
|
{:fields {:client_id {:type :id}
|
|
:vendor_id {:type :id}
|
|
:account_id {:type :id}
|
|
:amount_lte {:type :money}
|
|
:amount_gte {:type :money}
|
|
:bank_account_id {:type :id}
|
|
:date_range {:type :date_range}
|
|
:location {:type 'String}
|
|
:numeric_code {:type '(list :numeric_code_range)}
|
|
:start {:type 'Int}
|
|
:per_page {:type 'Int}
|
|
:only_external {:type 'Boolean}
|
|
:external_id_like {:type 'String}
|
|
:source {:type 'String}
|
|
:sort {:type '(list :sort_item)}}}
|
|
|
|
:import_ledger_line_item
|
|
{:fields {:account_identifier {:type 'String}
|
|
:location {:type 'String}
|
|
:debit {:type :money}
|
|
:credit {:type :money}}}
|
|
:import_ledger_entry
|
|
{:fields {:source {:type 'String}
|
|
:external_id {:type 'String}
|
|
:client_code {:type 'String}
|
|
:date {:type 'String}
|
|
:vendor_name {:type 'String}
|
|
:amount {:type :money}
|
|
:note {:type 'String}
|
|
:cleared_against {:type 'String}
|
|
:line_items {:type '(list :import_ledger_line_item)}}}
|
|
})
|
|
|
|
(def enums
|
|
{:ledger_category {:values [{:enum-value :sales}
|
|
{:enum-value :cogs}
|
|
{:enum-value :payroll}
|
|
{:enum-value :controllable}
|
|
{:enum-value :fixed_overhead}
|
|
{:enum-value :ownership_controllable}]}})
|
|
|
|
|
|
(def resolvers
|
|
{:get-ledger-page get-ledger-page
|
|
:get-balance-sheet get-balance-sheet
|
|
:get-profit-and-loss get-profit-and-loss
|
|
:profit-and-loss-pdf profit-and-loss-pdf
|
|
:journal-detail-report-pdf journal-detail-report-pdf
|
|
:balance-sheet-pdf balance-sheet-pdf
|
|
:get-journal-detail-report get-journal-detail-report
|
|
:mutation/delete-external-ledger delete-external-ledger
|
|
:mutation/import-ledger import-ledger})
|
|
|
|
(defn attach [schema]
|
|
(->
|
|
(merge-with merge schema
|
|
{:objects objects
|
|
:queries queries
|
|
:mutations mutations
|
|
:input-objects input-objects
|
|
:enums enums})
|
|
(attach-resolvers resolvers)))
|