(ns auto-ap.graphql.ledger (:require [auto-ap.datomic :refer [uri remove-nils]] [auto-ap.datomic.ledger :as l] [auto-ap.datomic.vendors :as d-vendors] [auto-ap.datomic.accounts :as a] [auto-ap.utils :refer [by dollars=]] [auto-ap.time :refer [parse iso-date]] [auto-ap.graphql.utils :refer [->graphql <-graphql limited-clients assert-admin result->page]] [clj-time.coerce :as coerce] [clojure.string :as str] [clj-time.core :as time] [auto-ap.parse.util :as parse] [datomic.api :as d] [auto-ap.parse.templates :as t] [auto-ap.datomic.clients :as d-clients])) (defn get-ledger-page [context args value] (let [args (assoc args :id (:id context)) [journal-entries journal-entries-count] (l/get-graphql (<-graphql args))] (result->page journal-entries journal-entries-count :journal_entries args))) ;; TODO a better way to do this might be to accumulate ALL credits and ALL debits, and then just do for credits: balance = credits - debits. and for debits balance = debits - credits (defn credit-account? [account] (or (#{:account-type/liability :account-type/equity :account-type/revenue} (:db/ident (:account/type account))) (#{:bank-account-type/credit} (-> account :bank-account/type :db/ident )))) (defn debit-account? [account] (or (#{:account-type/asset :account-type/dividend :account-type/expense} (:db/ident (:account/type account))) (#{:bank-account-type/check} (-> account :bank-account/type :db/ident )))) (defn expense-account? [account] (= :account-type/expense (:db/ident (:account/type account)))) (defn roll-up [results] (->> results (mapcat :journal-entry/line-items) (group-by (juxt :journal-entry-line/account :journal-entry-line/location)) (reduce-kv (fn [result [account location] line-items] ;; TODO fix #_(when-not (or (:bank-account/name account) (:account/name account)) (println "WARNING " account line-items)) (conj result {:name (str (or (:bank-account/name account) (:account/name account)) (when-not (#{"A" } location) (str "-" location))) :location location :id (str (:db/id account) "-" location) :numeric-code (or (:account/numeric-code account) (and (#{:bank-account-type/check} (:db/ident (:bank-account/type account))) 1100) (and (#{:bank-account-type/credit} (:db/ident (:bank-account/type account))) 2800)) :account-type (or (:db/ident (:account/type account)) ({:bank-account-type/check :asset :bank-account-type/credit :liability} (:db/ident (:bank-account/type account)))) :amount (reduce + 0 (map (fn [line-item] (cond (and (credit-account? account) (:journal-entry-line/debit line-item)) (- (:journal-entry-line/debit line-item)) (and (credit-account? account) (:journal-entry-line/credit line-item)) (:journal-entry-line/credit line-item) (and (debit-account? account) (:journal-entry-line/debit line-item)) (:journal-entry-line/debit line-item) (and (debit-account? account) (:journal-entry-line/credit line-item)) (- (:journal-entry-line/credit line-item)) :else 0.0)) line-items))})) []))) (defn get-balance-sheet [context args value] (let [args (assoc args :id (:id context)) [results] (l/get-graphql {:client-id (:client_id args) :date-range {:end (coerce/to-date (:date args))} :count Integer/MAX_VALUE}) [comparable-results] (l/get-graphql {:client-id (:client_id args) :to-date (coerce/to-date (time/minus (:date args) (time/years 1))) :count Integer/MAX_VALUE})] (->graphql {:balance-sheet-accounts (roll-up results) :comparable-balance-sheet-accounts (roll-up comparable-results)}))) (defn get-profit-and-loss [context args value] (let [args (assoc args :id (:id context)) pnl (fn [from-date to-date] (println "FROM" from-date to-date) (let [[starting-results] (l/get-graphql {:client-id (:client_id args) :date-range {:end (-> from-date (time/minus (time/seconds 1)) coerce/to-date)} :count Integer/MAX_VALUE}) [ending-results] (l/get-graphql {:client-id (:client_id args) :date-range {:end (coerce/to-date to-date)} :count Integer/MAX_VALUE}) starting-accounts (by :id (roll-up starting-results)) ending-accounts (by :id (roll-up ending-results))] (reduce-kv (fn [results k v] (conj results (update v :amount (fn [amt] (- amt (get-in starting-accounts [k :amount] 0)))))) [] ending-accounts)))] (->graphql {:balance-sheet-accounts (pnl (coerce/to-date-time (:start (:date_range args))) (coerce/to-date-time (:end (:date_range args)))) :comparable-balance-sheet-accounts (pnl (time/minus (coerce/to-date-time (:start (:date_range args))) (time/years 1)) (time/minus (coerce/to-date-time (:end (:date_range args))) (time/years 1)))}))) #_(get-profit-and-loss nil {:client_id [:client/code "CBC"] :from_date "2018-01-01" :to_date "2019-04-01"} nil) (defn assoc-error [f] (fn [entry] (try (f entry) (catch Exception e (assoc entry :error (.getMessage e)))))) (defn import-ledger [context args value] (assert-admin (:id context)) (let [all-vendors (by :vendor/name (d-vendors/get-graphql {})) 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 line_items]}] (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)) all-vendors (into all-vendors new-hidden-vendors) all-accounts (transduce (map :account/code) conj #{} (a/get-accounts)) transaction (doall (map (assoc-error (fn [entry] (let [entry (-> entry (update :amount #(Double/parseDouble %)) (update :line_items (fn [lis] (mapv (fn [li ] (-> li (update :debit #(Double/parseDouble (if (str/blank? %) "0" %))) (update :credit #(Double/parseDouble (if (str/blank? %) "0" %))))) lis))))] (let [vendor (all-vendors (:vendor_name entry))] (when-not (all-clients (:client_code entry)) (throw (Exception. (str "Client '" (:client_code entry )"' not found.")) )) (when-not vendor (throw (Exception. (str "Vendor '" (:vendor_name entry) "' not found.")))) (when-not (dollars= (doto (reduce + 0.0 (map :debit (:line_items entry)))) (reduce + 0.0 (map :credit (:line_items entry)))) (throw (Exception. (str "Debits '" (reduce + 0 (map :debit (:line_items entry))) "' and credits '" (reduce + 0 (map :credit (:line_items entry))) "' do not add up.")))) (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 (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] (when-not (get (get all-client-locations (:client_code entry)) (:location ea)) (throw (Exception. (str "Location '" (:location ea) "' not found.")))) (when (< (or (:debit ea) (:credit ea)) 0.0) (throw (Exception. (str (or (:debit ea) (:credit ea)) "must be greater than 0.")))) (when (and (not (all-accounts (:account_identifier ea))) (not (get (get all-client-bank-accounts (:client_code entry)) (:account_identifier ea)))) (throw (Exception. (str "Account '" (:account_identifier ea) "' not found.")))) (remove-nils {:journal-entry-line/account (if (re-matches #"^[0-9]+$" (:account_identifier ea)) (:db/id (a/get-account-by-numeric-code-and-sets (Integer/parseInt (:account_identifier ea)) ["default"])) [:bank-account/code (:account_identifier ea)]) :journal-entry-line/location (:location ea) :journal-entry-line/debit (when (> (:debit ea) 0) (:debit ea)) :journal-entry-line/credit (when (> (:credit ea) 0) (:credit ea))})) (:line_items entry)) :journal-entry/cleared true}))))) (:entries args))) errors (filter :error transaction) success (filter (comp not :error) transaction) retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:journal-entry/external-id x)]]) success)] (run! (fn [batch] (println "transacting retraction batch") @(d/transact (d/connect uri) batch)) (partition-all 100 retraction)) (run! (fn [batch] (println "transacting success batch") @(d/transact (d/connect uri) batch)) (partition-all 100 success)) {:successful (map (fn [x] {:external_id (:journal-entry/external-id x)}) success) :existing [] :errors (map (fn [x] {:external_id (:external_id x) :error (:error x)}) errors)}))