diff --git a/scratch-sessions/debug-more-duplicates.clj b/scratch-sessions/debug-more-duplicates.clj new file mode 100644 index 00000000..233fe178 --- /dev/null +++ b/scratch-sessions/debug-more-duplicates.clj @@ -0,0 +1,202 @@ +;; This buffer is for Clojure experiments and evaluation. + +;; Press C-j to evaluate the last expression. + +;; You can also press C-u C-j to evaluate the expression and pretty-print its result. + +(init-repl) + +(d/pull (d/db auto-ap.datomic/conn) '[* {:invoice/client [:client/code] :invoice/vendor [:vendor/name]}] 17592253143914) + +(entity-history 17592253143914 ) +(entity-history 13194206632809) + +(d/pull (d/db auto-ap.datomic/conn ) '[*] [:client/code "AFH"]) + +(auto-ap.parse/best-match (auto-ap.datomic.clients/get-all) "NACHMARKET" 0.5) + + +(println "hi") + +(user/init-repl) + +(def z (get-in (get-profit-and-loss {:id {:user/role "admin"}} + {:client_id 17592244691558 + :periods [{:start #inst "2021-01-01" + :end #inst "2021-12-31"}]} + nil) + [:periods 0 :accounts])) + + +(filter #(not (:numeric_code %)) z) + +(d/q '[:find (pull ?je [*]) + :in $ + :where [?je :journal-entry/client [:client/code "NGWH"]] + [?je :journal-entry/date ?d] + [?je :journal-entry/line-items ?jel] + (not [?jel :journal-entry-line/account])] + + (d/db conn)) + + +(d/pull (d/db conn) '[*] 17592253348380) +(user/entity-history 17592253348380) + +(d/pull (d/db conn) '[*] 13194206837275) + +[(d/pull (d/db conn) '[*] 17592249726033) + (d/pull (d/db conn) '[*] 17592248612376)] + +[(user/entity-history-with-revert 17592249726033) + (user/entity-history-with-revert 17592248612376)] + +(d/pull (d/db conn) '[*] 17592186046415) + +(user/entity-history 13194203214928) +;; => [[13194203214928 :db/txInstant #inst "2021-12-01T03:04:17.049-00:00"] +;; [13194203214928 :audit/user ":admin-Yodlee import"]] + + +(user/entity-history 13194202101271) +;; => [[13194202101271 :audit/user ":admin-Yodlee import"] +;; [13194202101271 :db/txInstant #inst "2021-11-25T16:32:46.542-00:00"]] + +(user/entity-history 13194203197770) +;; => [[13194203197770 :db/txInstant #inst "2021-12-01T02:07:33.416-00:00"]] + + + +(->> (d/db conn) + (d/q '[:find ?ba (count ?d) + :in $ + :where [?ba :bank-account/intuit-bank-account ?d]]) + #_(filter (fn [[_ x]] + (> x 1)))) + + +(d/pull (d/db auto-ap.datomic/conn) '[* {:transaction/bank-account [*]}] 17592204620282);; => {:transaction/bank-account +;; {:bank-account/sort-order 5, +;; :bank-account/include-in-reports true, +;; :bank-account/numeric-code 11307, +;; :bank-account/yodlee-account-id 16428403, +;; :bank-account/number "9392", +;; :bank-account/code "HIM-5", +;; :bank-account/current-balance 2063.2899999998335, +;; :bank-account/external-id 5, +;; :bank-account/name "HIM Citi Visa Costco 9392", +;; :db/id 17592186046421, +;; :bank-account/visible true, +;; :bank-account/type #:db{:id 17592186045425}, +;; :bank-account/bank-name "Citi Visa CC", +;; :bank-account/original-id "55-5"}, +;; :transaction/date #inst "2019-08-03T07:00:00.000-00:00", +;; :transaction/type "PURCHASE", +;; :transaction/client #:db{:id 17592186046415}, +;; :transaction/status "POSTED", +;; :transaction/account-id 16428403, +;; :db/id 17592204620282, +;; :transaction/id +;; "192dc451434cb0fc8698fae563618807fddfbc16e2aae8adc488d3dbb1cb5016", +;; :transaction/description-original "TAO TAO CAFE SUNNYVALE CA", +;; :transaction/approval-status #:db{:id 17592231963878}, +;; :transaction/amount -40.5, +;; :transaction/description-simple "TAO TAO CAFE SUNNYVALE CA"} + +;; => {:transaction/bank-account #:db{:id 17592186046421}, +;; :transaction/date #inst "2019-08-03T07:00:00.000-00:00", +;; :transaction/type "PURCHASE", +;; :transaction/client #:db{:id 17592186046415}, +;; :transaction/status "POSTED", +;; :transaction/account-id 16428403, +;; :db/id 17592204620282, +;; :transaction/id +;; "192dc451434cb0fc8698fae563618807fddfbc16e2aae8adc488d3dbb1cb5016", +;; :transaction/description-original "TAO TAO CAFE SUNNYVALE CA", +;; :transaction/approval-status #:db{:id 17592231963878}, +;; :transaction/amount -40.5, +;; :transaction/description-simple "TAO TAO CAFE SUNNYVALE CA"} + +(d/pull (d/db conn) '[*] 17592233127577) +;; => {:transaction/bank-account #:db{:id 17592186046421}, +;; :transaction/date #inst "2020-07-29T07:00:00.000-00:00", +;; :transaction/type "PURCHASE", +;; :transaction/client #:db{:id 17592186046415}, +;; :transaction/status "POSTED", +;; :transaction/account-id 16428403, +;; :db/id 17592233127577, +;; :transaction/id +;; "217ce419afa1daa217ca118d10ddcfa11e757788b48990bf43d92faf9250e531", +;; :transaction/description-original "TAO TAO CAFE SUNNYVALE CA", +;; :transaction/approval-status #:db{:id 17592231963878}, +;; :transaction/amount -49.7, +;; :transaction/description-simple "tao tao cafe"} + + +(user/init-repl) + +(d/pull (d/db conn) '[* {:transaction/approval-status [:db/ident]}] 17592251272764);; => {:transaction/bank-account #:db{:id 17592234230556}, +;; :transaction/date #inst "2021-12-06T08:00:00.000-00:00", +;; :transaction/check-number 387, +;; :transaction/client #:db{:id 17592234230520}, +;; :transaction/status "POSTED", +;; :db/id 17592251272764, +;; :transaction/id +;; "c15d3d1cad77b4a79510877f464b909e9ec9c8c4bfe9885008666526d6ca9885", +;; :transaction/description-original "Check Paid CHECK 387", +;; :transaction/approval-status +;; #:db{:ident :transaction-approval-status/suppressed}, +;; :transaction/amount -12411.0, +;; :transaction/raw-id +;; "2021-12-06T00:00:00.000-08:00-17592234230556-Check Paid CHECK 387--12411.0-0-17592234230520"} + + +(def z (auto-ap.yodlee.core/get-specific-transactions-with-date "16428403" "2021-01-01" "2021-12-31" (auto-ap.yodlee.core/get-auth-header))) + +(count z) +(csv/write-csv *out* + (->> z + (sort-by :date) + (map (fn [z] + (update z :date #(auto-ap.time/unparse (auto-ap.time/parse % auto-ap.time/iso-date) auto-ap.time/normal-date)))) + (map (fn [z] + [(:date z) (:original (:description z)) (:original (:description z)) "" (if (= "DEBIT" (:baseType z)) + (- (:amount (:amount z))) + (:amount (:amount z)))] + )) + + ) + + :separator \tab + ) + +(count z) + +(def z-p (d/q '[:find (pull ?t [:transaction/date :transaction/description-original :transaction/amount]) + :in $ ?ba + :where [?t :transaction/bank-account ?ba] + [?t :transaction/date ?d] + [(>= ?d #inst "2021-01-01T08:00:00")] + [(< ?d #inst "2022-01-01T08:00:00")] + (not [?t :transaction/approval-status :transaction-approval-status/suppressed])] + (d/db auto-ap.datomic/conn) + [:bank-account/code "HIM-5"])) + +(csv/write-csv *out* + (->> z-p + (map first) + (sort-by :transaction/date) + (map (fn [z] + (update z :transaction/date #(auto-ap.time/unparse-local (clj-time.coerce/to-date-time %) auto-ap.time/normal-date)))) + (map (fn [z] + [(:transaction/date z) (:transaction/description-original z) (:transaction/description-original z) "" (:transaction/amount z)] + )) + + ) + + :separator \tab + ) + + +(init-repl) + diff --git a/scratch-sessions/debug_sccb_transactions.clj b/scratch-sessions/debug_sccb_transactions.clj new file mode 100644 index 00000000..de660312 --- /dev/null +++ b/scratch-sessions/debug_sccb_transactions.clj @@ -0,0 +1,709 @@ +(ns debug-sccb-transactions + (:require [datomic.api :as d] + [auto-ap.datomic :refer [conn]] + [auto-ap.utils :refer [dollars=]] + [auto-ap.time :as atime :refer [iso-date]] + [clj-time.core :as t] + [clj-time.coerce :as coerce] + [clj-time.periodic :as periodic] + [clojure.data.csv :as csv] + + [auto-ap.yodlee.core :as y] + [auto-ap.yodlee.core2 :as y2])) + +(d/pull (d/db conn) '[*] [:bank-account/code "SCCB-9128CB"]) + +(def trans (y/get-specific-transactions-with-date 27995674 "2021-01-01" "2021-12-31" (y/get-auth-header))) + +(user/init-repl) + +[(->> trans + (sort-by :date) + (map (fn [t] + (if (= "DEBIT" (:baseType t)) + (- (:amount (:amount t))) + (:amount (:amount t))))) + (reduce + 0.0)) + (:amount (:runningBalance (first trans))) + (:amount (:runningBalance (last trans))) + (- (:amount (:runningBalance (last trans))) + (:amount (:runningBalance (first trans))))] + + +(defn y-tx->$ [b] + (if (= "DEBIT" (:baseType b)) + (- (:amount (:amount b))) + (:amount (:amount b)))) + + +(do + + (doseq [bank-account #_ (d/q '[:find [?yai ...] + :in $ + :where [_ :bank-account/yodlee-account-id ?yai]] + (d/db conn)) + :when (not= 0 bank-account)] + (try + (println + [bank-account (->> (y/get-specific-transactions-with-date bank-account "2021-01-01" "2021-12-31" (y/get-auth-header)) + (reverse) + (filter :runningBalance) + (partition 2 1) + (map (fn [[a b]] + (try + [(:amount (:runningBalance a)) + + (- (:amount (:runningBalance b)) + (if (= "DEBIT" (:baseType b)) + (- (:amount (:amount b))) + (:amount (:amount b)))) + ] + (catch Exception e + (println a b) + (println e) + (throw e) + )) + )) + (filter (fn [[a b]] + (not (dollars= a b)))) + count + + )]) + (catch Exception e + (println "please retry" bank-account))) + ) + (println "done")) + + + +(take 10 trans) + +(first trans) +(last trans) + + +(csv/write-csv *out* + (->> trans + (sort-by :date) + (map (fn [z] + (update z :date #(auto-ap.time/unparse (auto-ap.time/parse % auto-ap.time/iso-date) auto-ap.time/normal-date)))) + (map (fn [z] + [(:date z) (:original (:description z)) (if (= "DEBIT" (:baseType z)) + (- (:amount (:amount z))) + (:amount (:amount z)))] + )) + + ) + + :separator \tab + ) + +(def deep-dive (y/get-specific-transactions-with-date 27995674 "2021-12-03" "2021-12-03" (y/get-auth-header))) + + +(csv/write-csv *out* + (->> (->> (y/get-specific-transactions-with-date 27995674 "2021-01-01" "2021-12-31" (y/get-auth-header)) + (group-by (fn [d] + ((juxt :description + :amount + :date + :baseType + :type + :isManual + :merchantType + :status + :CONTAINER + :runningBalance + :subType + :isPhysical + :accountId + :postDate) d))) + (map (fn [[k v]] + (first (sort-by :id v))))) + (sort-by :date) + (map (fn [z] + (update z :date #(auto-ap.time/unparse (auto-ap.time/parse % auto-ap.time/iso-date) auto-ap.time/normal-date)))) + (map (fn [z] + [(:date z) (:original (:description z)) (if (= "DEBIT" (:baseType z)) + (- (:amount (:amount z))) + (:amount (:amount z)))] + )) + + ) + + :separator \tab + ) + +(->> (y/get-specific-transactions-with-date 27995674 "2021-12-30" "2021-12-30" (y/get-auth-header)) + (group-by (fn [d] + ((juxt :description + :amount + :date + :baseType + :type + :isManual + :merchantType + :status + :CONTAINER + :runningBalance + :subType + :isPhysical + :accountId + :postDate) d))) + (map (fn [[k v]] + [k (count v)] + #_(clojure.data/diff a b)))) + +(y2/get-accounts "DEMO2") + + +(->> (y2/get-specific-transactions "DEMO2" 15565801) + (group-by (fn [d] + ((juxt :description + :amount + :date + :baseType + :type + :isManual + :merchantType + :status + :CONTAINER + :runningBalance + :subType + :isPhysical + :accountId + :postDate) d))) + (map (fn [[k v]] + [k (count v)] + #_(clojure.data/diff a b))) + count) + +(csv/write-csv *out* + (->> (y2/get-specific-transactions "DEMO2" 15565801) + (sort-by :date) + (map (fn [z] + (update z :date #(auto-ap.time/unparse (auto-ap.time/parse % auto-ap.time/iso-date) auto-ap.time/normal-date)))) + (map (fn [z] + [(:date z) (:original (:description z)) (if (= "DEBIT" (:baseType z)) + (- (:amount (:amount z))) + (:amount (:amount z)))] + ))) + :separator \tab) + + + +(def bad-as [24265567 + 16422563 + 27327396 + 16279665 + 16279668 + 16279669 + 16279670 + 16279671 + 24230113 + 16279664 + 16421944 + 24287315 + 27723398 + 27723397 + 16422358 + 18685498 + 16422285 + 24370905 + 18911886 + 27995674 + 19935858 + 16422708]) + + +(def all-as [ +[18409209 0] +[18409210 0] +[27040638 0] +[18409211 0] +[27040639 0] +[27040640 0] +[27398018 0] +[24249175 0] +[16551129 0] +[24781151 0] +[24265567 2433] +[25829735 0] +[25976674 0] +[25829733 0] +[25829734 0] +[26403171 0] +[26403172 0] +[26403169 0] +[26403170 0] +[19679001 0] +[20772618 0] +[26403173 0] +[27509142 0] +[19679000 0] +[18937120 0] +[22730534 0] +[25640840 0] +[16551102 0] +[20186867 0] +[27784044 0] +[26216350 0] +[27784043 0] +[27784042 0] +[21961051 0] +[16422563 48] +[22981959 0] +[16422564 0] +[18356995 0] +[16422562 0] +[27040641 0] +[27040642 0] +[27040643 0] +[26997692 0] +[26997691 0] +[26793947 0] +[27327396 64] +[16279665 83] +[16279666 0] +[26988499 0] +[16279667 0] +[16279668 12] +[16279669 16] +[16279670 55] +[16279671 4] +[16279672 0] +[24230113 80] +[25624533 0] +[25624532 0] +[16279663 0] +[16279664 441] +[26991555 0] +[27798450 0] +[27798451 0] +[26607050 0] +[16422491 0] +[16422492 0] +[16422495 0] +[16422493 0] +[27847951 0] +[16422494 0] +[25646832 0] +[25646831 0] +[16421963 0] +[16422473 0] +[26920161 0] +[16422471 0] +[16422472 0] +[16421945 0] +[16422463 0] +[16422462 0] +[24591293 0] +[16421943 0] +[16421944 8] +[24566177 0] +[24434091 0] +[24434092 0] +[24104846 0] +[24104845 0] +[24104844 0] +[24411026 0] +[24104839 0] +[24104838 0] +[21754236 0] +[27576149 0] +[18099105 0] +[24499586 0] +[18937304 0] +[18937305 0] +[25308479 0] +[25308480 0] +[24104849 0] +[23662442 0] +[25308484 0] +[25308481 0] +[16876498 0] +[24265569 0] +[16876499 0] +[24265570 0] +[24287315 317] +[26983548 0] +[24287316 0] +[27723399 0] +[27723398 21] +[27723397 110] +[24287319 0] +[27576450 0] +[24287317 0] +[27576451 0] +[24287318 0] +[26983549 0] +[27576452 0] +[26794100 0] +[24287326 0] +[16422358 13] +[17303033 0] +[26942060 0] +[24776257 0] +[16876551 0] +[22883931 0] +[17970207 0] +[27361904 0] +[27361903 0] +[19941347 0] +[27345526 0] +[16417200 0] +[16428453 0] +[19756531 0] +[16428443 0] +[18685498 18] +[27465416 0] +[27465415 0] +[27465417 0] +[16422296 0] +[27847900 0] +[27847902 0] +[16422285 11] +[25350341 0] +[16428403 0] +[16551286 0] +[27282619 0] +[27282620 0] +[19942033 0] +[19942034 0] +[26220018 0] +[27780614 0] +[27793421 0] +[17303150 0] +[27793422 0] +[16428372 0] +[24370905 450] +[16428375 0] +[18911886 44] +[16428373 0] +[16428374 0] +[18911888 0] +[27995676 0] +[27995675 0] +[27995674 861] +[19942029 0] +[17408629 0] +[19942030 0] +[22219439 0] +[19942031 0] +[19942032 0] +[25543675 0] +[19935858 606] +[16422707 0] +[16422708 320] +[27116551 0] +[16428317 0] +[16428318 0] +[24510110 0] +[27847749 0] +[654654654 0] +[21489791 0] +[21489790 0] +[23139427 0] +[16422643 0] +[16422644 0] +[16422642 0] +[16428279 0] +[25247822 0] +[18409174 0] +[16422645 0] +[18948276 0] +[18948275 0] +]) +(filter (fn [[_ x]] (not= 0 x)) all-as) + +(->> (d/q '[:find [(pull ?ba [:bank-account/name :bank-account/code]) ...] + :in $ [?yai ...] + :where [?ba :bank-account/yodlee-account-id ?yai]] + (d/db conn) + + ) + (map (juxt :bank-account/name :bank-account/code))) + + +(def trans (y/get-specific-transactions-with-date 27995674 "2021-01-01" "2021-12-31" (y/get-auth-header))) + +(def starting (:amount (:runningBalance (last trans)))) + +(def dates (sort (set (map :date trans)))) + + +(defn find-mistakes [trans-set] + (let [sequences (->> trans-set + (filter :runningBalance) + (map (fn [index t] + {:index index + :id (:id t) + :starting (- (:amount (:runningBalance t)) + (if (= "DEBIT" (:baseType t)) + (- (:amount (:amount t))) + (:amount (:amount t)))) + :amount (if (= "DEBIT" (:baseType t)) + (- (:amount (:amount t))) + (:amount (:amount t))) + :after (:amount (:runningBalance t)) + :original t} + ) (range))) + graph (reduce + (fn [graph s] + (assoc graph s + (filter (fn [s2] + (dollars= (:starting s2) + (:after s)) + ) sequences) + ) + ) + {} + sequences) + mistakes (filter + (fn [[k v]] + (> (count v) 1) + ) + graph)] + + (->> mistakes + (map second) + (map (fn [duplicates] + (map :original duplicates))) + ))) + +(csv/write-csv *out* + (for [a bad-as + :let [{:bank-account/keys [name code]} (first (d/q '[:find [(pull ?ba [:bank-account/name :bank-account/code]) ...] + :in $ ?yai + :where [?ba :bank-account/yodlee-account-id ?yai]] + (d/db conn) + a)) + transactions (y/get-specific-transactions-with-date a "2021-01-01" "2021-12-31" (y/get-auth-header))] + + + mistakes (->> transactions + (group-by :date) + vals + (mapcat find-mistakes)) + ] + + [code + name + (:date mistakes) + (:amount (:amount mistakes))]) + :separator \tab) + +(def mistake-set + (doall + (for [a bad-as + :let [{:bank-account/keys [name code]} (first (d/q '[:find [(pull ?ba [:bank-account/name :bank-account/code]) ...] + :in $ ?yai + :where [?ba :bank-account/yodlee-account-id ?yai]] + (d/db conn) + a)) + transactions (y/get-specific-transactions-with-date a "2021-01-01" "2021-12-31" (y/get-auth-header))] + + + ] + + [code + (->> transactions + (group-by :date) + vals + (map find-mistakes) + (filter seq))]))) + +(user/init-repl) + +mistake-set + + +(csv/write-csv *out* + (mapcat identity + (for [[[client mistake-set] i] (map vector mistake-set (range)) + [mistake-set i2] (map vector mistake-set (range)) + [mistake-set i3] (map vector mistake-set (range)) + :let [already-fixed? (= (->> mistake-set + (map #(d/pull (d/db conn) + [:db/id {:transaction/approval-status [:db/ident]}] + [:transaction/id + (digest/sha-256 (str(:id %)))]) + ) + (filter :db/id) + (filter #(and (not= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status %)) ) + (not= :transaction-approval-status/exclude-from-ledger (:db/ident (:transaction/approval-status %))))) + count) + 1)]] + + (mapv + (fn [m] + (let [iol-transaction (d/pull (d/db conn) + [:db/id {:transaction/approval-status [:db/ident] + :transaction/bank-account [:bank-account/code]}] + [:transaction/id + (digest/sha-256 (str(:id m)))])] + [(str i "-" i2 "-" i3) + (or (:bank-account/code (:transaction/bank-account iol-transaction)) + "not found") + (:date m) + (:amount (:amount m)) + (:id m) + (or (:db/id iol-transaction) "not found") + (or (some-> (:db/ident (:transaction/approval-status iol-transaction)) + name) + "not found") + already-fixed? + ] + )) + mistake-set))) + + :separator \tab) + +(->> trans + (reverse) + (filter :runningBalance) + (partition 2 1) + (map (fn [[a b]] + (try + [ + (- starting + (:amount (:runningBalance a))) + + + (- starting + (:amount (:runningBalance b)) + (if (= "DEBIT" (:baseType b)) + (- (:amount (:amount b))) + (:amount (:amount b)))) + ] + (catch Exception e + (println a b) + (println e) + (throw e) + )) + )) + (filter (fn [[a b]] + (not (dollars= a b)))) + ) + +(csv/write-csv *out* + (->> trans + (reverse) + (map (fn [t] + [(:date t) + (:amount (:runningBalance t)) + (if (= "DEBIT" (:baseType t)) + (- (:amount (:amount t))) + (:amount (:amount t)))] + ))) + :separator \tab) + + + +(def ts (auto-ap.intuit.core/get-transactions "2022-02-01" "2022-02-05" "BCFM - Heritage Main 7362 ---BCFM-H7362")) + +(count ts) + + + +(defn check-transactions [db cc bac ba yba] + (let [all-transactions (d/q '[:find ?d ?a ?tx + :in $ ?ba + :where [?tx :transaction/bank-account ?ba] + [?tx :transaction/date ?d] + [(>= ?d #inst "2021-01-01T00:00:00-08:00")] + [(<= ?d #inst "2021-12-31T00:00:00-08:00")] + (not [?tx :transaction/approval-status :transaction-approval-status/suppressed]) + (not [?tx :transaction/approval-status :transaction-approval-status/excluded]) + [?tx :transaction/amount ?a]] + db ba) + not-excluded (fn [t] + (not (y/known-bad-yodlee-ids (:id t)))) + all-yodlee-transactions (->> (y/get-specific-transactions-with-date yba "2021-01-01" "2021-12-31" (y/get-auth-header)) + (filter not-excluded)) + transactions-by-date (->> all-transactions + (group-by first) + (map (fn [[k v]] + [(atime/unparse-local (coerce/to-date-time k) iso-date) v])) + (into {})) + yodlee-transactions-by-date (->> all-yodlee-transactions + (group-by :date) + (into {})) + yodlee-last-updated (-> (y/get-account yba (y/get-auth-header)) first :dataset first :lastUpdated) + + ] + (for [d (->> + (periodic/periodic-seq (coerce/to-date-time #inst "2021-01-01T00:00:00-08:00") (t/days 1)) + (map (fn [d] + (atime/unparse d iso-date))) + (take 365)) + + :let [txs (transactions-by-date d) + y-txes (yodlee-transactions-by-date d) + y-total (reduce + 0.0 (map y-tx->$ y-txes)) + t-total (reduce + 0.0 (map (fn [[_ a] + ] + a) + txs))]] + + (into + [cc bac + yba d + (count txs ) (count y-txes) + t-total y-total] + + (cond + (and + (= (count txs ) (count y-txes) 0) + (>= (.compareTo d yodlee-last-updated) 0)) + ["Match but yodlee feed stopped" + (format "Date %s is after yodlee feed ended (%s)", d yodlee-last-updated)] + + (and + (= (count txs ) (count y-txes)) + (dollars= t-total y-total)) + ["Perfect Match" + ""] + + + (>= (.compareTo d yodlee-last-updated) 0) + ["Mismatched because yodlee feed stopped" + (format "Date %s is after yodlee feed ended (%s)", d yodlee-last-updated)] + + (not= (count txs ) (count y-txes)) + ["Mismatched number of transactions" + ""] + + (not (dollars= t-total y-total)) + ["Transactions don't add up" + ""] + + :else + ["Mismatch" + "Mismatch, unknown cause"] + )) + ))) + + +(do ;;find iol duplicates + + #_(check-transactions (d/db auto-ap.datomic/conn) 17592188489475 18911886 ) + + (csv/write-csv *out* + (let [db (d/db auto-ap.datomic/conn)] + (for [[ba yba bac cc] (d/q '[:find ?ba ?yba ?bac ?cc + :in $ + :where [?ba :bank-account/yodlee-account-id ?yba] + [(not= ?yba 0)] + [?ba :bank-account/code ?bac] + [?c :client/bank-accounts ?ba] + [?c :client/code ?cc]] + db + ) + row (try (check-transactions db cc bac ba yba) + (catch Exception _ + []))] + row)) + :separator \tab) + + ) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 2e8b67be..336cacf3 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -119,27 +119,7 @@ :phone {:type 'String}}} - :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}}} - - :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)}}} - - :address + :address {:fields {:street1 {:type 'String} :street2 {:type 'String} :city {:type 'String} @@ -201,26 +181,7 @@ :sent {:type 'String} :vendor {:type :vendor}}} - :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)}}} + :order_line_item @@ -251,10 +212,6 @@ :date {:type 'String} :charges {:type '(list :charge)} :line_items {:type '(list :order_line_item)}}} - - - - :yodlee_merchant {:fields {:id {:type :id} :yodlee_id {:type 'String} :name {:type 'String}}} @@ -322,11 +279,7 @@ :start {:type 'Int} :end {:type 'Int}}} - :ledger_page {:fields {:journal_entries {:type '(list :journal_entry)} - :count {:type 'Int} - :total {:type 'Int} - :start {:type 'Int} - :end {:type 'Int}}} + :sales_order_page {:fields {:sales_orders {:type '(list :sales_order)} :count {:type 'Int} @@ -351,16 +304,7 @@ :paid {:type 'String} :unpaid {:type 'String}}} - :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)}}} - - :upcoming_transaction {:fields {:amount {:type :money} + :upcoming_transaction {:fields {:amount {:type :money} :identifier {:type 'String} :date {:type :iso_date}}} @@ -394,20 +338,7 @@ :cash_flow {:type :cash_flow_result :args {:client_id {:type :id}} :resolve :get-cash-flow} - :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)}} - :resolve :get-profit-and-loss} - - :yodlee_provider_account_page {:type :yodlee_provider_account_page + :yodlee_provider_account_page {:type :yodlee_provider_account_page :args {:client_id {:type :id}} :resolve :get-yodlee-provider-account-page} @@ -442,9 +373,7 @@ :description {:type 'String}} :resolve :get-transaction-rule-page} - :ledger_page {:type :ledger_page - :args {:filters {:type :ledger_filters}} - :resolve :get-ledger-page} + :sales_order_page {:type :sales_order_page :args {:client_id {:type :id} @@ -472,44 +401,9 @@ :sort_name {:type 'String} :asc {:type 'Boolean}}} - :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} - :from_numeric_code {:type 'Int} - :to_numeric_code {:type 'Int} - :start {:type 'Int} - :per_page {:type 'Int} - :only_external {:type 'Boolean} - :external_id_like {:type 'String} - :source {:type 'String} - :sort {:type '(list :sort_item)}}} - - - - :date_range {:fields {:start {:type :iso_date} :end {:type :iso_date}}} - :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)}}} - - :edit_user {:fields {:id {:type :id} :name {:type 'String} @@ -612,7 +506,6 @@ :type_1099 {:values [{:enum-value :none} {:enum-value :misc} {:enum-value :landlord}]} - :applicability {:values [{:enum-value :global} {:enum-value :optional} {:enum-value :customized}]} @@ -623,46 +516,40 @@ {:enum-value :equity} {:enum-value :revenue}]}} :mutations - {:request_import {:type 'String - :args {:which {:type 'String}} - :resolve :mutation/request-import} + {:request_import + {:type 'String + :args {:which {:type 'String}} + :resolve :mutation/request-import} - :delete_external_ledger {:type :message - :args {:filters {:type :ledger_filters} - :ids {:type '(list :id)}} - :resolve :mutation/delete-external-ledger} + :delete_transaction_rule + {:type :id + :args {:transaction_rule_id {:type :id}} + :resolve :mutation/delete-transaction-rule} + :merge_vendors + {:type :id + :args {:from {:type :id} + :to {:type :id}} + :resolve :mutation/merge-vendors} - :delete_transaction_rule {:type :id - :args {:transaction_rule_id {:type :id}} - :resolve :mutation/delete-transaction-rule} - :merge_vendors {:type :id - :args {:from {:type :id} - :to {:type :id}} - :resolve :mutation/merge-vendors} + :edit_user + {:type :user + :args {:edit_user {:type :edit_user}} + :resolve :mutation/edit-user} - :edit_user {:type :user - :args {:edit_user {:type :edit_user}} - :resolve :mutation/edit-user} + :upsert_vendor + {:type :vendor + :args {:vendor {:type :add_vendor}} + :resolve :mutation/upsert-vendor} - + :upsert_transaction_rule + {:type :transaction_rule + :args {:transaction_rule {:type :edit_transaction_rule}} + :resolve :mutation/upsert-transaction-rule} - :upsert_vendor {:type :vendor - :args {:vendor {:type :add_vendor}} - :resolve :mutation/upsert-vendor} - - :upsert_transaction_rule {:type :transaction_rule - :args {:transaction_rule {:type :edit_transaction_rule}} - :resolve :mutation/upsert-transaction-rule} - - :import_ledger {:type :import_ledger_result - :args {:entries {:type '(list :import_ledger_entry)}} - :resolve :mutation/import-ledger} - - :upsert_account {:type :account - :args {:account {:type :edit_account}} - :resolve :mutation/upsert-account} - - }}) + :upsert_account + {:type :account + :args {:account {:type :edit_account}} + :resolve :mutation/upsert-account}}}) (defn snake->kebab [s] @@ -881,10 +768,7 @@ :get-yodlee-provider-account-page gq-yodlee2/get-yodlee-provider-account-page :get-all-sales-orders get-all-sales-orders :get-accounts gq-accounts/get-accounts - :get-ledger-page gq-ledger/get-ledger-page :get-sales-order-page gq-sales-orders/get-sales-orders-page - :get-balance-sheet gq-ledger/get-balance-sheet - :get-profit-and-loss gq-ledger/get-profit-and-loss :get-transaction-rule-page gq-transaction-rules/get-transaction-rule-page :get-transaction-rule-matches gq-transaction-rules/get-transaction-rule-matches :get-expense-account-stats get-expense-account-stats @@ -895,17 +779,16 @@ :get-user get-user :mutation/delete-transaction-rule gq-transaction-rules/delete-transaction-rule :mutation/edit-user gq-users/edit-user - :mutation/delete-external-ledger gq-ledger/delete-external-ledger :mutation/upsert-transaction-rule gq-transaction-rules/upsert-transaction-rule :test-transaction-rule gq-transaction-rules/test-transaction-rule :run-transaction-rule gq-transaction-rules/run-transaction-rule :mutation/upsert-vendor gq-vendors/upsert-vendor :mutation/upsert-account gq-accounts/upsert-account :mutation/merge-vendors gq-vendors/merge-vendors - :mutation/import-ledger gq-ledger/import-ledger :mutation/request-import gq-requests/request-import :get-vendor gq-vendors/get-graphql}) gq-checks/attach + gq-ledger/attach gq-plaid/attach gq-import-batches/attach gq-transactions/attach diff --git a/src/clj/auto_ap/graphql/ledger.clj b/src/clj/auto_ap/graphql/ledger.clj index 7953d416..0ace63fe 100644 --- a/src/clj/auto_ap/graphql/ledger.clj +++ b/src/clj/auto_ap/graphql/ledger.clj @@ -1,20 +1,23 @@ (ns auto-ap.graphql.ledger - (:require [auto-ap.datomic :refer [audit-transact-batch remove-nils uri conn]] - [auto-ap.datomic.accounts :as a] - [auto-ap.datomic.clients :as d-clients] - [auto-ap.datomic.ledger :as l] - [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.utils :refer [by dollars=]] - [clj-time.coerce :as coerce] - [clojure.tools.logging :as log] - [datomic.api :as d] - [unilog.context :as lc] - [mount.core :as mount] - [yang.scheduler :as scheduler])) + (: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.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.utils :refer [by dollars=]] + [auto-ap.pdf.ledger :refer [print-pnl]] + [clj-time.coerce :as coerce] + [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 {})) @@ -205,6 +208,34 @@ #(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 args data)] + + (->graphql {:report_url result})) + #_(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)) + 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 assoc-error [f] (fn [entry] @@ -490,3 +521,169 @@ :start (scheduler/every (* 15 60 1000) refresh-running-balance-cache) :stop (scheduler/stop running-balance-cache-worker)) + + +(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}}} + + :profit_and_loss_pdf + {:fields {:report_url {: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)}}} + + :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)}}}}) + +(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)}} + :resolve :get-profit-and-loss} + + :profit_and_loss_pdf {:type :profit_and_loss_pdf + :args {:client_id {:type :id} + :client_ids {:type '(list :id)} + :periods {:type '(list :date_range)}} + :resolve :profit-and-loss-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 + {: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} + :from_numeric_code {:type 'Int} + :to_numeric_code {:type 'Int} + :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 + {:payment_type {:values [{:enum-value :check} + {:enum-value :cash} + {:enum-value :debit} + {:enum-value :credit}]} + :payment_status {:values [{:enum-value :voided} + {:enum-value :pending} + {:enum-value :cleared}]}}) + + +(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 + :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))) diff --git a/src/clj/auto_ap/pdf/ledger.clj b/src/clj/auto_ap/pdf/ledger.clj new file mode 100644 index 00000000..51d87079 --- /dev/null +++ b/src/clj/auto_ap/pdf/ledger.clj @@ -0,0 +1,456 @@ +(ns auto-ap.pdf.ledger + (:require + [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [conn]] + [auto-ap.graphql.utils :refer [<-graphql]] + [auto-ap.time :as atime] + [auto-ap.utils :refer [dollars-0?]] + [clj-pdf.core :as pdf] + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.walk :refer [postwalk]] + [config.core :refer [env]] + [datomic.api :as d]) + (:import + (java.io ByteArrayOutputStream) + (java.util UUID))) + +(defn distribute [nums] + (let [sum (reduce + 0 nums)] + (map #(* 100 (/ % sum)) nums))) + +(defn date->str [d] + (atime/unparse-local d atime/normal-date)) + +(def ranges + {:sales [40000 49999] + :cogs [50000 59999] + :payroll [60000 69999] + :controllable [70000 79999] + :fixed-overhead [80000 89999] + :ownership-controllable [90000 99999]}) + +(def groupings + {:sales [["40000-43999 Food Sales " 40000 43999] + ["44000-46999 Alcohol Sales" 44000 46999] + ["47000 Merchandise Sales" 47000 47999] + ["48000 Other Operating Income" 48000 48999] + ["49000 Non-Business Income" 49000 49999]] + :cogs [ + ["50000-54000 Food Costs" 50000 53999] + ["54000-56000 Alcohol Costs" 54000 55999] + ["56000 Merchandise Costs" 56000 56999] + ["57000-60000 Other Costs of Sales" 57000 59999]] + :payroll [["60000 Payroll - General" 60000 60999] + ["61000 Payroll - Management" 61000 61999] + ["62000 Payroll - BOH" 62000 62999] + ["63000-66000 Payroll - FOH" 63000 65999] + ["66000-70000 Payroll - Other" 66000 69999]] + + :controllable [["70000 72000 GM Controllable Costs - Ops Related" 70000 71999] + ["72000 GM Controllable Costs - Customer Related" 72000 72999] + ["73000 GM Controllable Costs - Employee Related" 73000 73999] + ["74000 GM Controllable Costs - Building & Equipment Related" 74000 74999] + ["75000 GM Controllable Costs - Office & Management Related" 75000 75999] + ["76000-80000 GM Controllable Costs - Other" 76000 79999]] + + :fixed-overhead [["80000-82000 Operational Costs" 80000 81999] + ["82000 Occupancy Costs" 82000 82999] + ["83000 Utility Costs" 83000 83999] + ["84000 Equipment Rental" 84000 84999] + ["85000-87000 Taxes & Insurance" 85000 86999] + ["87000-90000 Other Non-Controllable Costs" 87000 89999]] + :ownership-controllable [["90000-93000 Research & Entertainment" 90000 92999] + ["93000 Bank Charges & Interest" 93000 93999] + ["94000-96000 Other Owner Controllable Costs" 94000 95999] + ["96000 Depreciation" 96000 96999] + ["97000 Taxes" 97000 97999] + ["98000 Other Expenses" 98000 98999]]}) + +(defn in-range? [code] + (reduce + (fn [acc [start end]] + (if (<= start code end) + (reduced true) + acc)) + false + (vals ranges))) + +(defn locations [data] + (->> data + :periods + (mapcat :accounts) + (filter (comp in-range? :numeric-code)) + (group-by (juxt :client-id :location)) + (filter (fn [[k as]] + (not (dollars-0? (reduce + 0 (map :amount as)))))) + (mapcat second) + (map (fn [a] + (if (or (not (:client-id a)) + (empty? (:location a))) + nil + [(:client-id a) + (:location a)]))) + (filter identity) + (set) + + (sort-by (fn [x] + [(:client-id x) + (if (= (:location x) "HQ" ) + "ZZZZZZ" + (:location x))])))) + +(defn expand [data] + (postwalk (fn [x] + (cond + (map-entry? x) + x + + (sequential? x) + (vec (mapcat (fn [r] + (cond (and (sequential? r) + (= :<> (first r))) + (filter identity (rest r)) + + :else + [r])) + x)) + + :else + x + )) + data)) + +(defn map-periods [for-every between periods include-deltas] + (into [:<>] + (for [[_ i] (map vector periods (range))] + [:<> (for-every i) + (if (and include-deltas (not= 0 i)) + (between i))]))) + +(defn period-header [{:keys [include_deltas periods]}] + [ + [[:cell "Period"] + [:<> + (map-periods + (fn [i] + [:cell {:colspan 2} + (str (date->str (get-in periods [i :start])) " - " (date->str (get-in periods [i :end])))]) + (fn [i] + [:cell ""]) + periods + include_deltas)]] + [[:cell ""] + [:<> (map-periods + (fn [i] + [:<> + [:cell + "Amount"] + [:cell + "% Sales"]]) + (fn [i] + [:cell "𝝙"]) + periods + include_deltas)] + ]]) + +(defn all-accounts [data] + (transduce + (comp + (map #(->> (:accounts %) + (group-by (juxt :numeric-code :client-id :location)) + (map (fn [[k v]] + [k + (reduce (fn [a n] + (-> a + (update :count (fn [z] (+ z (:count n)))) + (update :amount (fn [z] (+ z (:amount n)))))) + (first v) + (rest v))])) + + (into {})))) + + conj + [] + (:periods data))) + + +(defn filter-accounts [accounts period [from to] only-client only-location] + (->> (get accounts period) + vals + + (filter (fn [{:keys [location client-id numeric-code]}] + (and (or (nil? only-location) + (= only-location location)) + (or (nil? only-client) + (= only-client client-id)) + (<= from numeric-code to)))) + (sort-by :numeric-code))) + +(defn aggregate-accounts [accounts] + (reduce (fnil + 0.0) 0.0 (map :amount accounts))) + +(defn used-accounts [accounts [from to] client-id location] + (->> accounts + (mapcat vals) + (filter #(<= from (:numeric-code %) to)) + (filter #(= client-id (:client-id %))) + (filter #(= location (:location %))) + (map #(select-keys % [:numeric-code :name])) + (set) + (sort-by :numeric-code))) + +(defn subtotal-row [args data types negs title client-id location] + (let [all-accounts (all-accounts data) + raw (map-indexed + (fn [i p] + (aggregate-accounts (mapcat (fn [t] + (cond->> (filter-accounts all-accounts i (ranges t) client-id location) + (negs t) (map #(update % :amount -)))) + types)) + ) + + (:periods args)) + sales (map-indexed + (fn [i _] + (aggregate-accounts (filter-accounts all-accounts i (ranges :sales) client-id location))) + + (:periods args)) + deltas (->> raw + (partition-all 2) + (map (fn [[a b]] + (- b + a))))] + (into [title] + (->> raw + (map (fn [s r] + [r (if (dollars-0? s) + 0.0 + (/ r s))]) + sales) + (partition-all 2) + (mapcat (fn [d [[a a-sales] [b b-sales]]] + [a a-sales b b-sales d] + ) + deltas))))) + +(defn location-summary-table [args data client-id location] + [(subtotal-row args data [:sales] #{} "Sales" client-id location) + (subtotal-row args data [:cogs ] #{} "Cogs" client-id location) + (subtotal-row args data [:payroll ]#{} "Payroll" client-id location) + (subtotal-row args data [:sales :payroll :cogs] #{:payroll :cogs} "Gross Profits" client-id location) + (subtotal-row args data [:controllable :fixed-overhead :ownership-controllable] #{} "Overhead" client-id location) + (subtotal-row args data [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" client-id location)]) + + +(defn detail-sub-rows [args data grouping client-id location] + (let [all-accounts (all-accounts data)] + (for [[grouping-name from to] grouping + :let [account-codes (used-accounts all-accounts [from to] client-id location)] + :when (seq account-codes)] + (-> + [[(str "---" grouping-name "---")]] + (into (for [{:keys [numeric-code name]} account-codes] + (let [raw (map-indexed + (fn [i p] + (get-in all-accounts [i [numeric-code client-id location] :amount] 0.0)) + + (:periods args)) + sales (map-indexed + (fn [i _] + (aggregate-accounts (filter-accounts all-accounts i (ranges :sales) client-id location))) + + (:periods args)) + deltas (->> raw + (partition-all 2) + (map (fn [[a b]] + (- b + a))))] + (into [name] + (->> raw + (map (fn [s r] + [r (if (dollars-0? s) + 0.0 + (/ r s))]) + sales) + (partition-all 2) + (mapcat (fn [d [[a a-sales] [b b-sales]]] + [a a-sales b b-sales d] + ) + deltas)))) + + #_[:tr + [:td name] + #_(map-periods + (fn [i] + (let [amount (get-in all-accounts [i [numeric-code client-id location] :amount] 0.0)] + [:<> + [:td.has-text-right (if multi-client? + [:span (->$ amount)] + [:a {:on-click (dispatch-event [::investigate-clicked location numeric-code numeric-code i :current]) + :disabled (boolean multi-client?)} + (->$ amount)])] + [:td.has-text-right (->% (percent-of-sales amount all-accounts i client-id location))]])) + (fn [i] + [:td.has-text-right (->$ (- (get-in all-accounts [i [numeric-code client-id location] :amount] 0.0) + (get-in all-accounts [(dec i) [numeric-code client-id location] :amount] 0.0)))]) + periods + include-deltas)]))) + ))) + +(defn detail-rows [args data type title client-id location] + (-> [[title]] + (into (detail-sub-rows args data (type groupings) client-id location)) + (conj (subtotal-row args data [type] #{} title client-id location)))) + +(defn location-detail-table [args data client-id location] + (-> [] + (into (detail-rows args data :sales (str location " Sales") client-id location)))) + +(defn summarize-pnl [args data] + {:summaries (for [[client-id location] (locations data)] + (location-summary-table args data client-id location)) + :details (for [[client-id location] (locations data)] + (location-detail-table args data client-id location))} + ) + +(defn make-pnl [args data] + + (let [data (<-graphql data) + args (<-graphql args) + _ (clojure.pprint/pprint (summarize-pnl (assoc args :deltas true) data))output-stream (ByteArrayOutputStream.) + _ (println (:client_ids args)) + clients (d/pull-many (d/db conn) '[:client/name] (:client-ids args))] + (pdf/pdf + (expand + [{:left-margin 25 :right-margin 0 :top-margin 0 :bottom-margin 0 :size :letter} + [:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))] + #_(for [[client-id location] (locations data)] + ^{:key (str client-id "-" location "-summary")} + (location-summary args data client-id location (:include-deltas data)) + ) + #_(let [{:keys [bank-account paid-to client check date amount memo] {print-as :vendor/print-as vendor-name :vendor/name :as vendor} :vendor} check + df (DecimalFormat. "#,###.00")] + [:table {:num-cols 6 :border false :leading 11 :widths (distribute [2 2 2 2 2 2])} + [(let [{:keys [:client/name] {:keys [:address/street1 :address/city :address/state :address/zip]} :client/address} client] + [:cell {:colspan 4 } [:paragraph {:leading 14} name "\n" street1 "\n" (str city ", " state " " zip)] ]) + (let [{:keys [:bank-account/bank-name :bank-account/bank-code] } bank-account] + [:cell {:colspan 6 :align :center} [:paragraph {:style :bold} bank-name] [:paragraph {:size 8 :leading 8} bank-code]]) + [:cell {:colspan 2 :size 13} + check]] + + [[:cell {:colspan 9}] + [:cell {:colspan 3 :leading -10} date]] + [[:cell {:colspan 12 :size 14}] + ] + + [[:cell {:size 13 :leading 13} "PAY"] + [:cell {:size 8 :leading 8 } "TO THE ORDER OF"] + [:cell {:colspan 7} (if (seq print-as) + print-as + vendor-name)] + [:cell {:colspan 3} amount]] + + [[:cell {}] + [:cell {:colspan 8} (str " -- " word-amount " " (str/join "" (take (max + 2 + (- 95 + (count word-amount))) + (repeat "-")))) + [:line {:line-width 0.15 :color [50 50 50]}]] + [:cell {:colspan 3}]] + + + [[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"] + [:cell {:colspan 5 :leading 11.5} (split-memo memo) + [:line {:line-width 0.15 :color [50 50 50]}]] + [:cell {:colspan 6 } (if (:client/signature-file client) + [:image { :top-margin 90 :xscale 0.30 :yscale 0.30 :align :center} + + (:client/signature-file client)] + [:spacer])]] + + #_[ + #_[:cell {:colspan 5} #_memo ] + #_[:cell {:colspan 6}]] + + [[:cell {:colspan 2}] + [:cell {:colspan 10 :leading 30} + [:phrase {:size 18 :ttf-name "public/micrenc.ttf"} (str "c" check "c a" (:bank-account/routing bank-account) "a " (:bank-account/number bank-account) "c")]]] + [[:cell {:colspan 12 :leading 18} [:spacer]]] + [[:cell] + (into + [:cell {:colspan 9}] + (let [{:keys [:client/name] + {:keys [:address/street1 :address/street2 :address/city :address/state :address/zip ]} :client/address} client] + (filter identity + (list + [:paragraph " " name] + [:paragraph " " street1] + (when (not (str/blank? street2)) + [:paragraph " " street2]) + [:paragraph " " city ", " state " " zip])))) + [:cell {:colspan 2 :size 13} + check]] + + [[:cell {:colspan 12 :leading 74} [:spacer]]] + + [[:cell] + [:cell {:colspan 5} [:paragraph + " " vendor-name "\n" + " " (:address/street1 (:vendor/address vendor)) "\n" + (when (not (str/blank? (:address/street2 (:vendor/address vendor)))) + (str " " (:address/street2 (:vendor/address vendor)) "\n") + ) + " " (:address/city (:vendor/address vendor)) ", " (:address/state (:vendor/address vendor)) " " (:address/zip (:vendor/address vendor))]] + [:cell {:align :right} + "Paid to:\n" + "Amount:\n" + "Date:\n"] + + [:cell {:colspan 5} + [:paragraph paid-to] + [:paragraph amount] + [:paragraph date]]] + + [[:cell {:colspan 3} "Memo:"] + [:cell {:colspan 9} memo]] + + [[:cell {:colspan 12} [:spacer]]] + [[:cell {:colspan 12} [:spacer]]] + [[:cell {:colspan 12} [:spacer]]] + [[:cell {:colspan 12} [:spacer]]] + + [[:cell {:colspan 5}] + [:cell {:align :right :colspan 2} + "Check:\n" + "Vendor:\n" + "Company:\n" + "Bank Account:\n" + "Paid To:\n" + "Amount:\n" + "Date:\n"] + + [:cell {:colspan 5} + [:paragraph check] + [:paragraph vendor-name] + [:paragraph (:client/name client)] + [:paragraph (:bank-account/bank-name bank-account)] + [:paragraph paid-to] + [:paragraph amount] + [:paragraph date]]] + [[:cell {:colspan 3} "Memo:"] + [:cell {:colspan 9} memo]] + ])]) + output-stream) + (.toByteArray output-stream))) + +(defn print-pnl [args data] + (let [uuid (str (UUID/randomUUID)) + pdf-data (make-pnl args data)] + (s3/put-object :bucket-name (:data-bucket env) + :key (str "reports/pnl/" uuid ".pdf") + :input-stream (io/make-input-stream pdf-data {}) + :metadata {:content-length (count pdf-data) + :content-type "application/pdf"}) + (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/reports/pnl/" uuid ".pdf"))) diff --git a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs index e05be1e8..837d98e0 100644 --- a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs @@ -212,8 +212,31 @@ :clients (mapv #(select-keys % [:name :id]) (:clients (:data db))) } :db (dissoc db :report)}))) - - +(re-frame/reg-event-fx + ::received-pdf + (fn [_ [_ result]] + (println result) + {:dispatch [::modal/modal-requested {:title "Your report is ready" + :body [:div + [:div "Click " [:a {:href (-> result :profit-and-loss-pdf :report-url) :target "_new"} "here"] " to view it."]]}]})) +(re-frame/reg-event-fx + ::export-pdf + [with-user (forms/in-form ::form)] + (fn [{:keys [db user] :as cofx}] + (cond-> {:graphql {:token user + :owns-state {:single ::page} + :query-obj {:venia/queries [[:profit-and-loss-pdf + {:client-ids (map :id (:clients (:data db))) + :periods (mapv (fn [[start end] ] {:start (date->str start standard) :end (date->str end standard)} ) + (:periods (:data db)))} + [:report_url]]]} + :on-success [::received-pdf]} + :set-uri-params {:periods (mapv (fn [[start end title]] + [(date->str start standard) + (date->str end standard)]) + (:periods (:data db))) + :clients (mapv #(select-keys % [:name :id]) (:clients (:data db))) } + :db (dissoc db :report)}))) (re-frame/reg-event-db @@ -876,7 +899,11 @@ :label "Include deltas" :type "checkbox"}]]]] [:div.level-right - [:button.button.is-primary "Run"]]] + [:div.buttons + [:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"] + [:button.button.is-primary "Run"]] + + ]] [:div.report-control-detail {:ref (fn [el] (when-not @!box (reset! !box el)))}]]))))