merged.
This commit is contained in:
@@ -12,25 +12,26 @@
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]))
|
||||
[datomic.api :as dc]
|
||||
[clj-time.core :as time]))
|
||||
|
||||
(defn potential-duplicate-ids [db args]
|
||||
(when (and (:potential-duplicates args)
|
||||
(:bank-account-id args))
|
||||
(:bank-account-id args))
|
||||
(->> (dc/q '[:find ?tx ?amount ?date
|
||||
:in $ ?ba
|
||||
:where
|
||||
[?tx :transaction/bank-account ?ba]
|
||||
[?tx :transaction/amount ?amount]
|
||||
[?tx :transaction/date ?date]
|
||||
(not [?tx :transaction/approval-status :transaction-approval-status/suppressed])]
|
||||
db
|
||||
(:bank-account-id args))
|
||||
:in $ ?ba
|
||||
:where
|
||||
[?tx :transaction/bank-account ?ba]
|
||||
[?tx :transaction/amount ?amount]
|
||||
[?tx :transaction/date ?date]
|
||||
(not [?tx :transaction/approval-status :transaction-approval-status/suppressed])]
|
||||
db
|
||||
(:bank-account-id args))
|
||||
(group-by (fn [[_ amount date]]
|
||||
[amount date]))
|
||||
(filter (fn [[_ txes]]
|
||||
(> (count txes) 1)))
|
||||
|
||||
|
||||
(vals)
|
||||
(mapcat identity)
|
||||
(map first)
|
||||
@@ -64,8 +65,8 @@
|
||||
(:potential-duplicates args)
|
||||
(merge-query {:query {:in '[[?e ...]]}
|
||||
:args [potential-duplicates]})
|
||||
|
||||
(:bank-account-id args)
|
||||
|
||||
(:bank-account-id args)
|
||||
(merge-query {:query {:in ['?bank-account-id]
|
||||
:where ['[?e :transaction/bank-account ?bank-account-id]]}
|
||||
:args [(:bank-account-id args)]})
|
||||
@@ -75,25 +76,25 @@
|
||||
:where ['[?import-batch-id :import-batch/entry ?e]]}
|
||||
:args [(:import-batch-id args)]})
|
||||
|
||||
(:account-id args)
|
||||
(:account-id args)
|
||||
(merge-query {:query {:in ['?account-id]
|
||||
:where ['[?e :transaction/accounts ?accounts]
|
||||
'[?accounts :transaction-account/account ?account-id]]}
|
||||
:args [(:account-id args)]})
|
||||
|
||||
(:vendor-id args)
|
||||
(:vendor-id args)
|
||||
(merge-query {:query {:in ['?vendor-id]
|
||||
:where ['[?e :transaction/vendor ?vendor-id]]}
|
||||
:args [(:vendor-id args)]})
|
||||
|
||||
|
||||
(:amount-gte args)
|
||||
|
||||
(:amount-gte args)
|
||||
(merge-query {:query {:in ['?amount-gte]
|
||||
:where ['[?e :transaction/amount ?a]
|
||||
'[(>= ?a ?amount-gte)]]}
|
||||
:args [(:amount-gte args)]})
|
||||
|
||||
(:amount-lte args)
|
||||
(:amount-lte args)
|
||||
(merge-query {:query {:in ['?amount-lte]
|
||||
:where ['[?e :transaction/amount ?a]
|
||||
'[(<= ?a ?amount-lte)]]}
|
||||
@@ -102,7 +103,7 @@
|
||||
(:approval-status args)
|
||||
(merge-query {:query {:in ['?approval-status]
|
||||
:where ['[?e :transaction/approval-status ?approval-status]]}
|
||||
:args [(:approval-status args)]})
|
||||
:args [(:approval-status args)]})
|
||||
|
||||
(= (:linked-to args) :payment)
|
||||
(merge-query {:query {:where ['[?e :transaction/payment]]}})
|
||||
@@ -124,20 +125,20 @@
|
||||
'[?c :client/original-id ?original-id]]}
|
||||
:args [(:original-id args)]})
|
||||
|
||||
(seq (:location args))
|
||||
(seq (:location args))
|
||||
(merge-query {:query {:in ['?location]
|
||||
:where ['[?e :transaction/accounts ?tas]
|
||||
'[?tas :transaction-account/location ?location]]}
|
||||
:args [(:location args)]})
|
||||
|
||||
(:unresolved args)
|
||||
(:unresolved args)
|
||||
(merge-query {:query {:where ['[?e :transaction/date]
|
||||
'(or-join [?e]
|
||||
(not [?e :transaction/accounts])
|
||||
(and [?e :transaction/accounts ?tas]
|
||||
(not [?tas :transaction-account/account])))]}})
|
||||
|
||||
(:description args)
|
||||
(:description args)
|
||||
(merge-query {:query {:in ['?description]
|
||||
:where ['[?e :transaction/description-original ?do]
|
||||
'[(clojure.string/lower-case ?do) ?do2]
|
||||
@@ -145,21 +146,21 @@
|
||||
:args [(clojure.string/lower-case (:description args))]})
|
||||
|
||||
(:sort args) (add-sorter-fields {"client" ['[?e :transaction/client ?c]
|
||||
'[?c :client/name ?sort-client]]
|
||||
'[?c :client/name ?sort-client]]
|
||||
"account" ['[?e :transaction/date]
|
||||
'(or-join [?e ?sort-account]
|
||||
(and [?e :transaction/bank-account ?c]
|
||||
[?c :bank-account/name ?sort-account])
|
||||
(and
|
||||
(not [?e :transaction/bank-account])
|
||||
[(ground "") ?sort-account]))]
|
||||
'(or-join [?e ?sort-account]
|
||||
(and [?e :transaction/bank-account ?c]
|
||||
[?c :bank-account/name ?sort-account])
|
||||
(and
|
||||
(not [?e :transaction/bank-account])
|
||||
[(ground "") ?sort-account]))]
|
||||
"description-original" ['[?e :transaction/description-original ?sort-description-original]]
|
||||
"date" ['[?e :transaction/date ?sort-date]]
|
||||
"vendor" ['(or-join [?e ?sort-vendor]
|
||||
(and [(missing? $ ?e :transaction/vendor)]
|
||||
[?e :transaction/description-original ?sort-vendor])
|
||||
(and [?e :transaction/vendor ?v]
|
||||
[?v :vendor/name ?sort-vendor]))]
|
||||
(and [(missing? $ ?e :transaction/vendor)]
|
||||
[?e :transaction/description-original ?sort-vendor])
|
||||
(and [?e :transaction/vendor ?v]
|
||||
[?v :vendor/name ?sort-vendor]))]
|
||||
"amount" ['[?e :transaction/amount ?sort-amount]]
|
||||
"status" ['[?e :transaction/status ?sort-status]]}
|
||||
args)
|
||||
@@ -171,18 +172,30 @@
|
||||
true (apply-sort-3 (assoc args :default-asc? false))
|
||||
true (apply-pagination args)))))
|
||||
|
||||
(defn is-locked? [transaction]
|
||||
(let [transaction-date (some-> transaction :transaction/date coerce/to-date-time)
|
||||
bank-account-start-date (some-> transaction :transaction/bank-account :bank-account/start-date coerce/to-date-time)
|
||||
client-locked-until (some-> transaction :transaction/client :client/locked-until coerce/to-date-time)
|
||||
locked-by-client? (cond (not transaction-date) false
|
||||
(not client-locked-until) false
|
||||
:else (time/before? transaction-date client-locked-until))
|
||||
locked-by-bank-account? (cond (not transaction-date) false
|
||||
(not bank-account-start-date) false
|
||||
:else (time/before? transaction-date bank-account-start-date))]
|
||||
(or locked-by-bank-account? locked-by-client?)))
|
||||
|
||||
(defn graphql-results [ids db _]
|
||||
(let [results (->> (pull-many db '[* {:transaction/client [:client/name :db/id :client/code]
|
||||
(let [results (->> (pull-many db '[* {:transaction/client [:client/name :db/id :client/code :client/locked-until]
|
||||
:transaction/approval-status [:db/ident :db/id]
|
||||
:transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance]
|
||||
:transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance :bank-account/start-date]
|
||||
:transaction/forecast-match [:db/id :forecasted-transaction/identifier]
|
||||
:transaction/vendor [:db/id :vendor/name]
|
||||
:transaction/matched-rule [:db/id :transaction-rule/note]
|
||||
:transaction/payment [:db/id :payment/date]
|
||||
:transaction/expected-deposit [:db/id :expected-deposit/date]
|
||||
:transaction/accounts [:transaction-account/amount
|
||||
:db/id
|
||||
:transaction-account/location
|
||||
:db/id
|
||||
:transaction-account/location
|
||||
{:transaction-account/account [:account/name :db/id
|
||||
:account/location
|
||||
{:account/client-overrides [:account-client-override/name
|
||||
@@ -190,22 +203,22 @@
|
||||
:transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name]
|
||||
:transaction/plaid-merchant [:db/id :plaid-merchant/name]}]
|
||||
ids)
|
||||
(map #(assoc % :transaction/is-locked (is-locked? %)))
|
||||
(map #(update % :transaction/date coerce/from-date))
|
||||
(map #(update % :transaction/post-date coerce/from-date))
|
||||
(map #(update % :transaction/accounts
|
||||
(fn [tas]
|
||||
(map
|
||||
(fn [ta]
|
||||
(update ta :transaction-account/account d-accounts/clientize (:db/id (:transaction/client %))))
|
||||
tas))))
|
||||
(fn [ta]
|
||||
(update ta :transaction-account/account d-accounts/clientize (:db/id (:transaction/client %))))
|
||||
tas))))
|
||||
(map (fn [transaction]
|
||||
(cond-> transaction
|
||||
(:transaction/payment transaction) (update-in [:transaction/payment :payment/date] coerce/from-date)
|
||||
(:transaction/expected-deposit transaction) (update-in [:transaction/expected-deposit :expected-deposit/date] coerce/from-date))
|
||||
))
|
||||
(:transaction/expected-deposit transaction) (update-in [:transaction/expected-deposit :expected-deposit/date] coerce/from-date))))
|
||||
(map #(dissoc % :transaction/id))
|
||||
(group-by :db/id))]
|
||||
|
||||
|
||||
(->> ids
|
||||
(map results)
|
||||
(map first))))
|
||||
@@ -218,7 +231,7 @@
|
||||
matching-count]))
|
||||
|
||||
(defn filter-ids [ids]
|
||||
(if ids
|
||||
(if ids
|
||||
(->> (dc/q {:find ['?e]
|
||||
:in ['$ '[?e ...]]
|
||||
:where ['[?e :transaction/date]]}
|
||||
@@ -228,24 +241,24 @@
|
||||
[]))
|
||||
|
||||
(defn get-by-id [id]
|
||||
(->
|
||||
(->
|
||||
(dc/pull (dc/db conn)
|
||||
'[* {:transaction/client [:client/name :db/id :client/code :client/locations :client/groups]
|
||||
:transaction/approval-status [:db/ident :db/id]
|
||||
:transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance]
|
||||
:transaction/vendor [:db/id :vendor/name]
|
||||
:transaction/matched-rule [:db/id :transaction-rule/note]
|
||||
:transaction/forecast-match [:db/id :forecasted-transaction/identifier]
|
||||
:transaction/accounts [:transaction-account/amount
|
||||
:db/id
|
||||
:transaction-account/location
|
||||
{ :transaction-account/account [:account/name :db/id
|
||||
'[* {:transaction/client [:client/name :db/id :client/code :client/locations :client/groups]
|
||||
:transaction/approval-status [:db/ident :db/id]
|
||||
:transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance]
|
||||
:transaction/vendor [:db/id :vendor/name]
|
||||
:transaction/matched-rule [:db/id :transaction-rule/note]
|
||||
:transaction/forecast-match [:db/id :forecasted-transaction/identifier]
|
||||
:transaction/accounts [:transaction-account/amount
|
||||
:db/id
|
||||
:transaction-account/location
|
||||
{:transaction-account/account [:account/name :db/id
|
||||
:account/location
|
||||
{:account/client-overrides [:account-client-override/name
|
||||
{:account-client-override/client [:db/id]}]}]}]
|
||||
:transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name]
|
||||
:transaction/plaid-merchant [:db/id :plaid-merchant/name]}]
|
||||
id)
|
||||
:transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name]
|
||||
:transaction/plaid-merchant [:db/id :plaid-merchant/name]}]
|
||||
id)
|
||||
(update :transaction/date coerce/from-date)
|
||||
(update :transaction/post-date coerce/from-date)
|
||||
(dissoc :transaction/id)))
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
[auto-ap.graphql.utils
|
||||
:refer [->graphql <-graphql assert-admin attach-tracing-resolvers
|
||||
can-see-client? is-admin? result->page]]
|
||||
[clj-time.coerce :as c]
|
||||
[clj-time.core :as time]
|
||||
[clojure.set :as set]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[datomic.api :as dc]))
|
||||
@@ -13,24 +15,27 @@
|
||||
(let [db (dc/db conn)
|
||||
clients (dc/q '[:find (pull ?c [:db/id :client/code {:client/bank-accounts [:db/id :bank-account/code]}])
|
||||
:where [?c :client/code]]
|
||||
db )]
|
||||
(doseq [[{client :db/id code :client/code bank-accounts :client/bank-accounts}] clients
|
||||
{bank-account :db/id bac :bank-account/code} bank-accounts]
|
||||
db)]
|
||||
(doseq [[{client :db/id code :client/code bank-accounts :client/bank-accounts}] clients
|
||||
{bank-account :db/id bac :bank-account/code} bank-accounts]
|
||||
@(dc/transact conn [{:db/id bank-account
|
||||
:bank-account/current-balance
|
||||
(or
|
||||
(->> (dc/index-pull db
|
||||
{:index :avet
|
||||
:selector [:db/id :journal-entry-line/location :journal-entry-line/account :journal-entry-line/running-balance :journal-entry-line/client+account+location+date {:journal-entry/_line-items [:journal-entry/date :journal-entry/client]}]
|
||||
:start [:journal-entry-line/client+account+location+date [client bank-account "A" #inst "2030-01-01"]]
|
||||
:reverse true
|
||||
})
|
||||
(filter (fn [{[c b] :journal-entry-line/client+account+location+date}]
|
||||
(and (= c client)
|
||||
(= b bank-account))))
|
||||
(map :journal-entry-line/running-balance)
|
||||
(first))
|
||||
0.0)}])))))
|
||||
:bank-account/current-balance-synced (c/to-date (time/now))
|
||||
:bank-account/current-balance
|
||||
(or
|
||||
(->> (dc/index-pull db
|
||||
{:index :avet
|
||||
:selector [:db/id :journal-entry-line/location :journal-entry-line/account :journal-entry-line/running-balance :journal-entry-line/client+account+location+date {:journal-entry/_line-items [:journal-entry/date :journal-entry/client]}]
|
||||
:start [:journal-entry-line/client+account+location+date [client bank-account "A" #inst "2030-01-01"]]
|
||||
:reverse true})
|
||||
(take 3)
|
||||
|
||||
(filter (fn [{[c b] :journal-entry-line/client+account+location+date}]
|
||||
(and (= c client)
|
||||
(= b bank-account))))
|
||||
|
||||
(map :journal-entry-line/running-balance)
|
||||
(first))
|
||||
0.0)}])))))
|
||||
|
||||
(defn get-client [context _ _]
|
||||
(->graphql
|
||||
|
||||
@@ -190,6 +190,22 @@
|
||||
client-ids))
|
||||
true ->graphql)))
|
||||
|
||||
(defn get-profit-and-loss-raw [client-ids periods]
|
||||
(let [ 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
|
||||
(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 get-profit-and-loss [context args _]
|
||||
(let [client-id (:client_id args)
|
||||
client-ids (or (some-> client-id vector)
|
||||
@@ -200,22 +216,10 @@
|
||||
(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)})))})))
|
||||
(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'"}))) ]
|
||||
(get-profit-and-loss-raw client-ids (:periods args))))
|
||||
|
||||
|
||||
|
||||
|
||||
;; profit and loss based off of index
|
||||
|
||||
@@ -374,8 +374,12 @@
|
||||
set
|
||||
(conj "A")
|
||||
(conj "HQ"))))]
|
||||
|
||||
(when-not (dollars= (Math/abs (:transaction/amount existing-transaction)) account-total)
|
||||
|
||||
(when (and (not (dollars= (Math/abs (:transaction/amount existing-transaction)) account-total))
|
||||
(or
|
||||
(and (= approval_status :unapproved)
|
||||
(> (count accounts) 0))
|
||||
(not= approval_status :unapproved)))
|
||||
(let [error (str "Expense account total (" account-total ") does not equal transaction total (" (Math/abs (:transaction/amount existing-transaction)) ")")]
|
||||
(throw (ex-info error {:validation-error error}))))
|
||||
(when missing-locations
|
||||
@@ -567,6 +571,7 @@
|
||||
(def objects
|
||||
{:transaction {:fields {:id {:type :id}
|
||||
:amount {:type 'String}
|
||||
:is_locked {:type 'Boolean}
|
||||
:description_original {:type 'String}
|
||||
:description_simple {:type 'String}
|
||||
:location {:type 'String}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
(= (:user/role id) "admin")
|
||||
nil
|
||||
|
||||
(#{"manager" "user" "power-user"} (:user/role id))
|
||||
(#{"manager" "user" "power-user" "read-only"} (:user/role id))
|
||||
(:user/clients id [])))
|
||||
|
||||
|
||||
|
||||
@@ -209,4 +209,6 @@
|
||||
:args [(dc/db conn)]})]
|
||||
{"id" (:db/id result)
|
||||
"name" (:vendor/name result)
|
||||
"hidden" (boolean (:vendor/hidden result))}))))
|
||||
"hidden" (boolean (:vendor/hidden result))}))))
|
||||
|
||||
#_(rebuild-search-index)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
(:require [amazonica.core :refer [defcredential]]
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic :refer [conn pull-many]]
|
||||
[auto-ap.graphql.utils :refer [limited-clients]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids limited-clients]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.routes.auth :as auth]
|
||||
[auto-ap.routes.exports :as exports]
|
||||
@@ -304,12 +304,26 @@
|
||||
{:status 500
|
||||
:body (pr-str e)})))))
|
||||
|
||||
(defn wrap-trim-clients [handler]
|
||||
(fn [request]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
(:client request)
|
||||
(:client-id (:parsed-query-params request))
|
||||
(when (:client-code (:parsed-query-params request))
|
||||
[:client/code (:client-code (:parsed-query-params request))]))
|
||||
trimmed-clients (->> valid-clients (take 20) set)]
|
||||
(handler (assoc request :valid-client-ids valid-clients
|
||||
:valid-trimmed-client-ids trimmed-clients
|
||||
:first-client-id (first valid-clients)
|
||||
:clients-trimmed? (not= (count trimmed-clients) (count valid-clients)))))))
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defonce app
|
||||
(-> route-handler
|
||||
(wrap-hx-current-url-params)
|
||||
(wrap-guess-route)
|
||||
(wrap-logging)
|
||||
(wrap-trim-clients)
|
||||
(wrap-hydrate-clients)
|
||||
(wrap-store-client-in-session)
|
||||
(wrap-gunzip-jwt)
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
(ns auto-ap.import.intuit
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.import.common :refer [wrap-integration]]
|
||||
[auto-ap.import.transactions :as t]
|
||||
[auto-ap.intuit.core :as i]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.time :as atime]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.string :as str]
|
||||
[com.unbounce.dogstatsd.core :as statsd]
|
||||
[datomic.api :as dc]))
|
||||
(:require [auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.import.common :refer [wrap-integration]]
|
||||
[auto-ap.import.transactions :as t]
|
||||
[auto-ap.intuit.core :as i]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.time :as atime]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.string :as str]
|
||||
[com.unbounce.dogstatsd.core :as statsd]
|
||||
[datomic.api :as dc]
|
||||
[iol-ion.utils :refer [remove-nils]]))
|
||||
|
||||
(defn get-intuit-bank-accounts
|
||||
( [db]
|
||||
(dc/q '[:find ?external-id ?ba ?c
|
||||
:in $
|
||||
:where
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/intuit-bank-account ?iab]
|
||||
[?iab :intuit-bank-account/external-id ?external-id]]
|
||||
db))
|
||||
([db & client-codes]
|
||||
(dc/q '[:find ?external-id ?ba ?c
|
||||
:in $ [?cc ...]
|
||||
:where
|
||||
[?c :client/code ?cc]
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/intuit-bank-account ?iab]
|
||||
[?iab :intuit-bank-account/external-id ?external-id]]
|
||||
db
|
||||
client-codes)))
|
||||
|
||||
(defn get-intuit-bank-accounts [db]
|
||||
(dc/q '[:find ?external-id ?ba ?c
|
||||
:in $
|
||||
:where
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/intuit-bank-account ?iab]
|
||||
[?iab :intuit-bank-account/external-id ?external-id]]
|
||||
db))
|
||||
|
||||
(defn intuit->transaction [transaction]
|
||||
(let [check-number (when (not (str/blank? (:Num transaction)))
|
||||
@@ -78,6 +90,9 @@
|
||||
bank-accounts (i/get-bank-accounts token)]
|
||||
@(dc/transact conn (mapv
|
||||
(fn [ba]
|
||||
{:intuit-bank-account/external-id (:name ba)
|
||||
:intuit-bank-account/name (:name ba)})
|
||||
(remove-nils
|
||||
{:intuit-bank-account/external-id (:name ba)
|
||||
:intuit-bank-account/name (:name ba)
|
||||
:intuit-bank-account/last-synced (coerce/to-date (:last-updated ba))
|
||||
:intuit-bank-account/current-balance (:current-balance ba)}))
|
||||
bank-accounts))))
|
||||
|
||||
@@ -10,10 +10,64 @@
|
||||
[clj-time.core :as time]
|
||||
[clojure.string :as str]
|
||||
[com.unbounce.dogstatsd.core :as statsd]
|
||||
[datomic.api :as dc]))
|
||||
[datomic.api :as dc]
|
||||
[clj-http.client :as client]))
|
||||
|
||||
(for [[e] (take 5 (get-intuit-bank-accounts (dc/db conn)))]
|
||||
|
||||
(i/get-transactions "2023-02-01"
|
||||
"2023-02-05"
|
||||
e))
|
||||
(defn get-bank-accounts [token]
|
||||
|
||||
(defn get-bank-accounts [token]
|
||||
(->> (:body (client/get (str i/prod-base-url "/company/" i/prod-company-id "/query")
|
||||
{:headers
|
||||
(i/with-auth i/prod-base-headers token)
|
||||
:as :json
|
||||
:query-params {"query" "SELECT * From Account maxresults 1000"}}))
|
||||
:QueryResponse
|
||||
:Account
|
||||
#_(filter
|
||||
#(#{"Bank" "Credit Card"} (:AccountType %))))))
|
||||
|
||||
|
||||
|
||||
(require 'auto-ap.time_reader)
|
||||
|
||||
(let [start #clj-time/date-time "2024-02-01"
|
||||
end #clj-time/date-time "2024-04-01"]
|
||||
(for [[ib ba c] (seq (get-intuit-bank-accounts (dc/db conn) "BCFM"))
|
||||
:let [raw-transactions (i/get-transactions (atime/unparse-local start atime/iso-date)
|
||||
(atime/unparse-local end atime/iso-date)
|
||||
ib)
|
||||
ideal-transactions (intuits->transactions raw-transactions ba c)
|
||||
|
||||
found-transactions (when (seq ideal-transactions)
|
||||
(into {} (dc/q '[:find ?si (count ?t)
|
||||
:in $ [?eid ...]
|
||||
:where
|
||||
[?t :transaction/id ?eid]
|
||||
[?t :transaction/approval-status ?s]
|
||||
[?s :db/ident ?si]]
|
||||
(dc/db conn)
|
||||
(map :transaction/id ideal-transactions))))
|
||||
|
||||
missing-transaction-ids (when (seq ideal-transactions)
|
||||
(->>
|
||||
(dc/q '[:find ?eid
|
||||
:in $ [?eid ...]
|
||||
:where (not [_ :transaction/id ?eid])]
|
||||
(dc/db conn)
|
||||
(map :transaction/id ideal-transactions))
|
||||
(map first)
|
||||
(into #{})))
|
||||
missing-transactions (filter (comp missing-transaction-ids :transaction/id) ideal-transactions)]]
|
||||
{:bank-account/name (pull-attr (dc/db conn) :bank-account/name ba)
|
||||
:external-transaction-count (count raw-transactions)
|
||||
:integreat-transaction-count (reduce + 0 (vals found-transactions))
|
||||
:approved-count (:transaction-approval-status/approved found-transactions 0)
|
||||
:unapproved-count (:transaction-approval-status/unapproved found-transactions 0)
|
||||
:requires-feedback-count (:transaction-approval-status/requires-feedback found-transactions 0)
|
||||
:missing-transactions missing-transactions}))
|
||||
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
(ns auto-ap.import.plaid
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn random-tempid]]
|
||||
[auto-ap.import.common :refer [wrap-integration]]
|
||||
[auto-ap.import.transactions :as t]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.plaid.core :as p]
|
||||
[auto-ap.solr]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [allow-once by]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[datomic.api :as dc]
|
||||
[digest :as di]
|
||||
[manifold.deferred :as de]
|
||||
[manifold.executor :as ex]
|
||||
[clojure.string :as str]))
|
||||
(:require [auto-ap.datomic :refer [conn random-tempid]]
|
||||
[auto-ap.import.common :refer [wrap-integration]]
|
||||
[auto-ap.import.transactions :as t]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.plaid.core :as p]
|
||||
[auto-ap.solr]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [allow-once by]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
[digest :as di]
|
||||
[iol-ion.utils :refer [remove-nils]]
|
||||
[manifold.deferred :as de]
|
||||
[manifold.executor :as ex]))
|
||||
|
||||
(defn get-plaid-accounts [db]
|
||||
(-> (dc/q '[:find ?ba ?c ?external-id ?t
|
||||
:in $
|
||||
:where
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/plaid-account ?pa]
|
||||
[?pa :plaid-account/external-id ?external-id]
|
||||
[?pi :plaid-item/accounts ?pa]
|
||||
[?pi :plaid-item/access-token ?t]]
|
||||
db )))
|
||||
(defn get-plaid-accounts
|
||||
([db]
|
||||
(-> (dc/q '[:find ?ba ?c ?external-id ?t
|
||||
:in $
|
||||
:where
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/plaid-account ?pa]
|
||||
[?pa :plaid-account/external-id ?external-id]
|
||||
[?pi :plaid-item/accounts ?pa]
|
||||
[?pi :plaid-item/access-token ?t]]
|
||||
db)))
|
||||
([db & client-codes]
|
||||
(-> (dc/q '[:find ?ba ?c ?external-id ?t
|
||||
:in $ [?cc ...]
|
||||
:where
|
||||
[?c :client/code ?cc]
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/plaid-account ?pa]
|
||||
[?pa :plaid-account/external-id ?external-id]
|
||||
[?pi :plaid-item/accounts ?pa]
|
||||
[?pi :plaid-item/access-token ?t]]
|
||||
db
|
||||
client-codes))))
|
||||
|
||||
|
||||
(defn plaid->transaction [t plaid-merchant->vendor-id]
|
||||
@@ -71,8 +84,36 @@
|
||||
{"id" (:db/id result)
|
||||
"name" (:plaid-merchant/name result)}))))
|
||||
|
||||
(defn upsert-accounts []
|
||||
(try
|
||||
(doseq [[bank-account-id client-id external-id access-token] (get-plaid-accounts (dc/db conn))]
|
||||
(try
|
||||
(let [accounts (p/get-accounts access-token)
|
||||
item (p/get-item access-token)]
|
||||
@(dc/transact
|
||||
conn
|
||||
(for [a (:accounts accounts)]
|
||||
(remove-nils
|
||||
{:plaid-account/external-id (:account_id a)
|
||||
:plaid-account/last-synced (coerce/to-date (coerce/to-date-time (-> item :status :transactions :last_successful_update)))
|
||||
:plaid-account/balance (or (some-> a
|
||||
:balances
|
||||
:current
|
||||
double)
|
||||
0.0)}))))
|
||||
(catch Exception e
|
||||
(alog/warn ::couldnt-upsert-account :error e))))
|
||||
|
||||
|
||||
(catch Exception e
|
||||
(alog/warn ::couldnt-upsert-accounts :error e))))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn import-plaid-int []
|
||||
(let [import-batch (t/start-import-batch :import-source/plaid "Automated plaid user")
|
||||
(let [_ (upsert-accounts)
|
||||
import-batch (t/start-import-batch :import-source/plaid "Automated plaid user")
|
||||
end (atime/local-now)
|
||||
start (time/plus end (time/days -30))
|
||||
plaid-merchant->vendor-id (build-plaid-merchant->vendor-id)]
|
||||
|
||||
80
src/clj/auto_ap/import/plaid.fiddle
Normal file
80
src/clj/auto_ap/import/plaid.fiddle
Normal file
@@ -0,0 +1,80 @@
|
||||
(ns auto-ap.import.plaid)
|
||||
|
||||
(let [end (atime/local-now)
|
||||
start (time/plus end (time/days -30))
|
||||
[_ _ external-id access-token] (first (get-plaid-accounts (dc/db conn) "BCFM"))]
|
||||
(p/get-balance access-token))
|
||||
|
||||
(def g *1)
|
||||
|
||||
(take 5 (:transactions g))
|
||||
|
||||
;; => ({:account_id "Dpj0d9yKmXsOxBd0eaL4UONyEJYomNIX7kba3",
|
||||
;; :balances
|
||||
;; {:available nil,
|
||||
;; :current 17764.42,
|
||||
;; :iso_currency_code "USD",
|
||||
;; :limit nil,
|
||||
;; :unofficial_currency_code nil},
|
||||
;; :mask "1006",
|
||||
;; :name "NICHOLAS TAPTELIS -91006",
|
||||
;; :official_name "Business Gold Rewards Card",
|
||||
;; :subtype "credit card",
|
||||
;; :type "credit"})
|
||||
|
||||
|
||||
(dc/q '[:find (pull ?pa [{ :plaid-item/_accounts [*]}])
|
||||
:in $ ?ba
|
||||
:where [?ba :bank-account/plaid-account ?pa]]
|
||||
(dc/db conn)
|
||||
[:bank-account/code "VS-BA6149"])
|
||||
|
||||
(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:bank-account/code "VS-BA6149"])
|
||||
|
||||
(p/get-transactions "access-production-1aee2c7d-0a57-403d-83dc-28a252fb92b4" "jZrAPpjMoLU55oZdpPVVuk8D7XVjXnuv1EJy6" (clj-time.coerce/to-date-time #inst "2024-05-01") (clj-time.coerce/to-date-time #inst "2024-05-15"))
|
||||
|
||||
(user/init-repl)
|
||||
|
||||
|
||||
|
||||
(defn import-plaid-int-2 []
|
||||
(let [
|
||||
import-batch (t/start-import-batch :import-source/plaid "Automated plaid user")
|
||||
end (atime/local-now)
|
||||
start (time/plus end (time/days -30))
|
||||
plaid-merchant->vendor-id (build-plaid-merchant->vendor-id)]
|
||||
(try
|
||||
(doseq [[bank-account-id client-id external-id access-token] (get-plaid-accounts (dc/db conn))
|
||||
:when (= bank-account-id 17592234448533)
|
||||
|
||||
:let [_ (println "TRYING INTEGRATION")
|
||||
transaction-result (wrap-integration #(p/get-transactions access-token external-id start end)
|
||||
bank-account-id)
|
||||
_ (println "FOUND" (count (:transactions transaction-result)))
|
||||
accounts-by-id (by :account_id (:accounts transaction-result))]
|
||||
transaction (:transactions transaction-result)]
|
||||
(when (not (:pending transaction))
|
||||
(t/import-transaction! import-batch (doto (assoc (plaid->transaction (assoc transaction
|
||||
:account
|
||||
(accounts-by-id (:account_id transaction)))
|
||||
plaid-merchant->vendor-id)
|
||||
:transaction/bank-account bank-account-id
|
||||
:transaction/client client-id)
|
||||
(#(println (:transaction/date %)))))))
|
||||
(try
|
||||
(rebuild-search-index)
|
||||
(catch Exception e
|
||||
(alog/error ::cant-index-plaid
|
||||
:error e)
|
||||
(println "CANT INDEX")))
|
||||
(t/finish! import-batch)
|
||||
(println "DONE")
|
||||
(catch Exception e
|
||||
(println "FAIL")
|
||||
(t/fail! import-batch e)))))
|
||||
|
||||
(import-plaid-int-2)
|
||||
|
||||
{:transaction/bank-account 17592234448533, :transaction/date #inst "2024-05-14T07:00:00.000-00:00", :transaction/client 17592234448526, :transaction/status "POSTED", :transaction/plaid-merchant {:plaid-merchant/name "Integreat Restau", :db/id "99cb3ac3-1326-4090-8e36-721a0db3a7cf"}, :db/id "89d4fb46-bb17-436f-b1f9-505bfd67e3ec", :transaction/id "0c56701d74584f800b19b1ce6c7b15212b420626a0d0d28761bab4fec4e10ee8", :transaction/description-original "INTEGREAT RESTAU DES:ACH ID:408-340-3111 INDN:PALA UMBERTO CO ID:XXXXX03620 CCD", :transaction/amount -275.0, :transaction/raw-id "drKydaj39qUPPaR0DQyyHVrD4zb8XBIyxe9QJ"}
|
||||
(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:bank-account/code "NGGG-CB"])
|
||||
|
||||
@@ -85,30 +85,35 @@
|
||||
(alog/info ::searching-unpaid-invoice
|
||||
:client-id client-id
|
||||
:amount amount)
|
||||
(let [candidate-invoices-vendor-groups (->> (dc/q {:find ['?vendor-id '?e '?outstanding-balance '?d]
|
||||
:in ['$ '?client-id]
|
||||
:where ['[?e :invoice/client ?client-id]
|
||||
'[?e :invoice/status :invoice-status/unpaid]
|
||||
'(not [_ :invoice-payment/invoice ?e])
|
||||
'[?e :invoice/vendor ?vendor-id]
|
||||
'[?e :invoice/outstanding-balance ?outstanding-balance]
|
||||
'[?e :invoice/date ?d]]}
|
||||
(dc/db conn) client-id)
|
||||
(sort-by last) ;; sort by scheduled payment date
|
||||
(group-by first) ;; group by vendors
|
||||
vals)
|
||||
considerations (for [candidate-invoices candidate-invoices-vendor-groups
|
||||
invoice-count (range 1 32)
|
||||
consideration (partition invoice-count 1 candidate-invoices)
|
||||
:when (dollars= (reduce (fn [acc [_ _ amount]]
|
||||
(+ acc amount)) 0.0 consideration)
|
||||
(- amount))]
|
||||
consideration)]
|
||||
(alog/info ::unpaid-invoice-considerations-found
|
||||
:client-id client-id
|
||||
:amount amount
|
||||
:count (count considerations))
|
||||
considerations))
|
||||
(try
|
||||
(let [candidate-invoices-vendor-groups (->> (dc/q {:find ['?vendor-id '?e '?outstanding-balance '?d]
|
||||
:in ['$ '?client-id]
|
||||
:where ['[?e :invoice/client ?client-id]
|
||||
'[?e :invoice/status :invoice-status/unpaid]
|
||||
'(not [_ :invoice-payment/invoice ?e])
|
||||
'[?e :invoice/vendor ?vendor-id]
|
||||
'[?e :invoice/outstanding-balance ?outstanding-balance]
|
||||
'[?e :invoice/date ?d]]}
|
||||
(dc/db conn) client-id)
|
||||
(sort-by last) ;; sort by scheduled payment date
|
||||
(group-by first) ;; group by vendors
|
||||
vals)
|
||||
considerations (for [candidate-invoices candidate-invoices-vendor-groups
|
||||
invoice-count (range 1 32)
|
||||
consideration (partition invoice-count 1 candidate-invoices)
|
||||
:when (dollars= (reduce (fn [acc [_ _ amount]]
|
||||
(+ acc amount)) 0.0 consideration)
|
||||
(- amount))]
|
||||
consideration)]
|
||||
(alog/info ::unpaid-invoice-considerations-found
|
||||
:client-id client-id
|
||||
:amount amount
|
||||
:count (count considerations))
|
||||
considerations)
|
||||
(catch Exception e
|
||||
(alog/error ::cant-get-considerations
|
||||
:error e)
|
||||
[])))
|
||||
|
||||
(defn match-transaction-to-single-unfulfilled-autopayments [amount client-id]
|
||||
(let [considerations (match-transaction-to-unfulfilled-autopayments amount client-id)]
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
(ns auto-ap.intuit.core
|
||||
(:require
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[clj-http.client :as client]
|
||||
[clojure.core.memoize :as m]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[config.core :as cfg :refer [env]])
|
||||
(:import
|
||||
(org.apache.commons.codec.binary Base64)))
|
||||
(:require [amazonica.aws.s3 :as s3]
|
||||
[clj-http.client :as client]
|
||||
[clj-time.coerce :as c]
|
||||
[clojure.core.memoize :as m]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[config.core :as cfg :refer [env]])
|
||||
(:import (org.apache.commons.codec.binary Base64)))
|
||||
|
||||
;; (def authorization-code "AB11638463964I0tYPR3A1inog2HL407u2bZBXHg6LEqCbILRO")
|
||||
;; (def realm-id "4620816365202617680")
|
||||
@@ -98,6 +97,13 @@
|
||||
{:headers base-headers
|
||||
:as :json})
|
||||
|
||||
(defn get-bank-accounts-raw [token]
|
||||
(->> (:body (client/get (str prod-base-url "/company/" prod-company-id "/query" )
|
||||
{:headers
|
||||
(with-auth prod-base-headers token)
|
||||
:as :json
|
||||
:query-params {"query" "SELECT * From Account maxresults 1000"}}))
|
||||
:QueryResponse))
|
||||
|
||||
|
||||
(defn get-bank-accounts [token]
|
||||
@@ -110,10 +116,13 @@
|
||||
:Account
|
||||
#_(filter
|
||||
#(#{"Bank" "Credit Card"} (:AccountType %)))
|
||||
(map (juxt :Id :Name))
|
||||
(map (fn [[id name]]
|
||||
(map (juxt :Id :Name :CurrentBalance :MetaData))
|
||||
(map (fn [[id name current-balance metadata]]
|
||||
{:id id
|
||||
:name name}))))
|
||||
:name name
|
||||
:last-updated (c/to-date-time (-> metadata :LastUpdatedTime))
|
||||
:current-balance (try (double current-balance) (catch Exception _ nil))}))))
|
||||
|
||||
|
||||
(defn get-all-transactions [start end]
|
||||
(let [token (get-fresh-access-token)]
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
(let [client (dc/pull (dc/db auto-ap.datomic/conn)
|
||||
square3/square-read
|
||||
client)
|
||||
days (Long/parseLong days)]
|
||||
days (cond-> days (string? days) ( #(Long/parseLong %)))]
|
||||
(doseq [square-location (:client/square-locations client)
|
||||
:when (:square-location/client-location square-location)]
|
||||
|
||||
(println "orders")
|
||||
(doseq [d (per/periodic-seq (time/plus (time/today) (time/days (- days)))
|
||||
(time/today)
|
||||
(time/plus (time/today) (time/days 2))
|
||||
(time/days 1))]
|
||||
(println d)
|
||||
@(square3/upsert client square-location (coerce/to-date-time d) (coerce/to-date-time (time/plus d (time/days 1)))))
|
||||
@@ -34,7 +34,8 @@
|
||||
|
||||
(defn load-historical-sales [args]
|
||||
(let [{:keys [days client]} args
|
||||
client (Long/parseLong client)]
|
||||
client (cond-> client
|
||||
( string? client) ( #( Long/parseLong %)))]
|
||||
(historical-load-sales client days)))
|
||||
|
||||
(defn -main [& _]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[clj-time.coerce :as c]
|
||||
[clj-time.core :as time]
|
||||
[clj-time.periodic :as per]
|
||||
[clojure.string :as str]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[datomic.api :as dc]))
|
||||
|
||||
@@ -31,6 +32,22 @@
|
||||
(dc/db conn))]
|
||||
(apply mark-dirty c (last-n-days days))))
|
||||
|
||||
(defn lookup-account [number]
|
||||
(ffirst (dc/q '[:find ?a
|
||||
:in $ ?number
|
||||
:where [?a :account/numeric-code ?number]]
|
||||
(dc/db conn)
|
||||
number)))
|
||||
|
||||
|
||||
(defn delete-all []
|
||||
@(dc/transact-async conn
|
||||
(->>
|
||||
(dc/q '[:find ?ss
|
||||
:where [?ss :sales-summary/date]]
|
||||
(dc/db conn))
|
||||
(map (fn [[ ss]]
|
||||
[:db/retractEntity ss])))))
|
||||
|
||||
|
||||
|
||||
@@ -43,8 +60,223 @@
|
||||
(filter (fn [sales-summary]
|
||||
(= client-id (:db/id (:sales-summary/client sales-summary))))))))
|
||||
|
||||
(defn- get-fee [c date]
|
||||
(- (or (ffirst (dc/q '[:find ?f
|
||||
:in $ ?client ?d
|
||||
:where
|
||||
[?e :expected-deposit/client ?client]
|
||||
[?e :expected-deposit/sales-date ?d]
|
||||
[?e :expected-deposit/fee ?f]]
|
||||
(dc/db conn)
|
||||
c
|
||||
date))
|
||||
0.0)))
|
||||
|
||||
(defn sales-summaries []
|
||||
(def name->number
|
||||
{"gyros and pitas" 40111
|
||||
"returns" 41300
|
||||
"card payments" 75460
|
||||
"cash payments" 75452
|
||||
"cash refunds" 41400
|
||||
"food app payments" 72350
|
||||
"unknown" 40000
|
||||
"discounts" 41000
|
||||
"fees" 75400
|
||||
"alcohol" 46900
|
||||
"beverages" 42000
|
||||
"bowls" 40118
|
||||
"catering" 43000
|
||||
"ezcater catering" 43010
|
||||
"desserts" 40116
|
||||
"fries" 40117
|
||||
"plates" 40113
|
||||
"sides" 40115
|
||||
"soup & salads" 40114
|
||||
"uncategorized" 40000
|
||||
"tax" 25700
|
||||
"tip" 25500
|
||||
"card refunds" 41400
|
||||
"food app refunds" 41400})
|
||||
|
||||
(defn get-payment-items [c date]
|
||||
(->>
|
||||
(dc/q '[:find ?processor ?type-name (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/type-name ?type-name]
|
||||
(or-join [?c ?processor]
|
||||
(and [?c :charge/processor ?p]
|
||||
[?p :db/ident ?processor])
|
||||
(and
|
||||
(not [?c :charge/processor])
|
||||
[(ground :ccp-processor/na) ?processor]))
|
||||
[?c :charge/total ?total]]
|
||||
(dc/db conn)
|
||||
[[c] date date])
|
||||
(reduce
|
||||
(fn [acc [processor type-name total]]
|
||||
(update
|
||||
acc
|
||||
(cond (= type-name "CARD")
|
||||
"Card Payments"
|
||||
(= type-name "CASH")
|
||||
"Cash Payments"
|
||||
(#{"SQUARE_GIFT_CARD" "WALLET" "GIFT_CARD"} type-name)
|
||||
"Gift Card Payments"
|
||||
(#{:ccp-processor/toast
|
||||
#_:ccp-processor/ezcater
|
||||
#_:ccp-processor/koala
|
||||
:ccp-processor/doordash
|
||||
:ccp-processor/grubhub
|
||||
:ccp-processor/uber-eats} processor)
|
||||
"Food App Payments"
|
||||
:else
|
||||
"Unknown")
|
||||
(fnil + 0.0)
|
||||
total))
|
||||
{})
|
||||
(map (fn [[k v]]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 0
|
||||
:sales-summary-item/category k
|
||||
|
||||
:ledger-mapped/amount (if (= "Card Payments" k)
|
||||
(- v (get-fee c date))
|
||||
v)
|
||||
:ledger-mapped/ledger-side :ledger-side/debit}))))
|
||||
|
||||
(defn get-discounts [c date]
|
||||
(when-let [discount (ffirst (dc/q '[:find (sum ?discount)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/discount ?discount]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 1
|
||||
:sales-summary-item/category "Discounts"
|
||||
:ledger-mapped/amount discount
|
||||
:ledger-mapped/ledger-side :ledger-side/debit}))
|
||||
|
||||
(defn get-refund-items [c date]
|
||||
(->>
|
||||
(dc/q '[:find ?type-name (sum ?t)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where
|
||||
:where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-refund/type ?type-name]
|
||||
[?e :sales-refund/total ?t]]
|
||||
(dc/db conn)
|
||||
[[c] date date])
|
||||
(reduce
|
||||
(fn [acc [type-name total]]
|
||||
(update
|
||||
acc
|
||||
(cond (= type-name "CARD")
|
||||
"Card Refunds"
|
||||
(= type-name "CASH")
|
||||
"Cash Refunds"
|
||||
:else
|
||||
"Food App Refunds")
|
||||
(fnil + 0.0)
|
||||
total))
|
||||
{})
|
||||
(map (fn [[k v]]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 3
|
||||
:sales-summary-item/category k
|
||||
:ledger-mapped/amount v
|
||||
:ledger-mapped/ledger-side :ledger-side/credit}))))
|
||||
|
||||
|
||||
|
||||
(defn get-fees [c date]
|
||||
(when-let [fee (get-fee c date)]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 2
|
||||
:sales-summary-item/category "Fees"
|
||||
:ledger-mapped/amount fee
|
||||
:ledger-mapped/ledger-side :ledger-side/debit}))
|
||||
|
||||
(defn- get-tax [c date]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/category "Tax"
|
||||
:sales-summary-item/sort-order 1
|
||||
:ledger-mapped/ledger-side :ledger-side/credit
|
||||
:ledger-mapped/amount
|
||||
(or (ffirst (dc/q '[:find (sum ?tax)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/tax ?tax]
|
||||
#_[?e :sales-order/charges ?c]
|
||||
#_[?c :charge/tax ?tax]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)})
|
||||
|
||||
(defn- get-tip [c date]
|
||||
{:ledger-mapped/ledger-side :ledger-side/credit
|
||||
:sales-summary-item/sort-order 2
|
||||
:db/id (str (java.util.UUID/randomUUID))
|
||||
|
||||
:sales-summary-item/category "Tip"
|
||||
:ledger-mapped/amount (or (ffirst (dc/q '[:find (sum ?tip)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/tip ?tip]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)})
|
||||
|
||||
(defn- get-sales [c date]
|
||||
(let [sales (->> (dc/q '[:find ?category (sum ?total) (sum ?tax) (sum ?discount)
|
||||
:with ?e ?li
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/line-items ?li]
|
||||
[(get-else $ ?li :order-line-item/category "Unknown") ?category]
|
||||
[?li :order-line-item/total ?total]
|
||||
[?li :order-line-item/tax ?tax]
|
||||
[?li :order-line-item/discount ?discount]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))]
|
||||
(for [[category total tax discount] sales]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/category category
|
||||
:sales-summary-item/sort-order 0
|
||||
:sales-summary-item/total total
|
||||
:sales-summary-item/net (- (+ total discount) tax)
|
||||
:sales-summary-item/tax tax
|
||||
:sales-summary-item/discount discount
|
||||
:ledger-mapped/ledger-side :ledger-side/credit
|
||||
:ledger-mapped/amount (- (+ total discount) tax)
|
||||
#_#_:ledger-mapped/account nil})))
|
||||
|
||||
(defn- get-returns [c date]
|
||||
(when-let [amount (ffirst (dc/q '[:find (sum ?r)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/returns ?r]
|
||||
|
||||
#_[?e :sales-order/charges ?c]
|
||||
#_[?c :charge/tax ?tax]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/category "Returns"
|
||||
|
||||
:ledger-mapped/amount amount
|
||||
:ledger-mapped/ledger-side :ledger-side/debit}))
|
||||
|
||||
(defn sales-summaries-v2 []
|
||||
(doseq [[c client-code] (dc/q '[:find ?c ?client-code
|
||||
:in $
|
||||
:where [?c :client/code ?client-code]]
|
||||
@@ -53,181 +285,41 @@
|
||||
(mu/with-context {:client-code client-code
|
||||
:date date}
|
||||
(alog/info ::updating)
|
||||
(let [sales (->> (dc/q '[:find ?item-name ?category (sum ?total) (sum ?tax) (sum ?discount)
|
||||
:with ?e ?li
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/line-items ?li]
|
||||
[?li :order-line-item/item-name ?item-name]
|
||||
[?li :order-line-item/category ?category]
|
||||
[?li :order-line-item/total ?total]
|
||||
[?li :order-line-item/tax ?tax]
|
||||
[?li :order-line-item/discount ?discount]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
result {:db/id id
|
||||
(let [result {:db/id id
|
||||
:sales-summary/client c
|
||||
:sales-summary/date date
|
||||
:sales-summary/dirty false
|
||||
:sales-summary/client+date [c date]
|
||||
:sales-summary/discount (or (ffirst (dc/q '[:find (sum ?discount)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/discount ?discount]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
:sales-summary/total-returns (or (ffirst (dc/q '[:find (sum ?r)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/returns ?r]
|
||||
|
||||
#_[?e :sales-order/charges ?c]
|
||||
#_[?c :charge/tax ?tax]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
|
||||
:sales-summary/sales-items
|
||||
|
||||
|
||||
|
||||
(for [[item-name category total tax discount] sales]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/item-name item-name
|
||||
:sales-summary-item/category category
|
||||
:sales-summary-item/total total
|
||||
:sales-summary-item/tax tax
|
||||
:sales-summary-item/discount discount})
|
||||
|
||||
:sales-summary/total-tax
|
||||
(or (ffirst (dc/q '[:find (sum ?tax)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/tax ?tax]
|
||||
#_[?e :sales-order/charges ?c]
|
||||
#_[?c :charge/tax ?tax]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
:sales-summary/total-tip
|
||||
(or (ffirst (dc/q '[:find (sum ?tip)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/tip ?tip]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
|
||||
:sales-summary/total-card-payments
|
||||
(or (ffirst (dc/q '[:find (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/type-name "CARD"]
|
||||
[?c :charge/total ?total]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
:sales-summary/total-card-fees
|
||||
(or (ffirst (dc/q '[:find ?f
|
||||
:in $ ?client ?d
|
||||
:where
|
||||
[?e :expected-deposit/client ?client]
|
||||
[?e :expected-deposit/sales-date ?d]
|
||||
[?e :expected-deposit/fee ?f]]
|
||||
(dc/db conn)
|
||||
c
|
||||
date))
|
||||
0.0)
|
||||
:sales-summary/total-card-refunds
|
||||
(or (ffirst (dc/q '[:find (sum ?t)
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where
|
||||
:where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-refund/type "CARD"]
|
||||
[?e :sales-refund/total ?t]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
|
||||
:sales-summary/total-cash-payments
|
||||
(or (ffirst (dc/q '[:find (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/total ?total]
|
||||
[?c :charge/type-name "CASH"]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
|
||||
:sales-summary/total-gift-card-payments
|
||||
(or (ffirst (dc/q '[:find (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/total ?total]
|
||||
(or [?c :charge/type-name "SQUARE_GIFT_CARD"]
|
||||
[?c :charge/type-name "WALLET"]
|
||||
[?c :charge/type-name "GIFT_CARD"])]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
|
||||
:sales-summary/total-cash-refunds
|
||||
(or (ffirst (dc/q '[:find (sum ?t)
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where
|
||||
:where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-refund/type "CASH"]
|
||||
[?e :sales-refund/total ?t]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)
|
||||
|
||||
:sales-summary/total-food-app-payments
|
||||
|
||||
(or (ffirst (dc/q '[:find (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date] [?processor ...]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/processor ?processor]
|
||||
[?c :charge/total ?total]]
|
||||
(dc/db conn)
|
||||
[[c] date date]
|
||||
#{:ccp-processor/toast
|
||||
#_:ccp-processor/ezcater
|
||||
#_:ccp-processor/koala
|
||||
:ccp-processor/doordash
|
||||
:ccp-processor/grubhub
|
||||
:ccp-processor/uber-eats}))
|
||||
0.0)
|
||||
:sales-summary/total-food-app-refunds
|
||||
(or (ffirst (dc/q '[:find (sum ?t)
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-refund/type "EXTERNAL"]
|
||||
[?e :sales-refund/total ?t]]
|
||||
(dc/db conn)
|
||||
[[c] date date]))
|
||||
0.0)}]
|
||||
(if (seq (:sales-summary/sales-items result))
|
||||
:sales-summary/items
|
||||
(->>
|
||||
(get-sales c date)
|
||||
(concat (get-payment-items c date))
|
||||
(concat (get-refund-items c date))
|
||||
(cons (get-discounts c date))
|
||||
(cons (get-fees c date))
|
||||
(cons (get-tax c date))
|
||||
(cons (get-tip c date))
|
||||
(cons (get-returns c date))
|
||||
(filter identity)
|
||||
(map (fn [z]
|
||||
(assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account)
|
||||
:sales-summary-item/manual? false))
|
||||
)) }]
|
||||
(if (seq (:sales-summary/items result))
|
||||
(do
|
||||
(alog/info ::upserting-summaries
|
||||
:category-count (count (:sales-summary/sales-items result)))
|
||||
:category-count (count (:sales-summary/items result)))
|
||||
@(dc/transact conn [[:upsert-entity result]]))
|
||||
@(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
|
||||
|
||||
(let [c (auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGCL" ])
|
||||
date #inst "2024-04-14T00:00:00-07:00"]
|
||||
(get-payment-items c date)
|
||||
|
||||
)
|
||||
|
||||
|
||||
(defn reset-summaries []
|
||||
@(dc/transact conn (->> (dc/q '[:find ?sos
|
||||
:in $
|
||||
@@ -242,21 +334,58 @@
|
||||
(comment
|
||||
(auto-ap.datomic/transact-schema conn)
|
||||
|
||||
(apply mark-dirty [:client/code "NGCL"] (last-n-days 12))
|
||||
@(dc/transact conn [{:db/ident :sales-summary/total-unknown-processor-payments
|
||||
:db/noHistory true,
|
||||
:db/valueType :db.type/double
|
||||
:db/cardinality :db.cardinality/one}])
|
||||
|
||||
(mark-all-dirty 30)
|
||||
(apply mark-dirty [:client/code "NGCL"] (last-n-days 30))
|
||||
|
||||
(sales-summaries)
|
||||
(apply mark-dirty [:client/code "NGDG"] (last-n-days 30))
|
||||
|
||||
(apply mark-dirty [:client/code "NGPG"] (last-n-days 30))
|
||||
|
||||
(mark-all-dirty 50)
|
||||
(delete-all)
|
||||
|
||||
(sales-summaries-v2)
|
||||
|
||||
(dc/q '[:find (pull ?sos [* {:sales-summary/sales-items [*]}])
|
||||
:in $
|
||||
:where [?sos :sales-summary/client [:client/code "NGCL"]]
|
||||
:where [?sos :sales-summary/client [:client/code "NGHW"]]
|
||||
[?sos :sales-summary/date ?d]
|
||||
[(= ?d #inst "2024-03-25T00:00:00-07:00")]]
|
||||
(dc/db conn)))
|
||||
[(= ?d #inst "2024-04-10T00:00:00-07:00")]]
|
||||
(dc/db conn))
|
||||
|
||||
(dc/q '[:find ?n ?p2 (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/type-name ?n]
|
||||
[?c :charge/processor ?p]
|
||||
[?p :db/ident ?p2]
|
||||
[?c :charge/total ?total]]
|
||||
(dc/db conn)
|
||||
[[(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGHW"])] #inst "2024-04-11T00:00:00-07:00" #inst "2024-04-11T00:00:00-07:00"])
|
||||
|
||||
(dc/q '[:find ?n
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/line-items ?li]
|
||||
[?li :order-line-item/item-name ?n] ]
|
||||
(dc/db conn)
|
||||
[[(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGCL"])] #inst "2024-04-11T00:00:00-07:00" #inst "2024-04-24T00:00:00-07:00"])
|
||||
|
||||
@(dc/transact conn [{:db/id :sales-summary/total-tax :db/ident :sales-summary/total-tax-legacy}
|
||||
{:db/id :sales-summary/total-tip :db/ident :sales-summary/total-tip-legacy}])
|
||||
|
||||
(auto-ap.datomic/transact-schema conn)
|
||||
|
||||
)
|
||||
|
||||
|
||||
|
||||
(defn -main [& _]
|
||||
(execute "sales-summaries" sales-summaries))
|
||||
(execute "sales-summaries" sales-summaries-v2))
|
||||
|
||||
@@ -1,29 +1,55 @@
|
||||
(ns auto-ap.jobs.sysco
|
||||
(:require
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.jobs.core :refer [execute]]
|
||||
[auto-ap.datomic :refer [audit-transact random-tempid]]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.invoices :refer [code-invoice]]
|
||||
[auto-ap.parse :as parse]
|
||||
[auto-ap.time :as t]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.data.csv :as csv]
|
||||
[clojure.java.io :as io]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[auto-ap.logging :as alog]
|
||||
[clojure.string :as str]
|
||||
[com.unbounce.dogstatsd.core :as statsd]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]
|
||||
[auto-ap.datomic.vendors :as d-vendors])
|
||||
(:import
|
||||
(java.util UUID)))
|
||||
(:require [amazonica.aws.s3 :as s3]
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.datomic :refer [audit-transact pull-attr random-tempid]]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.invoices :refer [code-invoice]]
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
[auto-ap.jobs.core :refer [execute]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.parse :as parse]
|
||||
[auto-ap.time :as t]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.data.csv :as csv]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc])
|
||||
(:import (java.util UUID)))
|
||||
|
||||
(def bucket-name (:data-bucket env))
|
||||
(def sysco-name->line (atom nil))
|
||||
(defn get-sysco->line []
|
||||
(when (nil? @sysco-name->line)
|
||||
(reset! sysco-name->line
|
||||
(with-open [data (io/reader (io/resource "sysco_line_item_mapping.csv"))]
|
||||
(let [data (csv/read-csv data)]
|
||||
(->> data
|
||||
(drop 1)
|
||||
(map (fn [[_ _ name _ account-number]]
|
||||
[name (ffirst (dc/q '[:find ?a
|
||||
:in $ ?an
|
||||
:where [?a :account/numeric-code ?an]]
|
||||
(dc/db conn)
|
||||
(Long/parseLong account-number)))]))
|
||||
(into {}))))))
|
||||
@sysco-name->line)
|
||||
|
||||
(defn get-line-account [item-name]
|
||||
(get (get-sysco->line)
|
||||
item-name
|
||||
(ffirst (dc/q '[:find ?a
|
||||
:in $ ?an
|
||||
:where [?a :account/numeric-code ?an]]
|
||||
(dc/db conn)
|
||||
50000))))
|
||||
|
||||
|
||||
(def ^:dynamic bucket-name (:data-bucket env))
|
||||
|
||||
(def header-keys ["TransCode" "GroupID" "Company" "CustomerNumber" "InvoiceNumber" "RecordType" "Item" "InvoiceDocument" "AccountName" "AccountDunsNo" "InvoiceDate" "AccountDate" "CustomerPONo" "PaymentTerms" "TermsDescription" "StoreNumber" "CustomerName" "AddressLine1" "AddressLine2" "City1" "State1" "Zip1" "Phone1" "Duns1" "Hin1" "Dea1" "TIDCustomer" "ChainNumber" "BidNumber" "ContractNumber" "CompanyNumber" "BriefName" "Address" "Address2" "City2" "State2" "Zip2" "Phone2" "Duns2" "Hin2" "Dea2" "Tid_OPCO" "ObligationIndicator" "Manifest" "Route" "Stop" "TermsDiscountPercent" "TermsDiscountDueDate" "TermsNetDueDate" "TermsDiscountAmount" "TermsDiscountCode" "OrderDate" "DepartmentCode"])
|
||||
(def item-price-index 15)
|
||||
(def item-name-index 29)
|
||||
|
||||
(def summary-keys ["TranCode" "GroupID" "Company" "CustomerNumber" "InvoiceNumber" "RecordType" "Item" "InvoiceDocument" "TotalLines" "TotalQtyInvoice" "TotalQty" "TotalQtySplit" "TotalQtyPounds" "TotalExtendedPrice" "TotalTaxAmount" "TotalInvoiceAmount" "AccountDate"])
|
||||
|
||||
@@ -46,6 +72,38 @@
|
||||
io/reader
|
||||
csv/read-csv))
|
||||
|
||||
(defn check-okay-amount? [i]
|
||||
(dollars=
|
||||
(:invoice/total i)
|
||||
(reduce + 0.0 (map :invoice-expense-account/amount (:invoice/expense-accounts i)))))
|
||||
|
||||
(defn code-individual-items [invoice csv-rows tax]
|
||||
(let [items (->> csv-rows
|
||||
butlast
|
||||
(reduce
|
||||
(fn [acc row]
|
||||
(update acc (get-line-account (nth row item-name-index))
|
||||
(fnil + 0.0)
|
||||
(Double/parseDouble (nth row item-price-index))
|
||||
)
|
||||
)
|
||||
{})
|
||||
)
|
||||
items-with-tax (update items (get-line-account "TAX")
|
||||
(fnil + 0.0)
|
||||
tax)
|
||||
updated-invoice (assoc invoice :invoice/expense-accounts
|
||||
(for [[account amount] items-with-tax]
|
||||
#:invoice-expense-account {:db/id (random-tempid)
|
||||
:account account
|
||||
:location (:invoice/location invoice)
|
||||
:amount amount}))]
|
||||
(if (check-okay-amount? updated-invoice)
|
||||
updated-invoice
|
||||
(do (alog/warn ::itemized-expenses-not-adding-up
|
||||
:invoice updated-invoice)
|
||||
invoice))))
|
||||
|
||||
(defn extract-invoice-details [csv-rows sysco-vendor]
|
||||
(let [[header-row & csv-rows] csv-rows
|
||||
header-row (into {} (map vector header-keys header-row))
|
||||
@@ -64,14 +122,17 @@
|
||||
(header-row "AddressLine2")
|
||||
(header-row "City1")
|
||||
(header-row "City2")])
|
||||
|
||||
|
||||
account-number (some-> account-number Long/parseLong str)
|
||||
matching-client (and account-number
|
||||
(d-clients/exact-match account-number))
|
||||
(d-clients/exact-match account-number))
|
||||
|
||||
_ (when-not matching-client
|
||||
(throw (ex-info "cannot find matching client"
|
||||
{:account-number account-number
|
||||
:name customer-identifier})))
|
||||
code-items (get (into #{} (pull-attr (dc/db conn) :client/feature-flags (:db/id matching-client)))
|
||||
"code-sysco-items")
|
||||
total (Double/parseDouble (summary-row "TotalExtendedPrice"))
|
||||
tax (Double/parseDouble (summary-row "TotalTaxAmount"))
|
||||
date (t/parse
|
||||
@@ -95,10 +156,11 @@
|
||||
:date (coerce/to-date date)
|
||||
:vendor (:db/id sysco-vendor )
|
||||
:client (:db/id matching-client)
|
||||
:import-status :import-status/completed
|
||||
:import-status :import-status/imported
|
||||
:status :invoice-status/unpaid
|
||||
:client-identifier customer-identifier}
|
||||
true (code-invoice))))
|
||||
true (code-invoice)
|
||||
code-items (code-individual-items csv-rows tax))))
|
||||
|
||||
(defn mark-key [k]
|
||||
(s3/copy-object {:source-bucket-name bucket-name
|
||||
@@ -117,6 +179,33 @@
|
||||
(s3/delete-object {:bucket-name bucket-name
|
||||
:key k}))
|
||||
|
||||
(defn get-test-invoice-file
|
||||
([] (get-test-invoice-file 999))
|
||||
( [i]
|
||||
(nth (->> (s3/list-objects-v2 {:bucket-name "data.prod.app.integreatconsult.com"
|
||||
:prefix "sysco/imported"})
|
||||
:object-summaries
|
||||
(map :key)
|
||||
)
|
||||
i)))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(comment
|
||||
(with-bindings { #'bucket-name "data.prod.app.integreatconsult.com"}
|
||||
(doall
|
||||
(for [n (range 930 940 )
|
||||
:let [result (-> (get-test-invoice-file n)
|
||||
read-sysco-csv
|
||||
(extract-invoice-details (get-sysco-vendor))
|
||||
)]
|
||||
#_#_:when (not (check-okay-amount? result))]
|
||||
|
||||
result)))
|
||||
|
||||
)
|
||||
|
||||
(defn import-sysco []
|
||||
(let [sysco-vendor (get-sysco-vendor)
|
||||
|
||||
@@ -575,7 +575,6 @@
|
||||
(refresh-running-balance-accounts accounts-needing-rebuild clients c i db)
|
||||
(mu/log ::client-completed))))))))
|
||||
|
||||
|
||||
;; TODO only enable once IOL is set up in clod
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(mount/defstate running-balance-cache-worker
|
||||
|
||||
@@ -650,29 +650,72 @@
|
||||
:parser {:date [:clj-time "MM/dd/yyyy"]
|
||||
:total [:trim-commas-and-negate nil]}
|
||||
:multi #"\n"
|
||||
:multi-match? #"^\s*Invoice\s{2,}"}])
|
||||
:multi-match? #"^\s*Invoice\s{2,}"}
|
||||
{:vendor "Reel Produce"
|
||||
:keywords [#"REEL Produce, Inc" #"Statement"]
|
||||
:extract {:date #"\s*([0-9]+/[0-9]+/[0-9]+)"
|
||||
:customer-identifier #"To:\s*\n\s+(.*?)\s{2,}"
|
||||
:invoice-number #"INV #(\d+)"
|
||||
:total #"INV #(?:.*?)\s{2,}([\d\-,]+\.\d{2,2}+)"}
|
||||
:parser {:date [:clj-time "MM/dd/yyyy"]
|
||||
:total [:trim-commas-and-negate nil]}
|
||||
:multi #"\n"
|
||||
:multi-match? #"INV #"}
|
||||
{:vendor "Paulino's Bakery"
|
||||
:keywords [#"Paulino's Bakery" #"Statement"]
|
||||
:extract {:date #"\s*([0-9]+/[0-9]+/[0-9]+)"
|
||||
:customer-identifier #"To:\s*\n\s+(.*?)\s{2,}"
|
||||
:invoice-number #"INV #(\d+)"
|
||||
:total #"INV #(?:.*?)\s{2,}([\d\-,]+\.\d{2,2}+)"}
|
||||
:parser {:date [:clj-time "MM/dd/yyyy"]
|
||||
:total [:trim-commas-and-negate nil]}
|
||||
:multi #"\n"
|
||||
:multi-match? #"INV #"}])
|
||||
|
||||
(def excel-templates
|
||||
[{:vendor "Mama Lu's Foods"
|
||||
:keywords [#"Mama Lu's Foods"]
|
||||
:extract (fn [sheet vendor]
|
||||
(transduce (comp
|
||||
(drop 5)
|
||||
(filter
|
||||
(fn [r]
|
||||
(and
|
||||
(seq r)
|
||||
(->> r second not-empty))))
|
||||
(map
|
||||
(fn [r]
|
||||
(let [[_ customer-order-number num date name amount] r]
|
||||
{:customer-identifier (second (re-find #"([^:]*):" name))
|
||||
:text name
|
||||
:full-text name
|
||||
:date (u/parse-value :clj-time "MM/dd/yyyy" (str/trim date))
|
||||
:invoice-number (str customer-order-number "-" (Integer/parseInt num))
|
||||
:total (str amount)
|
||||
:vendor-code vendor}))))
|
||||
(drop 5)
|
||||
(filter
|
||||
(fn [r]
|
||||
(and
|
||||
(seq r)
|
||||
(->> r second not-empty))))
|
||||
(map
|
||||
(fn [r]
|
||||
(let [[_ customer-order-number num date name amount] r]
|
||||
{:customer-identifier (second (re-find #"([^:]*):" name))
|
||||
:text name
|
||||
:full-text name
|
||||
:date (u/parse-value :clj-time "MM/dd/yyyy" (str/trim date))
|
||||
:invoice-number (str customer-order-number "-" (Integer/parseInt num))
|
||||
:total (str amount)
|
||||
:vendor-code vendor}))))
|
||||
conj
|
||||
[]
|
||||
sheet))}
|
||||
|
||||
{:vendor "Daylight Foods"
|
||||
:keywords [#"CUSTNO"]
|
||||
:extract (fn [sheet vendor]
|
||||
(transduce (comp
|
||||
(drop 1)
|
||||
(filter
|
||||
(fn [r]
|
||||
(and
|
||||
(seq r)
|
||||
(->> r first not-empty))))
|
||||
(map
|
||||
(fn [[customer-number _ _ _ invoice-number date amount :as row]]
|
||||
{:customer-identifier customer-number
|
||||
:text (str/join " " row)
|
||||
:full-text (str/join " " row)
|
||||
:date (u/parse-value :clj-time "MM/dd/yyyy" (str/trim date))
|
||||
:invoice-number invoice-number
|
||||
:total (str amount)
|
||||
:vendor-code vendor})))
|
||||
conj
|
||||
[]
|
||||
sheet))}])
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
(ns auto-ap.plaid.core
|
||||
(:require
|
||||
[clj-http.client :as client]
|
||||
[clojure.data.json :as json]
|
||||
[auto-ap.logging :as alog]
|
||||
[config.core :as cfg :refer [env]]
|
||||
[auto-ap.time :as atime]))
|
||||
(:require [auto-ap.logging :as alog]
|
||||
[auto-ap.time :as atime]
|
||||
[cemerick.url :as url]
|
||||
[clj-http.client :as client]
|
||||
[clojure.data.json :as json]
|
||||
[config.core :as cfg :refer [env]]))
|
||||
|
||||
(def base-url (-> env :plaid :base-url))
|
||||
|
||||
(def client-id (-> env :plaid :client-id))
|
||||
(def secret-key (-> env :plaid :secret-key))
|
||||
|
||||
|
||||
(defn get-link-token [client-code]
|
||||
(-> (client/post (str base-url "/link/token/create")
|
||||
{:as :json
|
||||
@@ -49,8 +48,16 @@
|
||||
:body (json/write-str {"client_id" client-id
|
||||
"secret" secret-key
|
||||
"public_token" public-token})})
|
||||
:body
|
||||
(doto println)))
|
||||
:body))
|
||||
|
||||
(defn get-item [access-token ]
|
||||
(-> (client/post (str base-url "/item/get")
|
||||
{:as :json
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/write-str {"client_id" client-id
|
||||
"secret" secret-key
|
||||
"access_token" access-token})})
|
||||
:body))
|
||||
|
||||
(defn get-accounts [access-token ]
|
||||
(-> (client/post (str base-url "/accounts/get")
|
||||
@@ -61,6 +68,15 @@
|
||||
"access_token" access-token})})
|
||||
:body))
|
||||
|
||||
(defn get-balance [access-token ]
|
||||
(-> (client/post (str base-url "/accounts/balance/get")
|
||||
{:as :json
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/write-str {"access_token" access-token
|
||||
"secret" secret-key
|
||||
"client_id" client-id})})
|
||||
:body))
|
||||
|
||||
(defn get-transactions [access-token account-id start end]
|
||||
(alog/info ::searching
|
||||
:start (str start)
|
||||
@@ -74,7 +90,8 @@
|
||||
"access_token" access-token
|
||||
"start_date" (atime/unparse start atime/iso-date)
|
||||
"end_date" (atime/unparse end atime/iso-date)
|
||||
"options" {"account_ids" [account-id]}})})
|
||||
"options" {"account_ids" [account-id]
|
||||
"count" 500}})})
|
||||
:body))
|
||||
|
||||
(comment
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[clj-time.core :as time]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; TODO should be able to get rid of this
|
||||
(defn wrap-copy-qp-pqp [handler]
|
||||
(fn [request]
|
||||
(handler (assoc request :parsed-query-params (:query-params request)))))
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
(de/chain
|
||||
(de/loop [attempt 0]
|
||||
(-> (de/chain (de/future-with (ex/execute-pool)
|
||||
(log/info ::request-started
|
||||
#_(log/info ::request-started
|
||||
:url (:url request)
|
||||
:attempt attempt
|
||||
:source "Square 3"
|
||||
@@ -53,6 +53,7 @@
|
||||
#_#_:connection-request-timeout 5000
|
||||
:as :json))
|
||||
(catch Throwable e
|
||||
(println e)
|
||||
(log/warn ::raw-request-failed
|
||||
:exception e)
|
||||
(throw e)))))
|
||||
@@ -313,7 +314,8 @@
|
||||
(capture-context->lc
|
||||
(let [is-order-only-for-charge? (= ["CUSTOM_AMOUNT"]
|
||||
(mapv :item_type (:line_items order)))]
|
||||
(if is-order-only-for-charge?
|
||||
(if (and is-order-only-for-charge?
|
||||
(not ((set (:client/feature-flags client)) "import-custom-amount")))
|
||||
(de/success-deferred
|
||||
(->> (:tenders order)
|
||||
(map #(tender->charge order client location %))))
|
||||
@@ -371,6 +373,7 @@
|
||||
(fn [e]
|
||||
(log/error ::failed-to-transform-order
|
||||
:exception e)))))))
|
||||
|
||||
(defn should-import-order? [order]
|
||||
;; sometimes orders stay open in square. At least one payment
|
||||
;; is needed to import, in order to avoid importing orders in-progress.
|
||||
@@ -731,6 +734,7 @@
|
||||
(def square-read [:db/id
|
||||
:client/code
|
||||
:client/square-auth-token
|
||||
:client/feature-flags
|
||||
{:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}])
|
||||
|
||||
(defn get-square-clients
|
||||
@@ -739,6 +743,7 @@
|
||||
:client/square-integration-status
|
||||
:client/code
|
||||
:client/square-auth-token
|
||||
:client/feature-flags
|
||||
{:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}])
|
||||
:in $
|
||||
:where [?c :client/square-auth-token]]
|
||||
@@ -747,6 +752,7 @@
|
||||
(map first (dc/q '[:find (pull ?c [:db/id
|
||||
:client/code
|
||||
:client/square-auth-token
|
||||
:client/feature-flags
|
||||
{:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}])
|
||||
:in $ [?code ...]
|
||||
:where [?c :client/square-auth-token]
|
||||
@@ -784,13 +790,17 @@
|
||||
:square-location/square-id (:id square-location)})))))))
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn reset []
|
||||
(defn reset [client]
|
||||
(->>
|
||||
(dc/q {:find ['?e]
|
||||
:in ['$]
|
||||
:where ['(or [?e :sales-order/date]
|
||||
[?e :expected-deposit/date])]}
|
||||
(dc/db conn))
|
||||
:in ['$ '?c]
|
||||
:where ['(or [?e :sales-order/client ?c]
|
||||
[?e :expected-deposit/client ?c]
|
||||
[?e :sales-refund/client ?c]
|
||||
[?e :charge/client ?c]
|
||||
[?e :cash-drawer-shift/client ?c])]}
|
||||
(dc/db conn)
|
||||
client)
|
||||
(map first)
|
||||
(map (fn [x] [:db/retractEntity x]))))
|
||||
|
||||
@@ -800,6 +810,69 @@
|
||||
:client/square-integration-status (assoc integration-status
|
||||
:db/id (or (-> client :client/square-integration-status :db/id)
|
||||
(str (java.util.UUID/randomUUID))))}]))
|
||||
(defn max-date [d1 d2]
|
||||
(if (time/after? d1 d2)
|
||||
d1
|
||||
d2))
|
||||
|
||||
|
||||
|
||||
(defn remove-voided-orders
|
||||
([client]
|
||||
(apply de/zip
|
||||
(for [square-location (:client/square-locations client)
|
||||
:when (:square-location/client-location square-location)]
|
||||
(remove-voided-orders client square-location (time/plus (time/now) (time/days -14)) (time/now)))))
|
||||
([client location start end]
|
||||
(let [start (max-date start (coerce/to-date-time #inst "2024-04-15T00:00:00-08:00"))]
|
||||
(capture-context->lc
|
||||
(-> (de/chain (search client location start end)
|
||||
(fn [search-results]
|
||||
(->> (or search-results [])
|
||||
(s/->source)
|
||||
(s/filter #(not (should-import-order? %)))
|
||||
(s/map #(mu/with-context lc (order->sales-order client location %)))
|
||||
(s/buffer 10)
|
||||
(s/realize-each)
|
||||
(s/filter (fn already-exists [[o]]
|
||||
(when (:sales-order/external-id o)
|
||||
(seq (dc/q '[:find ?i
|
||||
:in $ ?ei
|
||||
:where [?i :sales-order/external-id ?ei]]
|
||||
(dc/db conn)
|
||||
(:sales-order/external-id o))))))
|
||||
(s/map (fn [[o]]
|
||||
[[:db/retractEntity [:sales-order/external-id (:sales-order/external-id o)]]]))
|
||||
|
||||
(s/reduce into [])))
|
||||
|
||||
(fn [results]
|
||||
(mu/with-context lc
|
||||
(doseq [x (partition-all 100 results)]
|
||||
(log/info ::removing-orders
|
||||
:count (count x))
|
||||
@(dc/transact-async conn x)))))
|
||||
(de/catch (fn [e]
|
||||
(log/warn ::couldnt-remove :error e)
|
||||
nil) ))))))
|
||||
|
||||
#_(comment
|
||||
(require 'auto-ap.time-reader)
|
||||
|
||||
|
||||
|
||||
@(let [[c [l]] (get-square-client-and-location "NGAK") ]
|
||||
(log/peek :x [ c l])
|
||||
|
||||
(remove-voided-orders c l #clj-time/date-time "2024-04-11" #clj-time/date-time "2024-04-15"))
|
||||
(doseq [c (get-square-clients)]
|
||||
(try
|
||||
@(remove-voided-orders c)
|
||||
(catch Exception e
|
||||
nil)))
|
||||
|
||||
|
||||
)
|
||||
|
||||
(defn upsert-all [& clients]
|
||||
(capture-context->lc
|
||||
@@ -819,6 +892,10 @@
|
||||
(mu/with-context lc
|
||||
(log/info ::upsert-orders-started)
|
||||
(upsert client)))
|
||||
(fn [_]
|
||||
(mu/with-context lc
|
||||
(log/info ::remove-voided-orders-started)
|
||||
(remove-voided-orders client)))
|
||||
(fn [_]
|
||||
(mu/with-context lc
|
||||
(log/info ::upsert-payouts-started)
|
||||
@@ -936,7 +1013,25 @@
|
||||
|
||||
|
||||
|
||||
(clojure.pprint/pprint (let [[c [l]] (get-square-client-and-location "NGWC")]
|
||||
(require 'auto-ap.time-reader)
|
||||
|
||||
@(upsert-all "NGPG")
|
||||
|
||||
(clojure.pprint/pprint (let [[c [l]] (get-square-client-and-location "NGVT")]
|
||||
l
|
||||
|
||||
|
||||
(def z @(search c l #clj-time/date-time "2024-04-25T00:00:00-08:00"
|
||||
#clj-time/date-time "2024-04-28T00:00:00-08:00"))))
|
||||
|
||||
(->> z
|
||||
(filter (fn [o]
|
||||
(seq (filter (comp #{"OTHER"} :type) (:tenders o)))))
|
||||
(filter #(not (:name (:source %))))
|
||||
(count)
|
||||
|
||||
)
|
||||
|
||||
#_(filter (comp #{"OTHER"} :type) (mapcat :tenders z))
|
||||
|
||||
(get-order c l "yzmLBYVGhKXUPwGXm482GJb2VX9YY")))
|
||||
)
|
||||
@@ -35,61 +35,62 @@
|
||||
:name (first name)})))
|
||||
|
||||
|
||||
(defn account-search [{{:keys [q client-id purpose vendor-id] :as qp} :query-params id :identity}]
|
||||
(when client-id
|
||||
(assert-can-see-client id client-id))
|
||||
(let [num (some-> (re-find #"([0-9]+)" q)
|
||||
second
|
||||
(not-empty)
|
||||
Integer/parseInt)
|
||||
(defn account-search [{{:keys [q client-id purpose vendor-id] :as qp} :query-params id :identity :as request}]
|
||||
(let [client-id (or client-id (:db/id (:client request)))]
|
||||
(when client-id
|
||||
(assert-can-see-client id client-id))
|
||||
(let [num (some-> (re-find #"([0-9]+)" q)
|
||||
second
|
||||
(not-empty)
|
||||
Integer/parseInt)
|
||||
|
||||
valid-allowances (cond-> #{:allowance/allowed
|
||||
:allowance/warn}
|
||||
(is-admin? id) (conj :allowance/admin-only))
|
||||
allowance (cond (= purpose "vendor")
|
||||
:account/vendor-allowance
|
||||
(= purpose "invoice")
|
||||
:account/invoice-allowance
|
||||
:else
|
||||
:account/default-allowance)
|
||||
valid-allowances (cond-> #{:allowance/allowed
|
||||
:allowance/warn}
|
||||
(is-admin? id) (conj :allowance/admin-only))
|
||||
allowance (cond (= purpose "vendor")
|
||||
:account/vendor-allowance
|
||||
(= purpose "invoice")
|
||||
:account/invoice-allowance
|
||||
:else
|
||||
:account/default-allowance)
|
||||
|
||||
vendor-account (when vendor-id
|
||||
(-> (dc/q '[:find ?da
|
||||
:in $ ?v
|
||||
:where [?v :vendor/default-account ?da]]
|
||||
(dc/db conn)
|
||||
vendor-id)
|
||||
ffirst))
|
||||
xform (comp
|
||||
(filter (fn [[_ a]]
|
||||
(or
|
||||
(valid-allowances (-> a allowance :db/ident))
|
||||
(= (:db/id a) vendor-account))))
|
||||
(map (fn [[n a]]
|
||||
{:label (str (:account/numeric-code a) " - " n)
|
||||
:value (:db/id a)
|
||||
:location (:account/location a)
|
||||
:warning (when (= :allowance/warn (-> a allowance :db/ident))
|
||||
"This account is not typically used for this purpose.")})))]
|
||||
{:body (take 10 (if q
|
||||
(if num
|
||||
(->> (dc/q '[:find ?n (pull ?i pattern)
|
||||
:in $ ?numeric-code ?allowance pattern
|
||||
:where [?i :account/numeric-code ?numeric-code]
|
||||
[?i :account/name ?n]
|
||||
(or [?i :account/applicability :account-applicability/global]
|
||||
[?i :account/applicability :account-applicability/optional]
|
||||
[?i :account/applicability :account-applicability/customized])]
|
||||
(dc/db conn)
|
||||
num
|
||||
allowance
|
||||
search-pattern)
|
||||
(sequence xform))
|
||||
(->> (search- id q client-id)
|
||||
(sequence
|
||||
(comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))]))
|
||||
xform))))
|
||||
[]))}))
|
||||
vendor-account (when vendor-id
|
||||
(-> (dc/q '[:find ?da
|
||||
:in $ ?v
|
||||
:where [?v :vendor/default-account ?da]]
|
||||
(dc/db conn)
|
||||
vendor-id)
|
||||
ffirst))
|
||||
xform (comp
|
||||
(filter (fn [[_ a]]
|
||||
(or
|
||||
(valid-allowances (-> a allowance :db/ident))
|
||||
(= (:db/id a) vendor-account))))
|
||||
(map (fn [[n a]]
|
||||
{:label (str (:account/numeric-code a) " - " n)
|
||||
:value (:db/id a)
|
||||
:location (:account/location a)
|
||||
:warning (when (= :allowance/warn (-> a allowance :db/ident))
|
||||
"This account is not typically used for this purpose.")})))]
|
||||
{:body (take 10 (if q
|
||||
(if num
|
||||
(->> (dc/q '[:find ?n (pull ?i pattern)
|
||||
:in $ ?numeric-code ?allowance pattern
|
||||
:where [?i :account/numeric-code ?numeric-code]
|
||||
[?i :account/name ?n]
|
||||
(or [?i :account/applicability :account-applicability/global]
|
||||
[?i :account/applicability :account-applicability/optional]
|
||||
[?i :account/applicability :account-applicability/customized])]
|
||||
(dc/db conn)
|
||||
num
|
||||
allowance
|
||||
search-pattern)
|
||||
(sequence xform))
|
||||
(->> (search- id q client-id)
|
||||
(sequence
|
||||
(comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))]))
|
||||
xform))))
|
||||
[]))})))
|
||||
|
||||
(def account-search (wrap-json-response (wrap-schema-enforce account-search
|
||||
:query-schema [:map
|
||||
|
||||
@@ -64,7 +64,26 @@
|
||||
:class "hot-filter"
|
||||
:value (:code (:parsed-query-params request))
|
||||
:placeholder "11101"
|
||||
:size :small}))]])
|
||||
:size :small}))
|
||||
(com/field {:label "Type"}
|
||||
(com/radio-card {:size :small
|
||||
:name "type"
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "dividend"
|
||||
:content "Dividend"}
|
||||
{:value "asset"
|
||||
:content "Asset"}
|
||||
{:value "equity"
|
||||
:content "Equity"}
|
||||
{:value "liability"
|
||||
:content "Liability"}
|
||||
{:value "expense"
|
||||
:content "Expense"}
|
||||
{:value "revenue"
|
||||
:content "Revenue"}
|
||||
{:value "none"
|
||||
:content "None"}]}))]])
|
||||
|
||||
(def default-read '[:db/id
|
||||
:account/code
|
||||
@@ -82,9 +101,9 @@
|
||||
(defn fetch-ids [db request]
|
||||
(let [query-params (:parsed-query-params request)
|
||||
query (cond-> {:query {:find []
|
||||
:in '[$ ]
|
||||
:in '[$]
|
||||
:where '[]}
|
||||
:args [db ]}
|
||||
:args [db]}
|
||||
(:sort query-params) (add-sorter-fields {"name" ['[?e :account/name ?n]
|
||||
'[(clojure.string/upper-case ?n) ?sort-name]]
|
||||
"code" ['[(get-else $ ?e :account/numeric-code 0) ?sort-code]]
|
||||
@@ -96,17 +115,24 @@
|
||||
(merge-query {:query {:find []
|
||||
:in ['?ns]
|
||||
:where ['[?e :account/name ?an]
|
||||
'[(clojure.string/upper-case ?an) ?upper-an]
|
||||
'[(clojure.string/includes? ?upper-an ?ns)]]}
|
||||
'[(clojure.string/upper-case ?an) ?upper-an]
|
||||
'[(clojure.string/includes? ?upper-an ?ns)]]}
|
||||
:args [(str/upper-case (:name query-params))]})
|
||||
|
||||
|
||||
(some->> query-params :code)
|
||||
(merge-query {:query {:find []
|
||||
:in ['?nc]
|
||||
:where ['[?e :account/numeric-code ?nc]
|
||||
]}
|
||||
:where ['[?e :account/numeric-code ?nc]]}
|
||||
:args [(:code query-params)]})
|
||||
|
||||
|
||||
(some->> query-params :type)
|
||||
(merge-query {:query {:find []
|
||||
:in ['?rir]
|
||||
:where ['[?e :account/type ?r]
|
||||
'[?r :db/ident ?ri]
|
||||
'[(name ?ri) ?rir] ]}
|
||||
:args [(some->> query-params :type)]})
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]
|
||||
:where ['[?e :account/numeric-code ?un]
|
||||
|
||||
@@ -497,7 +497,9 @@
|
||||
:value (fc/field-value)
|
||||
:options [["new-square" "New Square+Ezcater (no effect)"]
|
||||
["manually-pay-cintas" "Manually Pay Cintas"]
|
||||
["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]]})))
|
||||
["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]
|
||||
["import-custom-amount" "Import Custom Amount Line Items from Square"]
|
||||
["code-sysco-items" "Code individual sysco line items"]]})))
|
||||
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))
|
||||
@@ -1282,11 +1284,10 @@
|
||||
[:td (fc/with-field :square-location/client-location
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)}))]]))]]]])
|
||||
(defn refresh-square-locations [request]
|
||||
#_(alog/peek (keys (:query-params request)))
|
||||
|
||||
(defn refresh-square-locations [request]
|
||||
(let [locations @(de/timeout!
|
||||
(de/chain (square/client-locations {:client/square-auth-token (get-in request [:query-params "step-params[client/square-auth-token]"])})
|
||||
(de/chain (square/client-locations {:client/square-auth-token (get-in request [:query-params (keyword "step-params[client/square-auth-token]")])})
|
||||
(fn [client-locations]
|
||||
(into []
|
||||
(for [square-location client-locations]
|
||||
@@ -1324,19 +1325,23 @@
|
||||
:body (mm/default-step-body
|
||||
{}
|
||||
[:div
|
||||
(fc/with-field :client/square-auth-token
|
||||
(com/validated-field
|
||||
{:errors (fc/field-errors)
|
||||
:label "Square Auth Token"}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/refresh-square-locations)
|
||||
:hx-trigger "keyup changed delay:1s queue:none"
|
||||
:hx-indicator "#square-locations"
|
||||
:hx-target "#square-locations"
|
||||
:placeholder "Token from square"
|
||||
:class "w-64"
|
||||
:value (fc/field-value)})))
|
||||
[:div.flex.gap-2.items-center
|
||||
(fc/with-field :client/square-auth-token
|
||||
(com/validated-field
|
||||
{:errors (fc/field-errors)
|
||||
:label "Square Auth Token"}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:id "square-token"
|
||||
:error? (fc/error?)
|
||||
:placeholder "Token from square"
|
||||
:class "w-64"
|
||||
:value (fc/field-value)})))
|
||||
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/refresh-square-locations)
|
||||
:hx-include "#square-token"
|
||||
:hx-trigger "click"
|
||||
:hx-indicator "#square-locations"
|
||||
:hx-target "#square-locations" }
|
||||
"Refresh")]
|
||||
|
||||
(fc/with-field :client/square-locations
|
||||
(square-location-table))])
|
||||
|
||||
@@ -2,20 +2,31 @@
|
||||
(:require [auto-ap.datomic
|
||||
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
|
||||
query2]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.routes.admin.sales-summaries :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.components.multi-modal :as mm]
|
||||
[auto-ap.ssr.form-cursor :as fc]
|
||||
[auto-ap.ssr.grid-page-helper :as helper]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers]]
|
||||
:refer [apply-middleware-to-all-handlers entity-id html-response
|
||||
money strip temp-id wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as c]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
[iol-ion.query :refer [dollars=]]))
|
||||
[hiccup.util :as hu]
|
||||
[iol-ion.query :refer [dollars=]]
|
||||
[malli.core :as mc]
|
||||
[malli.util :as mut]))
|
||||
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
@@ -44,8 +55,18 @@
|
||||
:size :small}))]])
|
||||
|
||||
(def default-read '[:db/id
|
||||
*
|
||||
[:sales-summary/date :xform clj-time.coerce/from-date]
|
||||
*]) ;; TODO
|
||||
{:sales-summary/client [:client/code :client/name :db/id]}
|
||||
{:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident]
|
||||
} ;; TODO clientize
|
||||
:ledger-mapped/account
|
||||
:ledger-mapped/amount
|
||||
:sales-summary-item/category
|
||||
:sales-summary-item/sort-order
|
||||
:db/id
|
||||
:sales-summary-item/manual?]
|
||||
} ]) ;; TODO
|
||||
|
||||
(defn fetch-ids [db request]
|
||||
(let [query-params (:parsed-query-params request)
|
||||
@@ -95,7 +116,7 @@
|
||||
[(->> (hydrate-results ids-to-retrieve db request))
|
||||
matching-count]))
|
||||
|
||||
(defn get-debits [ss]
|
||||
#_(defn get-debits [ss]
|
||||
{:card-payments (+ (:sales-summary/total-card-payments ss 0.0)
|
||||
(:sales-summary/total-card-fees ss 0.0)
|
||||
(- (:sales-summary/total-card-refunds ss 0.0)))
|
||||
@@ -103,8 +124,8 @@
|
||||
(:sales-summary/total-food-app-fees ss 0.0)
|
||||
(- (:sales-summary/total-food-app-refunds ss 0.0)))
|
||||
:gift-card-payments (+ (:sales-summary/total-gift-card-payments ss 0.0)
|
||||
(:sales-summary/total-gift-card-fees ss 0.0)
|
||||
(- (:sales-summary/total-gift-card-refunds ss 0.0)))
|
||||
(:sales-summary/total-gift-card-fees ss 0.0)
|
||||
(- (:sales-summary/total-gift-card-refunds ss 0.0)))
|
||||
#_#_:refunds (+ (:sales-summary/total-food-app-refunds ss 0.0)
|
||||
(:sales-summary/total-card-refunds ss 0.0)
|
||||
(:sales-summary/total-cash-refunds ss 0.0))
|
||||
@@ -112,8 +133,25 @@
|
||||
:fees (- (:sales-summary/total-card-fees ss 0.0))
|
||||
:cash-payments (+ (:sales-summary/total-cash-payments ss 0.0)
|
||||
(- (:sales-summary/total-cash-refunds ss 0.0)))
|
||||
:total-unknown-processor-payments (:sales-summary/total-unknown-processor-payments ss 0.0)
|
||||
:discounts (+ (:sales-summary/discount ss 0.0))
|
||||
:returns (+ (:sales-summary/total-returns ss 0.0))})
|
||||
(defn sort-items [ss]
|
||||
(sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss))
|
||||
|
||||
|
||||
|
||||
(defn total-debits [items]
|
||||
(->> items
|
||||
(filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)))
|
||||
(map #(:ledger-mapped/amount % 0.0))
|
||||
(reduce + 0.0)))
|
||||
|
||||
(defn total-credits [items]
|
||||
(->> items
|
||||
(filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)))
|
||||
(map #(:ledger-mapped/amount % 0.0))
|
||||
(reduce + 0.0)))
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
@@ -122,7 +160,10 @@
|
||||
:fetch-page fetch-page
|
||||
:page-specific-nav filters
|
||||
:row-buttons (fn [_ entity]
|
||||
[])
|
||||
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/edit-wizard
|
||||
:db/id (:db/id entity))}
|
||||
svg/pencil)])
|
||||
:oob-render
|
||||
(fn [request]
|
||||
[#_(assoc-in (date-range-field {:value {:start (:start-date (:parsed-query-params request))
|
||||
@@ -140,86 +181,53 @@
|
||||
:title "Sales Summaries"
|
||||
:entity-name "Daily Summary"
|
||||
:route ::route/table
|
||||
:headers [{:key "date"
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(= (count (:clients args)) 1))
|
||||
:render #(-> % :sales-summary/client :client/code)}
|
||||
|
||||
|
||||
{:key "date"
|
||||
:name "Date"
|
||||
:sort-key "date"
|
||||
:render #(some-> % :sales-summary/date (atime/unparse-local atime/normal-date))}
|
||||
{:key "credits"
|
||||
:name "credits"
|
||||
:sort-key "credits"
|
||||
:render (fn [ss]
|
||||
(let [total-debits (reduce + 0.0 (vals (get-debits ss)))
|
||||
total-credits (+ (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss)))
|
||||
|
||||
(reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss))))
|
||||
(reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss))))
|
||||
|
||||
(:sales-summary/total-tax ss 0.0)
|
||||
(:sales-summary/total-tip ss 0.0))]
|
||||
[:ul
|
||||
(for [[n x] (group-by :sales-summary-item/category (:sales-summary/sales-items ss))]
|
||||
[:li n ": " (format "$%,.2f" (- (+ (reduce + 0.0 (map :sales-summary-item/total x))
|
||||
(reduce + 0.0 (map :sales-summary-item/discount x)))
|
||||
(reduce + 0.0 (map :sales-summary-item/tax x))))])
|
||||
[:li "Sales subtotal: " (format "$%,.2f" (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss)))
|
||||
(reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss))))
|
||||
|
||||
(reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))))]
|
||||
[:li "Tax: " (format "$%,.2f" (:sales-summary/total-tax ss))]
|
||||
[:li "Tips: " (format "$%,.2f" (:sales-summary/total-tip ss))]
|
||||
[:li (com/pill {:color (if (dollars= total-debits total-credits)
|
||||
:primary
|
||||
:red)} "Total: " (format "$%,.2f" total-credits))]])
|
||||
|
||||
#_(count))}
|
||||
|
||||
{:key "debits"
|
||||
:name "debits"
|
||||
:sort-key "debits"
|
||||
:render (fn [ss]
|
||||
(let [{:keys [card-payments food-app-payments
|
||||
cash-payments discounts fees
|
||||
gift-card-payments
|
||||
returns refunds] :as debits} (get-debits ss)
|
||||
|
||||
total-debits (reduce + 0.0 (vals debits))
|
||||
total-credits (+ (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss)))
|
||||
|
||||
(reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss))))
|
||||
(reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss))))
|
||||
|
||||
(:sales-summary/total-tax ss 0.0)
|
||||
(:sales-summary/total-tip ss 0.0))]
|
||||
(let [total-debits (total-debits (:sales-summary/items ss))
|
||||
total-credits (total-credits (:sales-summary/items ss))]
|
||||
[:ul
|
||||
[:li "Card Payments: "
|
||||
(format "$%,.2f" card-payments)]
|
||||
|
||||
[:li "Food App Payments: "
|
||||
(format "$%,.2f" food-app-payments)]
|
||||
[:li "Gift Card Payments"
|
||||
(format "$%,.2f" gift-card-payments)]
|
||||
[:li "Cash Payments: "
|
||||
|
||||
(format "$%,.2f" cash-payments)]
|
||||
[:li "Discounts: "
|
||||
|
||||
(format "$%,.2f" discounts)]
|
||||
|
||||
[:li "Fees: "
|
||||
(format "$%,.2f" fees)]
|
||||
[:li "Returns: "
|
||||
(format "$%,.2f" returns)]
|
||||
#_[:li "Refunds: "
|
||||
(format "$%,.2f" refunds)]
|
||||
|
||||
|
||||
|
||||
|
||||
(for [si (sort-items (:sales-summary/items ss))
|
||||
:when (= :ledger-side/debit (:ledger-mapped/ledger-side si))]
|
||||
[:li (:sales-summary-item/category si) ": " (format "$%,.2f" (:ledger-mapped/amount si))
|
||||
(when-not (:ledger-mapped/account si)
|
||||
[:span.pl-4 (com/pill {:color :red}
|
||||
"missing account")])]
|
||||
)
|
||||
[:li (com/pill {:color (if (dollars= total-debits total-credits)
|
||||
:primary
|
||||
:red)} "Total: " (format "$%,.2f" total-debits))]])
|
||||
|
||||
#_(count))}]}))
|
||||
:red)} "Total: " (format "$%,.2f" total-debits))]]))}
|
||||
{:key "credits"
|
||||
:name "credits"
|
||||
:sort-key "credits"
|
||||
:render (fn [ss]
|
||||
(let [total-debits (total-debits (:sales-summary/items ss))
|
||||
total-credits (total-credits (:sales-summary/items ss))]
|
||||
[:ul
|
||||
(for [si (sort-items (:sales-summary/items ss))
|
||||
:when (= :ledger-side/credit (:ledger-mapped/ledger-side si))]
|
||||
[:li (:sales-summary-item/category si) ": " (format "$%,.2f" (:ledger-mapped/amount si))
|
||||
(when-not (:ledger-mapped/account si)
|
||||
[:span.pl-4 (com/pill {:color :red}
|
||||
"missing account")])])
|
||||
[:li (com/pill {:color (if (dollars= total-debits total-credits)
|
||||
:primary
|
||||
:red)} "Total: " (format "$%,.2f" total-credits))]]))}]}))
|
||||
|
||||
;; TODO schema cleanup
|
||||
;; Decide on what should be calculated as generating ledger entries, and what should be calculated
|
||||
@@ -231,11 +239,302 @@
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
|
||||
(def edit-schema
|
||||
[:map
|
||||
[:db/id entity-id]
|
||||
[:sales-summary/client [:map [:db/id entity-id]]]
|
||||
[:sales-summary/items
|
||||
[:vector {:coerce? true}
|
||||
[:and
|
||||
[:map
|
||||
[:db/id [:or entity-id temp-id]]
|
||||
[:sales-summary-item/category [:string {:decode/string strip}]]
|
||||
[:sales-summary-item/manual? {:default false :decode/arbitrary (fn [x] (cond
|
||||
(boolean? x)
|
||||
x
|
||||
(nil? x)
|
||||
false
|
||||
(str/blank? x)
|
||||
false
|
||||
:else
|
||||
true))} :boolean]
|
||||
[:ledger-mapped/account entity-id]
|
||||
[:credit {:optional true} [:maybe money]]
|
||||
[:debit {:optional true} [:maybe money]]]
|
||||
[:fn {:error/message "Must choose one of credit/debit"
|
||||
:error/path [:credit]}
|
||||
(fn [x]
|
||||
(not (and (:credit x)
|
||||
(:debit x))))]]]] ])
|
||||
|
||||
|
||||
(defn summary-total-row* [request]
|
||||
(let [total-credits (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:sales-summary/items
|
||||
(total-credits))
|
||||
total-debits (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:sales-summary/items
|
||||
(total-debits))]
|
||||
|
||||
(com/data-grid-row {:id "total-row"
|
||||
:hx-trigger "change from:closest form target:.amount-field"
|
||||
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total)
|
||||
:hx-target "this"
|
||||
:hx-swap "innerHTML"}
|
||||
(com/data-grid-cell {})
|
||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
|
||||
(com/data-grid-cell {:class "text-right"}
|
||||
(format "$%,.2f" total-debits))
|
||||
(com/data-grid-cell {:class "text-right"}
|
||||
(format "$%,.2f" total-credits)))))
|
||||
|
||||
(defn unbalanced-row* [request]
|
||||
(let [total-credits (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:sales-summary/items
|
||||
(total-credits))
|
||||
total-debits (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:sales-summary/items
|
||||
(total-debits))]
|
||||
|
||||
(com/data-grid-row {:id "total-row"
|
||||
:hx-trigger "change from:closest form target:.amount-field"
|
||||
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total)
|
||||
:hx-target "this"
|
||||
:hx-swap "innerHTML"}
|
||||
(com/data-grid-cell {})
|
||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "UNBALANCED"])
|
||||
(com/data-grid-cell {:class "text-right"}
|
||||
(when (and
|
||||
(not (dollars= total-credits total-debits))
|
||||
(> total-debits total-credits))
|
||||
(format "$%,.2f" (- total-debits total-credits))))
|
||||
(com/data-grid-cell {:class "text-right"}
|
||||
(when
|
||||
(and (not (dollars= total-credits total-debits))
|
||||
(> total-credits total-debits))
|
||||
(format "$%,.2f" (- total-credits total-debits)))))))
|
||||
|
||||
(defn- account-typeahead*
|
||||
[{:keys [name value client-id]}]
|
||||
[:div.flex.flex-col
|
||||
(com/typeahead {:name name
|
||||
:placeholder "Search..."
|
||||
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||
{:client-id client-id
|
||||
:purpose "invoice"})
|
||||
:value value
|
||||
:content-fn (fn [value]
|
||||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
||||
client-id)))})])
|
||||
|
||||
(defn sales-summary-item-row* [{:keys [value client-id]}]
|
||||
(let [manual? (fc/field-value (:sales-summary-item/manual? value))]
|
||||
(com/data-grid-row (cond-> {:x-ref "p"
|
||||
:x-data (hx/json {})}
|
||||
(fc/field-value (:new? value)) (hx/htmx-transition-appear ))
|
||||
(fc/with-field :db/id
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)}))
|
||||
(when manual?
|
||||
(fc/with-field :sales-summary-item/manual?
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value true})))
|
||||
(com/data-grid-cell {}
|
||||
(fc/with-field :sales-summary-item/category
|
||||
(if manual?
|
||||
(com/validated-field {:errors (fc/field-errors)}
|
||||
(com/text-input {:placeholder "Category/Explanation"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)}))
|
||||
|
||||
(list
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)})
|
||||
(fc/field-value (:sales-summary-item/category value))))))
|
||||
(com/data-grid-cell {}
|
||||
(fc/with-field :ledger-mapped/account
|
||||
(com/validated-field {:errors (fc/field-errors)}
|
||||
(account-typeahead* {:value (fc/field-value)
|
||||
:client-id client-id
|
||||
:name (fc/field-name)}))))
|
||||
(com/data-grid-cell {:class "text-right"}
|
||||
|
||||
(if manual?
|
||||
(fc/with-field :debit
|
||||
(com/validated-field {:errors (fc/field-errors)}
|
||||
(com/money-input {:class "w-24"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)})))
|
||||
(when (= (fc/field-value (:ledger-mapped/ledger-side value))
|
||||
:ledger-side/debit)
|
||||
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value))))))
|
||||
(com/data-grid-cell {:class "text-right"}
|
||||
|
||||
(if manual?
|
||||
(fc/with-field :credit
|
||||
(com/validated-field {:errors (fc/field-errors)}
|
||||
(com/money-input {:class "w-24"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)})))
|
||||
(when (= (fc/field-value (:ledger-mapped/ledger-side value))
|
||||
:ledger-side/credit)
|
||||
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value))))))
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(when manual?
|
||||
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))))
|
||||
|
||||
(defrecord MainStep [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
"Main")
|
||||
(step-key [_]
|
||||
:main)
|
||||
|
||||
(edit-path [_ _]
|
||||
[])
|
||||
|
||||
(step-schema [_]
|
||||
(mut/select-keys (mm/form-schema linear-wizard) #{:db/id :sales-summary/items}))
|
||||
|
||||
(render-step
|
||||
[this {:keys [multi-form-state] :as request}]
|
||||
(mm/default-render-step
|
||||
linear-wizard this
|
||||
:head [:div.p-2 "New invoice"]
|
||||
:body (mm/default-step-body
|
||||
{}
|
||||
[:div
|
||||
(fc/with-field :db/id
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)}))
|
||||
(com/data-grid {:headers
|
||||
[(com/data-grid-header {} "Category")
|
||||
(com/data-grid-header {} "Account")
|
||||
(com/data-grid-header {} "Debits")
|
||||
(com/data-grid-header {} "Credits")
|
||||
(com/data-grid-header {} "")]}
|
||||
(fc/with-field :sales-summary/items
|
||||
(list
|
||||
(fc/cursor-map #(sales-summary-item-row* {:value %
|
||||
:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) }))
|
||||
;; TODO
|
||||
(com/data-grid-new-row {:colspan 5
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
|
||||
:row-offset 0
|
||||
:index (count (fc/field-value))
|
||||
:tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} ;; TODO
|
||||
"New Summary Item")))
|
||||
(summary-total-row* request)
|
||||
(unbalanced-row* request)) ])
|
||||
|
||||
:footer
|
||||
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate)
|
||||
:validation-route ::route/edit-wizard-navigate
|
||||
:width-height-class "lg:w-[850px] lg:h-[900px]")))
|
||||
|
||||
(defn attach-ledger [i]
|
||||
(cond-> i
|
||||
(:credit i) (assoc :ledger-mapped/ledger-side :ledger-side/credit
|
||||
:ledger-mapped/amount (:credit i))
|
||||
(:debit i) (assoc :ledger-mapped/ledger-side :ledger-side/debit
|
||||
:ledger-mapped/amount (:debit i))
|
||||
true (dissoc :credit :debit)
|
||||
true (assoc :sales-summary-item/manual? true)))
|
||||
|
||||
(defrecord EditWizard [_ current-step]
|
||||
mm/LinearModalWizard
|
||||
(hydrate-from-request
|
||||
[this request]
|
||||
this)
|
||||
(navigate [this step-key]
|
||||
(assoc this :current-step step-key))
|
||||
(get-current-step
|
||||
[this]
|
||||
(mm/get-step this :main))
|
||||
(render-wizard [this {:keys [multi-form-state] :as request}]
|
||||
(mm/default-render-wizard
|
||||
this request
|
||||
:form-params
|
||||
(-> mm/default-form-props
|
||||
(assoc :hx-put
|
||||
(str (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-submit))))
|
||||
:render-timeline? false))
|
||||
(steps [_]
|
||||
[:main])
|
||||
(get-step [this step-key]
|
||||
(let [step-key-result (mc/parse mm/step-key-schema step-key)
|
||||
[step-key-type step-key] step-key-result]
|
||||
(->MainStep this)))
|
||||
(form-schema [_]
|
||||
edit-schema)
|
||||
(submit [this {:keys [multi-form-state request-method identity] :as request}]
|
||||
(let [result (:snapshot multi-form-state )
|
||||
transaction [:upsert-entity {:db/id (:db/id result)
|
||||
:sales-summary/items (map
|
||||
(fn [i]
|
||||
(if (:sales-summary-item/manual? i)
|
||||
(attach-ledger i)
|
||||
{:db/id (:db/id i)
|
||||
:ledger-mapped/account (:ledger-mapped/account i)
|
||||
}))
|
||||
(:sales-summary/items result))}]]
|
||||
(clojure.pprint/pprint (:sales-summary/items result))
|
||||
@(dc/transact conn [ transaction])
|
||||
(html-response
|
||||
(row* identity (dc/pull (dc/db conn) default-read (:db/id result))
|
||||
{:flash? true
|
||||
:request request})
|
||||
:headers (cond-> {"hx-trigger" "modalclose"
|
||||
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id result))
|
||||
"hx-reswap" "outerHTML"})))))
|
||||
|
||||
(def edit-wizard (->EditWizard nil nil))
|
||||
|
||||
(defn initial-edit-wizard-state [request]
|
||||
(let [entity (dc/pull (dc/db conn) default-read (:db/id (:route-params request)))
|
||||
entity (select-keys entity (mut/keys edit-schema))
|
||||
entity (update entity :sales-summary/items (comp #(map (fn [x]
|
||||
(if (= :ledger-side/debit (:ledger-mapped/ledger-side x))
|
||||
(assoc x :debit (:ledger-mapped/amount x))
|
||||
(assoc x :credit (:ledger-mapped/amount x))))
|
||||
%) sort-items))]
|
||||
|
||||
(mm/->MultiStepFormState entity [] entity)))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{::route/page (helper/page-route grid-page)
|
||||
::route/table (helper/table-route grid-page)})
|
||||
::route/table (helper/table-route grid-page)
|
||||
::route/edit-wizard (-> mm/open-wizard-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-init-multi-form-state initial-edit-wizard-state)
|
||||
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
||||
::route/edit-wizard-navigate (-> mm/next-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
|
||||
(fn render [cursor request]
|
||||
(sales-summary-item-row*
|
||||
{:value cursor
|
||||
:client-id (:client-id (:query-params request)) }))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :sales-summary-item/manual? true)))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
::route/edit-wizard-submit (-> mm/submit-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-admin)
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
request
|
||||
(com/page {:nav com/company-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:request request
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
(ns auto-ap.ssr.company.reports
|
||||
(:require
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields
|
||||
apply-pagination
|
||||
apply-sort-3
|
||||
conn
|
||||
merge-query
|
||||
pull-many
|
||||
query2]]
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.grid-page-helper :as helper]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils :refer [html-response]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clojure.set :as set]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]))
|
||||
(:require [amazonica.aws.s3 :as s3]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query
|
||||
pull-many query2]]
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]]
|
||||
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated
|
||||
wrap-secure]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.company.reports.expense :as company-expense-report]
|
||||
[auto-ap.ssr.company.reports.reconciliation :as company-reconciliation-report]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.grid-page-helper :as helper]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
|
||||
html-response]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clojure.set :as set]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]))
|
||||
|
||||
(def default-read '[:db/id :report/client [:report/created :xform clj-time.coerce/from-date] :report/url :report/name :report/creator])
|
||||
|
||||
@@ -132,3 +131,17 @@
|
||||
{:flash? true
|
||||
:delete-after-settle? true}))))
|
||||
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
(into
|
||||
{:company-reports page
|
||||
:company-reports-table table
|
||||
:company-reports-delete delete-report}
|
||||
company-expense-report/key->handler)
|
||||
(into company-reconciliation-report/key->handler))
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-secure)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
295
src/clj/auto_ap/ssr/company/reports/expense.clj
Normal file
295
src/clj/auto_ap/ssr/company/reports/expense.clj
Normal file
@@ -0,0 +1,295 @@
|
||||
(ns auto-ap.ssr.company.reports.expense
|
||||
(:require [auto-ap.datomic :refer [conn merge-query]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.form-cursor :as fc]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
|
||||
clj-date-schema html-response
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[datomic.api :as dc]
|
||||
[hiccup2.core :as hiccup]))
|
||||
|
||||
(defn lookup-breakdown-data [request]
|
||||
(let [query (cond-> {:query '{:find [?cn ?user-date (sum ?amt)]
|
||||
:with [?e]
|
||||
:in [$ [?clients ?start ?end]]
|
||||
:where
|
||||
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
(not [?e :invoice/status :invoice-status/voided])
|
||||
[?e :invoice/date ?d]
|
||||
[?e :invoice/client ?c]
|
||||
[?e :invoice/expense-accounts ?iea]
|
||||
[?iea :invoice-expense-account/amount ?amt]
|
||||
[?c :client/name ?cn]
|
||||
[(clj-time.coerce/to-date-time ?d) ?user-date]]}
|
||||
:args
|
||||
[(dc/db conn)
|
||||
[(extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(some-> (time/plus (time/now) (time/days -65)) coerce/to-date)
|
||||
(some-> (time/now) coerce/to-date)]]}
|
||||
|
||||
(:vendor-id (:query-params request))
|
||||
(merge-query {:query '{:in [?v]
|
||||
:where [ [?e :invoice/vendor ?v]]}
|
||||
:args [ (:db/id (:vendor-id (:query-params request)))]})
|
||||
|
||||
(:account-id (:query-params request))
|
||||
(merge-query {:query '{:in [?a]
|
||||
:where [ [?iea :invoice-expense-account/account ?a]]}
|
||||
:args [ (:db/id (:account-id (:query-params request)))]}))]
|
||||
|
||||
(dc/query query)))
|
||||
|
||||
(defn lookup-invoice-total-data [request]
|
||||
(let [start (:start-date (:query-params request) (time/plus (time/now) (time/days -30)))
|
||||
end (:end-date (:query-params request) (time/now))
|
||||
query (cond-> {:query '{:find [?cn ?vn (sum ?t)]
|
||||
:with [ ?e]
|
||||
:in [$ [?clients ?start ?end]]
|
||||
:where
|
||||
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
(not [?e :invoice/status :invoice-status/voided])
|
||||
[?e :invoice/client ?c]
|
||||
[?e :invoice/total ?t]
|
||||
[?e :invoice/vendor ?v]
|
||||
[?v :vendor/name ?vn]
|
||||
[?c :client/name ?cn]
|
||||
]}
|
||||
:args
|
||||
[(dc/db conn)
|
||||
[(extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(some-> start coerce/to-date)
|
||||
(some-> end coerce/to-date)]]})]
|
||||
|
||||
(dc/query query)))
|
||||
|
||||
(defn week-seq
|
||||
([c] (week-seq c (atime/last-monday)))
|
||||
([c starting] (reverse (for [n (range c)
|
||||
:let [start (time/minus starting (time/weeks n))
|
||||
end (time/minus starting (time/weeks (dec n)))]]
|
||||
[(atime/as-local-time (coerce/to-date-time start)) (atime/as-local-time (coerce/to-date-time end))]))))
|
||||
|
||||
|
||||
(defn- best-week [d weeks]
|
||||
(reduce
|
||||
(fn [acc [start end]]
|
||||
(if (and (time/after? d start)
|
||||
(time/before? d end))
|
||||
(reduced [start end])
|
||||
nil))
|
||||
nil
|
||||
weeks))
|
||||
|
||||
|
||||
(defn expense-breakdown-card* [request]
|
||||
(com/card {:class "w-full h-full" :id "expense-breakdown-report"}
|
||||
[:div {:class "flex flex-col px-8 py-8 space-y-3 w-full h-full"}
|
||||
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card)
|
||||
:hx-trigger "change"
|
||||
:hx-target "#expense-breakdown-report"
|
||||
:hx-swap "outerHTML"}
|
||||
(fc/start-form
|
||||
(:query-params request)
|
||||
(:form-errors request)
|
||||
[:div.flex.justify-between
|
||||
[:h1.text-2xl.mb-3.font-bold "Expense breakdown report, last 8 weeks"]
|
||||
[:div.flex.gap-2
|
||||
(fc/with-field :vendor-id
|
||||
(com/validated-field {:label "Vendor"
|
||||
:errors (fc/field-errors)}
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||
:value (fc/field-value)
|
||||
:value-fn :db/id
|
||||
:content-fn :vendor/name})))
|
||||
(fc/with-field :account-id
|
||||
(com/validated-field {:label "Account"
|
||||
:errors (fc/field-errors)}
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||
:value (fc/field-value)
|
||||
:value-fn :db/id
|
||||
:content-fn :account/name})))]])]
|
||||
[:div.flex-grow
|
||||
(let [data (lookup-breakdown-data request)
|
||||
distinct-accounts (->> data
|
||||
(reduce
|
||||
(fn [acc [an _ amount]]
|
||||
(update acc an (fnil + 0.0) amount))
|
||||
{})
|
||||
(sort-by last)
|
||||
(reverse)
|
||||
(map first)
|
||||
(take 20))
|
||||
weeks (week-seq 8)
|
||||
x-axis (for [[start end] weeks]
|
||||
(str (iol-ion.query/excel-date (coerce/to-date start))
|
||||
" - "
|
||||
(iol-ion.query/excel-date (coerce/to-date end))))
|
||||
|
||||
lookup (->>
|
||||
(reduce
|
||||
(fn [acc [a d v]]
|
||||
(update-in acc [a (best-week d weeks)] (fnil + 0.0) v))
|
||||
{}
|
||||
data))
|
||||
series (for [ea distinct-accounts]
|
||||
(for [d weeks]
|
||||
(get-in lookup [ea d] 0)))]
|
||||
[:canvas {:x-data (hx/json {:chart nil
|
||||
:labels x-axis
|
||||
:datasets (map (fn [s a] {:label a
|
||||
:data s
|
||||
:borderWidth 1})
|
||||
series
|
||||
distinct-accounts)})
|
||||
:x-init
|
||||
"new Chart($el, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});"}])]]))
|
||||
|
||||
(defn vendor-invoice-total-card* [request]
|
||||
(com/content-card {:class "w-full" :id "invoice-totals-report"}
|
||||
[:div {:class "flex flex-col px-8 py-8 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Invoice totals by vendor"]
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-invoice-total-card )
|
||||
:hx-trigger "change"
|
||||
:hx-target "#invoice-totals-report"
|
||||
:hx-swap "outerHTML"}
|
||||
(fc/start-form
|
||||
(:query-params request)
|
||||
(:form-errors request)
|
||||
[:div.flex.gap-2
|
||||
(fc/with-field :start-date
|
||||
(com/validated-field {:label "Start"
|
||||
:errors (fc/field-errors)}
|
||||
[:div {:class "w-64"}
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))
|
||||
(fc/with-field :end-date
|
||||
(com/validated-field {:label "End"
|
||||
:errors (fc/field-errors)}
|
||||
[:div {:class "w-64"}
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))])]
|
||||
[:div {:class "overflow-scroll min-w-full max-h-[700px]"}
|
||||
(let [data (lookup-invoice-total-data request)
|
||||
companies (sort (set (map first data)))
|
||||
vendors (sort (set (map second data)))
|
||||
result (by (juxt first second) last data)
|
||||
]
|
||||
(com/data-grid
|
||||
{:headers (into
|
||||
[(com/data-grid-header {:class "sticky left-0 z-60 bg-gray-100"} "Vendor")]
|
||||
(for [company companies]
|
||||
(com/data-grid-header {} company)))
|
||||
:thead-params {:class "sticky top-0 z-50"}}
|
||||
(for [vendor vendors]
|
||||
(com/data-grid-row
|
||||
{}
|
||||
(com/data-grid-cell {:class "sticky left-0 z-0 bg-gray-100"}
|
||||
vendor)
|
||||
(for [company companies]
|
||||
(com/data-grid-cell
|
||||
{}
|
||||
(or (some->> (get result [company vendor])
|
||||
(format "$%,.2f" ))
|
||||
[:span.text-gray-200 "-"])))))))]]]))
|
||||
|
||||
(defn page [request]
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/company-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :company-expense-report)
|
||||
:hx-trigger "clientSelected from:body"
|
||||
:hx-select "#app-contents"
|
||||
:hx-swap "outerHTML swap:300ms"}}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :company)}
|
||||
"My Company"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :company-expense-report)}
|
||||
"Expense Report"])
|
||||
(expense-breakdown-card* request)
|
||||
(vendor-invoice-total-card* request))
|
||||
"My Company"))
|
||||
|
||||
(defn normalize-query-params [request]
|
||||
(-> request
|
||||
:query-params
|
||||
(update :vendor-id :db/id)
|
||||
(update :account-id :db/id)
|
||||
(update :start-date #(atime/unparse-local % atime/normal-date))
|
||||
(update :end-date #(atime/unparse-local % atime/normal-date))
|
||||
|
||||
url/map->query))
|
||||
|
||||
(defn expense-breakdown-card [request]
|
||||
(html-response
|
||||
(expense-breakdown-card* request)
|
||||
:headers {"hx-push-url" (str "?" (normalize-query-params request))}))
|
||||
(defn invoice-total-card [request]
|
||||
(html-response
|
||||
(vendor-invoice-total-card* request)
|
||||
:headers {"hx-push-url" (str "?" (normalize-query-params request))}))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{:company-expense-report page
|
||||
:company-expense-report-breakdown-card expense-breakdown-card
|
||||
:company-expense-report-invoice-total-card invoice-total-card}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-schema-enforce :query-schema
|
||||
[:map {:default {}}
|
||||
[:start-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:end-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:vendor-id {:optional true}
|
||||
[:maybe
|
||||
[:entity-map {:pull [:vendor/name :db/id]}]]]
|
||||
[:account-id {:optional true}
|
||||
[:maybe
|
||||
[:entity-map {:pull [:account/name :db/id]}]]]])))))
|
||||
201
src/clj/auto_ap/ssr/company/reports/reconciliation.clj
Normal file
201
src/clj/auto_ap/ssr/company/reports/reconciliation.clj
Normal file
@@ -0,0 +1,201 @@
|
||||
(ns auto-ap.ssr.company.reports.reconciliation
|
||||
(:require [auto-ap.datomic :refer [conn pull-attr]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.import.intuit :refer [get-intuit-bank-accounts
|
||||
intuits->transactions]]
|
||||
[auto-ap.intuit.core :refer [get-transactions]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.form-cursor :as fc]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
|
||||
clj-date-schema html-response
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clj-time.coerce :as coerce]
|
||||
[datomic.api :as dc]
|
||||
[auto-ap.ssr.svg :as svg]))
|
||||
|
||||
(defn report* [{:keys [request report]}]
|
||||
[:div #_{:class "overflow-scroll min-w-full max-h-[700px]"}
|
||||
(com/data-grid
|
||||
{:headers (into
|
||||
[(com/data-grid-header {} "Bank Account")
|
||||
(com/data-grid-header {} "Source count")
|
||||
(com/data-grid-header {} "Synced count")
|
||||
(com/data-grid-header {} "Approved transactions")
|
||||
(com/data-grid-header {} "Unapproved transactions")
|
||||
(com/data-grid-header {} "Requires feedback transactions")
|
||||
(com/data-grid-header {} "Missing transactions")])
|
||||
#_#_:thead-params {:class "sticky top-0 z-50"}}
|
||||
(for [row report]
|
||||
(let [matches? (= (:external-transaction-count row)
|
||||
(:integreat-transaction-count row))
|
||||
class (if matches? "bg-primary-200 text-primary-900"
|
||||
"bg-red-200 text-red-900")]
|
||||
(com/data-grid-row
|
||||
{}
|
||||
(com/data-grid-cell {:class class}
|
||||
(:bank-account/name row))
|
||||
(com/data-grid-cell {:class class}
|
||||
(:external-transaction-count row))
|
||||
(com/data-grid-cell {:class class}
|
||||
(:integreat-transaction-count row))
|
||||
(com/data-grid-cell {:class class}
|
||||
(:approved-count row))
|
||||
(com/data-grid-cell {:class class}
|
||||
(:unapproved-count row))
|
||||
(com/data-grid-cell {:class class}
|
||||
(:requires-feedback-count row))
|
||||
(com/data-grid-cell {:class class}
|
||||
(when (> (count (:missing-transactions row)) 0)
|
||||
[:div { :x-data (hx/json {:popper nil
|
||||
:hovering false})
|
||||
"x-init" "popper = Popper.createPopper($refs.hover_target, $refs.tooltip, {placement: 'bottom', strategy:'fixed', modifiers: [{name: 'preventOverflow'}, {name: 'offset', options: {offset: [0, 10]}}]});"}
|
||||
(com/button {"x-ref" "hover_target"
|
||||
"@click.prevent" "hovering=!hovering; $nextTick(() => popper.update())"}
|
||||
[:div.flex.gap-2.items-center
|
||||
(count (:missing-transactions row))
|
||||
[:div.w-4.h-4 svg/question]
|
||||
|
||||
])
|
||||
[:div (hx/alpine-appear {:x-ref "tooltip"
|
||||
:x-show "hovering"
|
||||
:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"})
|
||||
(com/data-grid {:headers [(com/data-grid-header {} "Date")
|
||||
(com/data-grid-header {} "Amount")]}
|
||||
(for [r (:missing-transactions row)]
|
||||
(com/data-grid-row {}
|
||||
(com/data-grid-cell {}
|
||||
(atime/unparse-local (coerce/to-date-time (:transaction/date r)) atime/normal-date))
|
||||
(com/data-grid-cell {}
|
||||
(format "$%,.2f" (:transaction/amount r)))))) ] ]))))))])
|
||||
|
||||
|
||||
(defn reconciliation-card* [{:keys [request report]}]
|
||||
(com/content-card {:class "w-full" :id "reconciliation-report"}
|
||||
[:div {:class "flex flex-col px-8 py-8 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Bank Reconciliation Report"]
|
||||
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-reconciliation-report-card)
|
||||
:hx-target "#reconciliation-report"
|
||||
:hx-swap "outerHTML"}
|
||||
(fc/start-form
|
||||
(:query-params request)
|
||||
(:form-errors request)
|
||||
[:div.flex.gap-2
|
||||
(fc/with-field :start-date
|
||||
(com/validated-field {:label "Start"
|
||||
:errors (fc/field-errors)}
|
||||
[:div {:class "w-64"}
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))
|
||||
(fc/with-field :end-date
|
||||
(com/validated-field {:label "End"
|
||||
:errors (fc/field-errors)}
|
||||
[:div {:class "w-64"}
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))
|
||||
(com/button {:color :primary :class "self-center w-24"} "Run")])]
|
||||
(if report
|
||||
(report* {:request request :report report})
|
||||
[:div "Please choose a time range to run the report"])
|
||||
]]))
|
||||
|
||||
(defn page [request]
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/company-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :company-reconciliation-report)
|
||||
:hx-trigger "clientSelected from:body"
|
||||
:hx-select "#app-contents"
|
||||
:hx-swap "outerHTML swap:300ms"}}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :company)}
|
||||
"My Company"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :company-reconciliation-report)}
|
||||
"Reconciliation Report"])
|
||||
(reconciliation-card* {:request request :report nil}))
|
||||
"My Company"))
|
||||
|
||||
(defn normalize-query-params [request]
|
||||
(-> request
|
||||
:query-params
|
||||
(update :vendor-id :db/id)
|
||||
(update :account-id :db/id)
|
||||
(update :start-date #(atime/unparse-local % atime/normal-date))
|
||||
(update :end-date #(atime/unparse-local % atime/normal-date))
|
||||
|
||||
url/map->query))
|
||||
|
||||
(defn get-report-data [start-date end-date client-ids]
|
||||
(let [client-codes (map first (dc/q '[:find ?cc :in $ [?c ...] :where [?c :client/code ?cc]] (dc/db conn ) client-ids))]
|
||||
(for [[ib ba c] (seq (apply get-intuit-bank-accounts (dc/db conn) client-codes))
|
||||
:let [raw-transactions (get-transactions (atime/unparse-local start-date atime/iso-date)
|
||||
(atime/unparse-local end-date atime/iso-date)
|
||||
ib)
|
||||
ideal-transactions (intuits->transactions raw-transactions ba c)
|
||||
|
||||
found-transactions (when (seq ideal-transactions)
|
||||
(into {} (dc/q '[:find ?si (count ?t)
|
||||
:in $ [?eid ...]
|
||||
:where
|
||||
[?t :transaction/id ?eid]
|
||||
[?t :transaction/approval-status ?s]
|
||||
[?s :db/ident ?si]]
|
||||
(dc/db conn)
|
||||
(map :transaction/id ideal-transactions))))
|
||||
|
||||
missing-transaction-ids (when (seq ideal-transactions)
|
||||
(->>
|
||||
(dc/q '[:find ?eid
|
||||
:in $ [?eid ...]
|
||||
:where (not [_ :transaction/id ?eid])]
|
||||
(dc/db conn)
|
||||
(map :transaction/id ideal-transactions))
|
||||
(map first)
|
||||
(into #{})))
|
||||
missing-transactions (filter (comp missing-transaction-ids :transaction/id) ideal-transactions)]]
|
||||
{:bank-account/name (pull-attr (dc/db conn) :bank-account/name ba)
|
||||
:external-transaction-count (count raw-transactions)
|
||||
:integreat-transaction-count (reduce + 0 (vals found-transactions))
|
||||
:approved-count (:transaction-approval-status/approved found-transactions 0)
|
||||
:unapproved-count (:transaction-approval-status/unapproved found-transactions 0)
|
||||
:requires-feedback-count (:transaction-approval-status/requires-feedback found-transactions 0)
|
||||
:missing-transactions missing-transactions})))
|
||||
|
||||
(defn card [{ {:keys [start-date end-date]} :query-params :as request}]
|
||||
(let [client-ids (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
report (get-report-data start-date end-date client-ids)]
|
||||
(html-response
|
||||
(reconciliation-card* {:request request
|
||||
:report report})
|
||||
:headers {"hx-push-url" (str "?" (normalize-query-params request))})))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{:company-reconciliation-report page
|
||||
:company-reconciliation-report-card card}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-schema-enforce :query-schema
|
||||
[:map {:default {}}
|
||||
[:start-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:end-date {:optional true}
|
||||
[:maybe clj-date-schema]] ])))))
|
||||
@@ -10,6 +10,7 @@
|
||||
[auto-ap.routes.outgoing-invoice :as oi-routes]
|
||||
[auto-ap.routes.payments :as payment-routes]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components.tags :as tags]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
@@ -45,14 +46,14 @@
|
||||
:class (fnil hh/add-class "") "space-y-1.5 max-h-0 transition transition-all overflow-hidden")
|
||||
true (assoc ":class" (format "selected == '%s' ? 'py-0.5' : 'py-0'" (:selector params))
|
||||
:x-ref "submenu"
|
||||
:style (cond-> {} (:active? params) (assoc "max-height" "400px"))
|
||||
":style" (format "selected == '%s' ? 'max-height: ' + $refs.submenu.scrollHeight + 'px' : ''" (:selector params))))
|
||||
:style (cond-> {} (:active? params) (assoc "max-height" "900px"))
|
||||
":style" (format "selected == '%s' ? 'max-height: ' + $el.scrollHeight + 'px' : ''" (:selector params))))
|
||||
(for [c children]
|
||||
[:li
|
||||
(update-in c [1 1 :class ] (fn [c]
|
||||
(hh/add-class (or c "") " flex items-center p-2 pl-11 w-full text-base font-normal rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")))])])
|
||||
|
||||
(defn left-aside- [{:keys [nav page-specific]} & children]
|
||||
(defn left-aside- [{:keys [nav page-specific]} & _]
|
||||
[:aside {:id "left-nav",
|
||||
:class "fixed top-0 left-0 pt-16 z-20 w-64 h-screen transition-transform",
|
||||
"x-transition:enter" "transition duration-500"
|
||||
@@ -66,12 +67,13 @@
|
||||
:x-show "leftNavShow"
|
||||
":aria-hidden" "leftNavShow ? 'false' : 'true'"}
|
||||
|
||||
;; TODO this causes a leftNavShow error when hitting back button. maybe amke a container
|
||||
[:template {:x-teleport "body"}
|
||||
|
||||
[:div.fixed.inset-0.lg:hidden {:x-show "leftNavShow" :x-transition:enter "transition duration-500" :x-transition:enter-start "opacity-0" :x-transition:enter-end "opacity-100"
|
||||
:x-transition:leave "transition duration-500" :x-transition:leave-start "opacity-100" :x-transition:leave-end "opacity-0"
|
||||
"@click.capture.prevent" "leftNavShow=false"}
|
||||
[:div.fixed.inset-0.bg-gray-800.z-10.opacity-70]]]
|
||||
[:div.fixed.inset-0.bg-gray-800.z-100.opacity-70]]]
|
||||
|
||||
[:div {:class "overflow-y-auto py-5 px-3 h-full bg-gray-50 border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700"}
|
||||
nav
|
||||
@@ -83,12 +85,12 @@
|
||||
|
||||
(defn main-aside-nav- [request]
|
||||
(let [selected (cond
|
||||
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page} (:matched-route request))
|
||||
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page :invoice-glimpse :invoice-glimpse-textract-invoice} (:matched-route request))
|
||||
"invoices"
|
||||
|
||||
(#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request))
|
||||
"sales"
|
||||
(#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page } (:matched-route request))
|
||||
(#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request))
|
||||
"payments"
|
||||
:else
|
||||
nil)]
|
||||
@@ -102,7 +104,7 @@
|
||||
|
||||
(when (can? (:identity request)
|
||||
{:subject :invoice-page})
|
||||
(list
|
||||
(list
|
||||
(menu-button- {"@click.prevent" "if (selected == 'invoices') {selected = null } else { selected = 'invoices'} "
|
||||
:icon svg/accounting-invoice-mail}
|
||||
"Invoices")
|
||||
@@ -135,13 +137,26 @@
|
||||
:hx-boost "true"}
|
||||
"Voided")
|
||||
|
||||
|
||||
(when (can? (:identity request)
|
||||
{:subject :invoice
|
||||
:activity :import})
|
||||
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/import-page)
|
||||
:active? (= ::invoice-route/import-page (:matched-route request))
|
||||
:hx-boost "true"} "Import"))
|
||||
:hx-boost "true"} "Import"))
|
||||
|
||||
|
||||
(when (can? (:identity request)
|
||||
{:subject :invoice
|
||||
:activity :import})
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse))
|
||||
:active? (= :invoice-glimpse (:matched-route request))
|
||||
:hx-boost "true"}
|
||||
[:div.flex.gap-2
|
||||
"Glimpse"
|
||||
(tags/pill- {:color :secondary} "Beta")]))
|
||||
|
||||
|
||||
(when (can? (:identity request)
|
||||
@@ -272,42 +287,62 @@
|
||||
:external-import-ledger)} "External Ledger Import")))))]))
|
||||
|
||||
|
||||
(defn company-aside-nav- [_]
|
||||
(defn company-aside-nav- [request]
|
||||
[:ul {:class "space-y-2" :hx-boost "true"}
|
||||
[:li
|
||||
(menu-button- {:icon svg/vendors
|
||||
:active? (= :company (:matched-route request))
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)
|
||||
:hx-boost true}
|
||||
"My Company")]
|
||||
|
||||
[:li
|
||||
(menu-button- {:icon svg/report
|
||||
:active? (= :company-reports (:matched-route request))
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-reports)
|
||||
:hx-boost true}
|
||||
"Reports")]
|
||||
[:li
|
||||
(menu-button- {:icon svg/report
|
||||
:active? (= :company-expense-report (:matched-route request))
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-expense-report)
|
||||
:hx-boost true}
|
||||
"Expense Report")]
|
||||
(when (can? (:identity request)
|
||||
{:subject :reconciliation-report})
|
||||
[:li
|
||||
(menu-button- {:icon svg/report
|
||||
:active? (= :company-reconciliation-report (:matched-route request))
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-reports)
|
||||
:company-reconciliation-report)
|
||||
:hx-boost true}
|
||||
"Reports")]
|
||||
"Bank Sync Report")])
|
||||
[:li
|
||||
(menu-button- {:icon svg/bank
|
||||
:active? (= :company-plaid (:matched-route request))
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid)
|
||||
:hx-boost true}
|
||||
"Plaid Link")]
|
||||
[:li
|
||||
(menu-button- {:icon svg/bank
|
||||
:active? (= :company-yodlee (:matched-route request))
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee)
|
||||
:hx-boost true}
|
||||
"Yodlee Link")]
|
||||
[:li
|
||||
(menu-button- {:icon svg/government-building
|
||||
:active? (= :company-1099 (:matched-route request))
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-1099)
|
||||
:hx-boost true}
|
||||
"1099 Vendor Info"
|
||||
)]])
|
||||
"1099 Vendor Info")]])
|
||||
|
||||
(defn admin-aside-nav- [{:keys [matched-route] :as request}]
|
||||
(defn admin-aside-nav- [{:keys [matched-route]}]
|
||||
[:ul {:class "space-y-2" :x-data (hx/json {:selected "nil"})}
|
||||
[:li
|
||||
(menu-button- {:icon svg/dashboard
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
(defn data-grid- [{:keys [headers thead-params id] :as params} & rest]
|
||||
[:table (merge {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400"}
|
||||
(dissoc params :headers :thead-params))
|
||||
[:thead (assoc thead-params :class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400")
|
||||
[:thead (update thead-params :class #(-> "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
|
||||
(hh/add-class (or % ""))))
|
||||
(into
|
||||
[:tr]
|
||||
headers)]
|
||||
@@ -137,7 +138,7 @@
|
||||
(a-button- (merge
|
||||
(dissoc params :index :colspan)
|
||||
{
|
||||
"@click" "$dispatch('newRow', {index: (newRowIndex++)})"
|
||||
"@click.prevent" "$dispatch('newRow', {index: (newRowIndex++)})"
|
||||
:color :secondary
|
||||
:hx-trigger "newRow"
|
||||
:hx-vals (hiccup/raw "js:{index: event.detail.index }")
|
||||
|
||||
@@ -175,10 +175,12 @@
|
||||
(update :class (fnil hh/add-class "") default-input-classes)
|
||||
(assoc :x-modelable "value")
|
||||
(assoc :type "text")
|
||||
(assoc "_" (hiccup/raw "init initDatepicker(me)"))
|
||||
(assoc "@change" "value = $event.target.value; console.log(value)")
|
||||
(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\")
|
||||
htmx:beforeCleanupElement: this.dp.destroy()"))
|
||||
(assoc :x-data (hx/json {:dp nil}) )
|
||||
(assoc :x-init " dp = initDatepicker($el);")
|
||||
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)" )
|
||||
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)" )
|
||||
(assoc "@change" "value = $event.target.value;")
|
||||
(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\") "))
|
||||
(update :class #(str % (use-size size) " w-full"))
|
||||
(dissoc :size))]])
|
||||
|
||||
|
||||
@@ -165,12 +165,13 @@
|
||||
:else
|
||||
[:div "No action possible."])]])
|
||||
|
||||
(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route]}]
|
||||
(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route width-height-class]}]
|
||||
(let [is-last? (= (step-key step) (last (steps linear-wizard)))]
|
||||
(com/modal-card-advanced
|
||||
{"@keydown.enter.prevent.stop" "if ($refs.next ) {$refs.next.click()}"
|
||||
:class (str
|
||||
"w-full h-full md:w-[750px] md:h-[600px]
|
||||
(or width-height-class " md:w-[750px] md:h-[600px] ")
|
||||
" w-full h-full
|
||||
group-[.forward]/transition:htmx-swapping:opacity-0
|
||||
group-[.forward]/transition:htmx-swapping:-translate-x-1/4
|
||||
group-[.forward]/transition:htmx-swapping:scale-75
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
(ns auto-ap.ssr.components.navbar
|
||||
(:require
|
||||
[auto-ap.graphql.utils :refer [is-admin?]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.company-dropdown :as cd]
|
||||
[auto-ap.ssr.components.buttons :refer [icon-button-]]
|
||||
[auto-ap.ssr.components.user-dropdown :as user-dropdown]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[bidi.bidi :as bidi]))
|
||||
(:require [auto-ap.graphql.utils :refer [is-admin? limited-clients]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.company-dropdown :as cd]
|
||||
[auto-ap.ssr.components.buttons :refer [icon-button-]]
|
||||
[auto-ap.ssr.components.user-dropdown :as user-dropdown]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[bidi.bidi :as bidi]))
|
||||
|
||||
(defn navbar- [{:keys [client-selection client identity clients dd-env]}]
|
||||
[:nav {:class "fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700"}
|
||||
@@ -39,7 +38,10 @@
|
||||
:hx-target "#modal-holder"
|
||||
:hx-swap "outerHTML"}
|
||||
svg/search)
|
||||
(cd/dropdown {:client-selection client-selection :client client :identity identity
|
||||
:clients clients})
|
||||
(let [limited-clients (limited-clients identity)]
|
||||
(when (or (nil? limited-clients)
|
||||
(> (count limited-clients) 1))
|
||||
(cd/dropdown {:client-selection client-selection :client client :identity identity
|
||||
:clients clients})))
|
||||
|
||||
(user-dropdown/dropdown {:identity identity})]]]])
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
on notification from body put event.detail.value into #notification-details then add .htmx-added to #notification-holder then remove .hidden from #notification-holder then wait 30ms then remove .htmx-added from #notification-holder
|
||||
on htmx:responseError put event.detail.xhr.response into #error-details then add .htmx-added to #error-holder then remove .hidden from #error-holder then wait 30ms then remove .htmx-added from #error-holder"
|
||||
)
|
||||
:x-data (hx/json {:leftNavShow true})}
|
||||
:x-data (hx/json {:leftNavShow true})
|
||||
}
|
||||
(navbar- {:client-selection client-selection
|
||||
:clients clients
|
||||
:client client
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
(ns auto-ap.ssr.core
|
||||
(:require
|
||||
[auto-ap.routes.ezcater-xls :as ezcater-xls]
|
||||
[auto-ap.routes.utils
|
||||
(:require [auto-ap.permissions :refer [wrap-must]]
|
||||
[auto-ap.routes.ezcater-xls :as ezcater-xls]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
[auto-ap.ssr.account :as account]
|
||||
[auto-ap.ssr.payments :as payments]
|
||||
[auto-ap.ssr.admin :as admin]
|
||||
[auto-ap.ssr.admin.accounts :as admin-accounts]
|
||||
[auto-ap.ssr.admin.background-jobs :as admin-jobs]
|
||||
[auto-ap.ssr.admin.excel-invoice :as admin-excel-invoices]
|
||||
[auto-ap.ssr.admin.history :as history]
|
||||
[auto-ap.ssr.admin.import-batch :as import-batch]
|
||||
[auto-ap.ssr.admin.transaction-rules :as admin-rules]
|
||||
[auto-ap.ssr.admin.vendors :as admin-vendors]
|
||||
[auto-ap.ssr.admin.clients :as admin-clients]
|
||||
[auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries]
|
||||
[auto-ap.ssr.auth :as auth]
|
||||
[auto-ap.ssr.indicators :as indicators]
|
||||
[auto-ap.ssr.company :as company]
|
||||
[auto-ap.ssr.company-dropdown :as company-dropdown]
|
||||
[auto-ap.ssr.company.company-1099 :as company-1099]
|
||||
[auto-ap.ssr.company.plaid :as company-plaid]
|
||||
[auto-ap.ssr.company.reports :as company-reports]
|
||||
[auto-ap.ssr.company.yodlee :as company-yodlee]
|
||||
[auto-ap.ssr.invoice.glimpse :as invoice-glimpse]
|
||||
[auto-ap.ssr.pos.cash-drawer-shifts :as pos-cash-drawer-shifts]
|
||||
[auto-ap.ssr.pos.expected-deposits :as pos-expected-deposits]
|
||||
[auto-ap.ssr.pos.refunds :as pos-refunds]
|
||||
[auto-ap.ssr.pos.sales-orders :as pos-sales]
|
||||
[auto-ap.ssr.pos.tenders :as pos-tenders]
|
||||
[auto-ap.ssr.invoices :as invoice]
|
||||
[auto-ap.ssr.outgoing-invoice.new :as oin]
|
||||
[auto-ap.ssr.search :as search]
|
||||
[auto-ap.ssr.transaction.insights :as insights]
|
||||
[auto-ap.ssr.users :as users]
|
||||
[auto-ap.ssr.vendor :as vendors]
|
||||
[ring.middleware.json :refer [wrap-json-response]]))
|
||||
[auto-ap.ssr.account :as account]
|
||||
[auto-ap.ssr.admin :as admin]
|
||||
[auto-ap.ssr.admin.accounts :as admin-accounts]
|
||||
[auto-ap.ssr.admin.background-jobs :as admin-jobs]
|
||||
[auto-ap.ssr.admin.clients :as admin-clients]
|
||||
[auto-ap.ssr.admin.excel-invoice :as admin-excel-invoices]
|
||||
[auto-ap.ssr.admin.history :as history]
|
||||
[auto-ap.ssr.admin.import-batch :as import-batch]
|
||||
[auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries]
|
||||
[auto-ap.ssr.admin.transaction-rules :as admin-rules]
|
||||
[auto-ap.ssr.admin.vendors :as admin-vendors]
|
||||
[auto-ap.ssr.auth :as auth]
|
||||
[auto-ap.ssr.dashboard :as dashboard]
|
||||
[auto-ap.ssr.company :as company]
|
||||
[auto-ap.ssr.company-dropdown :as company-dropdown]
|
||||
[auto-ap.ssr.company.company-1099 :as company-1099]
|
||||
[auto-ap.ssr.company.plaid :as company-plaid]
|
||||
[auto-ap.ssr.company.reports :as company-reports]
|
||||
[auto-ap.ssr.company.yodlee :as company-yodlee]
|
||||
[auto-ap.ssr.indicators :as indicators]
|
||||
[auto-ap.ssr.invoice.glimpse :as invoice-glimpse]
|
||||
[auto-ap.ssr.invoices :as invoice]
|
||||
[auto-ap.ssr.outgoing-invoice.new :as oin]
|
||||
[auto-ap.ssr.payments :as payments]
|
||||
[auto-ap.ssr.pos.cash-drawer-shifts :as pos-cash-drawer-shifts]
|
||||
[auto-ap.ssr.pos.expected-deposits :as pos-expected-deposits]
|
||||
[auto-ap.ssr.pos.refunds :as pos-refunds]
|
||||
[auto-ap.ssr.pos.sales-orders :as pos-sales]
|
||||
[auto-ap.ssr.pos.tenders :as pos-tenders]
|
||||
[auto-ap.ssr.search :as search]
|
||||
[auto-ap.ssr.transaction.insights :as insights]
|
||||
[auto-ap.ssr.users :as users]
|
||||
[auto-ap.ssr.vendor :as vendors]
|
||||
[ring.middleware.json :refer [wrap-json-response]]))
|
||||
|
||||
;; from auto-ap.ssr-routes, because they're shared
|
||||
|
||||
@@ -66,14 +67,11 @@
|
||||
:company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog))
|
||||
:company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account))
|
||||
:company-yodlee-provider-account-reauthenticate (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/reauthenticate))
|
||||
:company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page))
|
||||
:company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/table))
|
||||
:company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report))
|
||||
:invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/page))
|
||||
:invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/upload))
|
||||
:invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice))
|
||||
:invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice))
|
||||
:invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice))
|
||||
:invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/page) {:activity :import :subject :invoice}))
|
||||
:invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/upload) {:activity :import :subject :invoice}))
|
||||
:invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/textract-invoice) {:activity :import :subject :invoice}))
|
||||
:invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/create-invoice) {:activity :import :subject :invoice}))
|
||||
:invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/update-textract-invoice) {:activity :import :subject :invoice}))
|
||||
:vendor-search (wrap-client-redirect-unauthenticated (wrap-secure vendors/search))
|
||||
:transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page))
|
||||
:transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table))
|
||||
@@ -83,6 +81,7 @@
|
||||
:transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain))
|
||||
:admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page))
|
||||
:search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}
|
||||
(into company-reports/key->handler)
|
||||
(into company-1099/key->handler)
|
||||
(into invoice/key->handler)
|
||||
(into import-batch/key->handler)
|
||||
@@ -100,6 +99,7 @@
|
||||
(into admin-vendors/key->handler)
|
||||
(into admin-clients/key->handler)
|
||||
(into admin-rules/key->handler)
|
||||
(into dashboard/key->handler)
|
||||
(into indicators/key->handler)
|
||||
(into payments/key->handler)
|
||||
(into oin/route->handler)))
|
||||
|
||||
340
src/clj/auto_ap/ssr/dashboard.clj
Normal file
340
src/clj/auto_ap/ssr/dashboard.clj
Normal file
@@ -0,0 +1,340 @@
|
||||
(ns auto-ap.ssr.dashboard
|
||||
(:require [auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.graphql.ledger :refer [get-profit-and-loss-raw]]
|
||||
[auto-ap.graphql.utils :refer [<-graphql]]
|
||||
[auto-ap.ledger.reports :as r]
|
||||
[auto-ap.routes.dashboard :as d-routes]
|
||||
[auto-ap.routes.invoice :as i-routes]
|
||||
[auto-ap.routes.utils :refer [wrap-admin
|
||||
wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.company.reports.expense :refer [expense-breakdown-card]]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
|
||||
html-response]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[datomic.api :as dc]
|
||||
[hiccup.util :as hu]))
|
||||
|
||||
(defn bank-accounts-card [request]
|
||||
(html-response
|
||||
(com/card {:class "h-full"}
|
||||
[:div.p-4.h-full
|
||||
[:h1.text-2xl.font-bold "Bank Accounts"]
|
||||
[:div (hx/htmx-transition-appear {:class "h-full overflow-scroll" })
|
||||
(for [c (:valid-trimmed-client-ids request)
|
||||
b (:client/bank-accounts (dc/pull (dc/db conn) '[{:client/bank-accounts
|
||||
|
||||
[:bank-account/current-balance
|
||||
{[:bank-account/type :xform iol-ion.query/ident] [:db/ident]}
|
||||
|
||||
[:bank-account/current-balance-synced :xform clj-time.coerce/from-date]
|
||||
:bank-account/name
|
||||
{:bank-account/intuit-bank-account [:intuit-bank-account/current-balance
|
||||
[:intuit-bank-account/last-synced :xform clj-time.coerce/from-date]]}
|
||||
{:bank-account/yodlee-account [:yodlee-account/available-balance
|
||||
[:yodlee-account/last-synced :xform clj-time.coerce/from-date]]}
|
||||
{:bank-account/plaid-account [:plaid-account/balance
|
||||
[:plaid-account/last-synced :xform clj-time.coerce/from-date]]}]}]
|
||||
c))
|
||||
:when (not= :bank-account-type/cash (:bank-account/type b))]
|
||||
[:div.flex.flex-col.p-4.border-b-2.border-gray-200
|
||||
[:div.font-bold.text-gray-700 (:client/name c)]
|
||||
[:div (:bank-account/name b)]
|
||||
[:div.grid.grid-cols-3.gap-x-2.items-baseline
|
||||
[:div "Ledger Balance"]
|
||||
[:div.text-right (format "$%,.2f" (or (:bank-account/current-balance b) 0.0))]
|
||||
[:div.text-xs.text-gray-400.text-right (some-> (:bank-account/current-balance-synced b)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %)))]
|
||||
|
||||
(when-let [n (cond (-> b :bank-account/intuit-bank-account)
|
||||
"Intuit"
|
||||
(-> b :bank-account/yodlee-account)
|
||||
"Yodlee"
|
||||
(-> b :bank-account/plaid-account)
|
||||
"Plaid"
|
||||
:else
|
||||
nil)]
|
||||
(list
|
||||
[:div (str n " Balance")]
|
||||
[:div.text-right (format "$%,.2f" (or (-> b :bank-account/intuit-bank-account :intuit-bank-account/current-balance)
|
||||
(-> b :bank-account/yodlee-account :yodlee-account/available-balance)
|
||||
(-> b :bank-account/plaid-account :plaid-account/balance)
|
||||
0.0))]
|
||||
[:div.text-xs.text-gray-400.text-right (or (some-> (:bank-account/intuit-bank-account b)
|
||||
(:intuit-bank-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %)))
|
||||
(some-> (:bank-account/yodlee-account b)
|
||||
(:yodlee-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %)))
|
||||
(some-> (:bank-account/plaid-account b)
|
||||
(:plaid-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %))))]
|
||||
[:div.inline-flex.justify-end.text-xs.text-gray-400.it]))
|
||||
#_[:div.inline-flex.justify-between.items-baseline]]])]])))
|
||||
|
||||
(defn sales-chart-card [request]
|
||||
(html-response
|
||||
(let [ totals
|
||||
(->> (dc/q '[:find ?sd (sum ?total)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/date ?d]
|
||||
[(iol-ion.query/iso-date ?d) ?sd]
|
||||
[?e :sales-order/total ?total]]
|
||||
(dc/db conn)
|
||||
[(:valid-trimmed-client-ids request)
|
||||
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/days -14))))
|
||||
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/days 1))))])
|
||||
(sort-by first))]
|
||||
(com/card {:class "w-full h-full p-4"}
|
||||
[:h1.text-2xl.font-bold.text-slate-700 "Gross sales, last 14 days"]
|
||||
[:div.w-full.h-full
|
||||
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
|
||||
:labels (map first totals)
|
||||
:data (map second totals)})
|
||||
:x-init
|
||||
"new Chart($el, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Gross sales',
|
||||
data: data,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});"}]]))))
|
||||
|
||||
(defn expense-pie-card [request]
|
||||
(html-response
|
||||
(let [ totals
|
||||
(->> (dc/q '[:find ?an (sum ?amt)
|
||||
:with ?iea
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-invoices $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :invoice/total ?total]
|
||||
[?e :invoice/expense-accounts ?iea]
|
||||
[?iea :invoice-expense-account/account ?ea]
|
||||
[?iea :invoice-expense-account/amount ?amt]
|
||||
[?ea :account/name ?an]]
|
||||
(dc/db conn)
|
||||
[(:valid-trimmed-client-ids request)
|
||||
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/months -1))))
|
||||
(coerce/to-date (time/plus (time/with-time-at-start-of-day (time/now)) (time/days 1)))])
|
||||
(sort-by last)
|
||||
(reverse)
|
||||
(take 5))]
|
||||
(com/card {:class "w-full h-full p-4"}
|
||||
[:h1.text-2xl.font-bold.text-slate-700
|
||||
"Expenses, last month"]
|
||||
[:div.w-full.h-full
|
||||
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
|
||||
:labels (map first totals)
|
||||
:data (map second totals)})
|
||||
:x-init " new Chart($el, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Total invoices',
|
||||
data: data,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
|
||||
}
|
||||
});"}]]))))
|
||||
|
||||
(defn pnl-card [request]
|
||||
(html-response
|
||||
(com/card {:class "w-full h-full p-4"}
|
||||
[:h1.text-2xl.font-bold.text-gray-700
|
||||
"Profit and Loss, last month" ]
|
||||
(let [ data (<-graphql (get-profit-and-loss-raw (:valid-trimmed-client-ids request)
|
||||
[{:start (time/plus (time/now) (time/months -1))
|
||||
:end (time/now)}]))
|
||||
data (r/->PNLData {} (:accounts (first (:periods data))) {})
|
||||
sales (r/aggregate-accounts (r/filter-categories data [ :sales]))
|
||||
expenses (r/aggregate-accounts (r/filter-categories data [ :cogs :payroll :controllable :fixed-overhead :ownership-controllable ]))]
|
||||
(list
|
||||
#_(when (not= (count all-clients) (count clients))
|
||||
)
|
||||
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
|
||||
:labels [(format "Income $%,.2f" sales) (format "Expenses $%,.2f" expenses)]
|
||||
:data [sales expenses]})
|
||||
:x-init
|
||||
"new Chart($el, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Dollars',
|
||||
data: data,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
indexAxis: 'y',
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});"}]
|
||||
[:div
|
||||
"Income: " (format "$%,.2f" sales)]
|
||||
[:div
|
||||
"Expenses: " (format "$%,.2f" expenses)])))))
|
||||
|
||||
(defn tasks-card [request]
|
||||
(html-response
|
||||
(com/card {:class "w-full h-full p-4"}
|
||||
[:h1.text-2xl.font-bold.text-gray-700
|
||||
"Tasks"]
|
||||
[:div (hx/htmx-transition-appear {:class "space-y-2"})
|
||||
(let [[unpaid-invoice-count unpaid-invoice-amount]
|
||||
(first (dc/q '[:find (count ?e) (sum ?ab)
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-invoices $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :invoice/status :invoice-status/unpaid]
|
||||
[?e :invoice/outstanding-balance ?ab]]
|
||||
(dc/db conn)
|
||||
[(:valid-trimmed-client-ids request)
|
||||
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1))))
|
||||
nil]))
|
||||
|
||||
[uncategorized-transaction-count uncategorized-transaction-amount]
|
||||
(first (dc/q '[:find (count ?e) (sum ?am)
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-transactions $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :transaction/approval-status :transaction-approval-status/requires-feedback]
|
||||
[?e :transaction/amount ?am]]
|
||||
(dc/db conn)
|
||||
[(:valid-trimmed-client-ids request)
|
||||
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1))))
|
||||
nil]))]
|
||||
(list
|
||||
(when (not= 0 (or unpaid-invoice-count 0))
|
||||
[:div.bg-gray-50.rounded.p-4
|
||||
[:span "You have " (str unpaid-invoice-count) " unpaid invoices with an outstanding balance of " (format "$%,.2f" unpaid-invoice-amount) ". " ]
|
||||
|
||||
(com/link {:href (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::i-routes/unpaid-page)
|
||||
{:date-range "year"})
|
||||
}
|
||||
|
||||
"Pay now")
|
||||
])
|
||||
(when (not= 0 (or uncategorized-transaction-count 0))
|
||||
[:div.bg-gray-50.rounded.p-4
|
||||
[:span "You have " (str uncategorized-transaction-count) " transactions needing your feedback. " ]
|
||||
|
||||
(com/link {:href (str (bidi.bidi/path-for client-routes/routes :requires-feedback-transactions)
|
||||
"?date-range="
|
||||
(url/url-encode (pr-str {:start (atime/unparse-local (time/plus (time/now) (time/years -1)) atime/iso-date) :end (atime/unparse-local (time/now) atime/iso-date)}))) }
|
||||
|
||||
"Review now")])))])))
|
||||
|
||||
(defn stub-card [params & children]
|
||||
(com/card (-> params
|
||||
(dissoc :title)
|
||||
(update :class #(hh/add-class (or % "") "w-full h-full p-4 space-y-2"))
|
||||
(assoc :hx-swap "outerHTML"))
|
||||
[:h1.text-2xl.font-bold.text-gray-700
|
||||
(:title params)]
|
||||
[:div.w-full.h-full.flex.justify-center.items-center
|
||||
[:div.htmx-indicator (svg/spinner {:class "inline w-32 h-32 text-green-500"})]]))
|
||||
|
||||
(defn- page-contents [request]
|
||||
[:div.mb-8
|
||||
[:div {:class "grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-4 mb-8"}
|
||||
[:div.h-96 (stub-card {:title "Expenses"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/expense-card)
|
||||
:hx-trigger "load"} )]
|
||||
[:div.h-96
|
||||
(stub-card {:title "Tasks"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/tasks-card)
|
||||
:hx-trigger "load"} )]
|
||||
[:div {:class " row-span-2 h-[49rem]"}
|
||||
(stub-card {:title "Bank Accounts"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/bank-accounts-card)
|
||||
:hx-trigger "load"} )
|
||||
]
|
||||
|
||||
[:div.h-96
|
||||
(stub-card {:title "Gross Sales, last 14 days"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/sales-card)
|
||||
:hx-trigger "load"})
|
||||
]
|
||||
[:div.h-96
|
||||
(stub-card {:title "Profit and Loss, last month"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/pnl-card)
|
||||
:hx-trigger "load"}) ]
|
||||
[:div.col-span-2.h-96
|
||||
(stub-card {:title "Expense breakdown"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card)
|
||||
:hx-trigger "load"} )]
|
||||
[:div]] ])
|
||||
|
||||
(defn page [request]
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::d-routes/page)
|
||||
:hx-trigger "clientSelected from:body"
|
||||
:hx-select "#app-contents"
|
||||
:hx-swap "outerHTML swap:300ms"}
|
||||
|
||||
:request request}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes ::d-routes/page)}
|
||||
"Dashboard"])
|
||||
(when (:clients-trimmed? request)
|
||||
[:div.bg-yellow-100.rounded-lg.p-4.my-2.text-yellow-900.border-1 "Warning: These reports are only for twenty of the selected customers. Please select a specific customer to see more detail."])
|
||||
(page-contents request))
|
||||
"Dashboard"))
|
||||
|
||||
(def key->handler
|
||||
( apply-middleware-to-all-handlers
|
||||
{::d-routes/page page
|
||||
::d-routes/expense-card expense-pie-card
|
||||
::d-routes/pnl-card pnl-card
|
||||
::d-routes/sales-card sales-chart-card
|
||||
::d-routes/bank-accounts-card bank-accounts-card
|
||||
::d-routes/tasks-card tasks-card }
|
||||
(fn [h]
|
||||
(wrap-client-redirect-unauthenticated (wrap-admin h)))))
|
||||
@@ -213,6 +213,7 @@
|
||||
set)]
|
||||
(handler (assoc request :trimmed-clients valid-clients)))))
|
||||
|
||||
|
||||
(defn table-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
|
||||
(cond-> (fn table [{:keys [identity] :as request}]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns auto-ap.ssr.hx
|
||||
(:require [cheshire.core :as cheshire]
|
||||
[clojure.string :as str]))
|
||||
[clojure.string :as str]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]))
|
||||
|
||||
|
||||
(defn vals [m]
|
||||
@@ -53,3 +54,9 @@
|
||||
(defn trigger-click-or-enter [m]
|
||||
(assoc m :hx-trigger "click, keyup[keyCode==13]"))
|
||||
|
||||
(defn htmx-transition-appear [params]
|
||||
(-> params
|
||||
(update :class (fn [c]
|
||||
(-> (or c "")
|
||||
(hh/add-class "opacity-100 transition htmx-added:opacity-0 duration-300")))))
|
||||
)
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
:invoice/total
|
||||
:invoice/outstanding-balance
|
||||
:invoice/source-url
|
||||
|
||||
|
||||
[:invoice/date :xform clj-time.coerce/from-date]
|
||||
[:invoice/due :xform clj-time.coerce/from-date]
|
||||
[:invoice/scheduled-payment :xform clj-time.coerce/from-date]
|
||||
@@ -17,9 +15,10 @@
|
||||
{:account-client-override/client [:db/id]}]}]}]
|
||||
[:transaction/_invoices :as :invoice/transaction] [:db/id]
|
||||
[:journal-entry/_original-entity :as :invoice/journal-entry] [:db/id]
|
||||
[:payment/_invoices :as :invoice/payments] [:db/id :payment/date :payment/amount
|
||||
[:invoice-payment/_invoice :as :invoice/payments] [{:invoice-payment/payment [:db/id :payment/date :payment/amount
|
||||
|
||||
{[:transaction/_payment :as :payment/transaction] [:db/id]
|
||||
[:payment/status :xform iol-ion.query/ident] [:db/ident]}]
|
||||
{[:transaction/_payment :as :payment/transaction] [:db/id]
|
||||
[:payment/status :xform iol-ion.query/ident] [:db/ident]}]}]
|
||||
#_[:payment/_invoices :as :invoice/payments]
|
||||
[:invoice/status :xform iol-ion.query/ident] [:db/ident]
|
||||
:invoice/vendor [:vendor/name :db/id]}])
|
||||
@@ -1,33 +1,32 @@
|
||||
|
||||
(ns auto-ap.ssr.invoice.glimpse
|
||||
(:require
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[amazonica.aws.textract :as textract]
|
||||
[auto-ap.datomic :refer [conn pull-attr pull-id]]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [html-response path->name]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clj-time.coerce :as coerce]
|
||||
[cheshire.core :as cheshire]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]
|
||||
[hiccup2.core :as hiccup]
|
||||
[iol-ion.tx :refer [random-tempid]]
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
[clj-time.core :as time])
|
||||
(:import
|
||||
(java.util UUID)))
|
||||
(:require [amazonica.aws.s3 :as s3]
|
||||
[amazonica.aws.textract :as textract]
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic :refer [conn pull-attr]]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [html-response path->name]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[cheshire.core :as cheshire]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]
|
||||
[hiccup2.core :as hiccup]
|
||||
[iol-ion.tx :refer [random-tempid]])
|
||||
(:import (java.util UUID)))
|
||||
|
||||
(def bucket-name (:data-bucket env))
|
||||
|
||||
@@ -63,8 +62,12 @@
|
||||
[[] #{}]
|
||||
xs)))
|
||||
|
||||
(defn textract->textract-invoice [id tx]
|
||||
(defn textract->textract-invoice [request id tx]
|
||||
(let [lookup (lookup tx)
|
||||
valid-client-ids (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
total-options (->> (stack-rank #{"AMOUNT_DUE"} lookup)
|
||||
(map (fn [t]
|
||||
[t (some->> t
|
||||
@@ -87,8 +90,16 @@
|
||||
[t (->> (solr/query solr/impl "clients" {"query" (format "name:(%s) ", (clean-customer t)) "fields" "score, *"})
|
||||
#_(filter (fn [d] (> (:score d) 4.0)))
|
||||
(map (comp #(Long/parseLong %) :id))
|
||||
first)]))))
|
||||
first)]))
|
||||
(filter (fn [[t id]]
|
||||
(valid-client-ids id)))))
|
||||
deduplicate)
|
||||
customer-identifier-options (if (seq customer-identifier-options)
|
||||
customer-identifier-options
|
||||
(->> valid-client-ids
|
||||
(take 10)
|
||||
(map (fn [c]
|
||||
[(pull-attr (dc/db conn) :client/name c) c]))))
|
||||
vendor-name-options (->> (stack-rank #{"VENDOR_NAME"} lookup)
|
||||
(mapcat (fn [t]
|
||||
(for [m (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", t) "fields" "score, *"})
|
||||
@@ -161,13 +172,13 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(update :textract-invoice/vendor-name vendor-name-tuple->vendor-tuple)
|
||||
(update :textract-invoice/vendor-name-options #(map vendor-name-tuple->vendor-tuple %) )))
|
||||
|
||||
(defn refresh-job [id]
|
||||
(defn refresh-job [request id]
|
||||
(let [{:keys [:db/id :textract-invoice/job-id :textract-invoice/textract-status]} (get-job id)]
|
||||
(when (and job-id (= "IN_PROGRESS" textract-status))
|
||||
(let [result (textract/get-expense-analysis {:job-id job-id})
|
||||
new-status (:job-status result)]
|
||||
(cond (= "SUCCEEDED" new-status)
|
||||
@(dc/transact conn [[:upsert-entity (textract->textract-invoice id result)]])
|
||||
@(dc/transact conn [[:upsert-entity (textract->textract-invoice request id result)]])
|
||||
:else
|
||||
@(dc/transact conn [{:db/id id :textract-invoice/textract-status new-status}]))))
|
||||
(get-job id)))
|
||||
@@ -198,6 +209,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(com/field {:label "Client"}
|
||||
(com/text-input {:name (path->name [:invoice/client])
|
||||
:value (-> textract-invoice :textract-invoice/customer-identifier second second)
|
||||
:class "w-96"
|
||||
:placeholder "Client"
|
||||
:disabled true
|
||||
:autofocus true}))]
|
||||
@@ -213,6 +225,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
[:div.col-span-6
|
||||
(com/field {:label "Vendor"}
|
||||
(com/text-input {:name (path->name [:invoice/vendor])
|
||||
:class "w-96"
|
||||
:value (-> textract-invoice :textract-invoice/vendor-name second second)
|
||||
:disabled true
|
||||
:placeholder "Vendor"}))]
|
||||
@@ -270,8 +283,8 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(str invoice-number))})]
|
||||
(com/button {:color :primary} "Save")])
|
||||
|
||||
(defn job-progress* [id]
|
||||
(let [textract-invoice (refresh-job id)]
|
||||
(defn job-progress* [request id]
|
||||
(let [textract-invoice (refresh-job request id)]
|
||||
(cond
|
||||
(= "IN_PROGRESS" (:textract-invoice/textract-status textract-invoice))
|
||||
[:div.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.max-w-md.w-md.text-center.cursor-pointer
|
||||
@@ -290,12 +303,12 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
[:div {:style {:width "805"}}
|
||||
(com/card {}
|
||||
[:iframe.p-4 {:src (:textract-invoice/pdf-url textract-invoice) :width 791 :height 700}])]
|
||||
[:div {:class "basis-1/4"}
|
||||
[:div {:class "basis-1/2"}
|
||||
(com/card {}
|
||||
[:div.p-4
|
||||
(textract->invoice-form* textract-invoice)])]]])))
|
||||
|
||||
(defn page* [id]
|
||||
(defn page* [request id]
|
||||
[:div#invoice-glimpse-content.mt-4
|
||||
(com/card {}
|
||||
[:div.px-4.py-3.space-y-4.flex.flex-col
|
||||
@@ -307,7 +320,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
[:p.text-sm.italic "Import your invoices with the power of AI. Please only use PDFs with a single invoice in them."]
|
||||
|
||||
(when id
|
||||
(job-progress* id))
|
||||
(job-progress* request id))
|
||||
(when-not id
|
||||
(upload-form*))])])
|
||||
|
||||
@@ -402,7 +415,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
_ (when new-invoice-id @(dc/transact conn [{:db/id (:db/id current-job)
|
||||
:textract-invoice/invoice new-invoice-id}]))]
|
||||
(if new-invoice-id
|
||||
(html-response (page* nil)
|
||||
(html-response (page* request nil)
|
||||
:headers {"hx-push-url" (bidi/path-for ssr-routes/only-routes :invoice-glimpse)
|
||||
"hx-retarget" "#invoice-glimpse-content"
|
||||
"hx-trigger" (cheshire/generate-string {"notification" (str (hiccup/html [:div "Successfully created "
|
||||
@@ -420,10 +433,11 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
:method request-method)
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
(com/page {:nav com/main-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:request request
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse)
|
||||
@@ -437,7 +451,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse)}
|
||||
"Glimpse"])
|
||||
(page* (some-> request
|
||||
(page* request (some-> request
|
||||
:route-params
|
||||
:textract-invoice-id
|
||||
Long/parseLong)))
|
||||
@@ -446,7 +460,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
|
||||
(defn textract-invoice [request]
|
||||
(if (get-in request [:headers "hx-request"])
|
||||
(html-response (job-progress* (some-> request
|
||||
(html-response (job-progress* request (some-> request
|
||||
:route-params
|
||||
:textract-invoice-id
|
||||
Long/parseLong)))
|
||||
|
||||
@@ -379,8 +379,7 @@
|
||||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
||||
client-id)))})])
|
||||
|
||||
(defn- invoice-expense-account-row*
|
||||
[{:keys [value client-id]}]
|
||||
(defn- invoice-expense-account-row* [{:keys [value client-id]}]
|
||||
(com/data-grid-row
|
||||
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
|
||||
:accountId (fc/field-value (:invoice-expense-account/account value))})
|
||||
@@ -743,7 +742,13 @@
|
||||
|
||||
(exception->4xx #(assert-not-locked client-id (:invoice/date invoice)))
|
||||
(let [transaction-result (audit-transact [transaction] (:identity request))]
|
||||
(solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"]))
|
||||
(try
|
||||
(solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"]))
|
||||
(catch Exception e
|
||||
(alog/error ::cant-save-solr
|
||||
:error e
|
||||
))
|
||||
)
|
||||
(if extant?
|
||||
|
||||
(html-response
|
||||
|
||||
@@ -85,6 +85,14 @@
|
||||
:value (:vendor (:query-params request))
|
||||
:value-fn :db/id
|
||||
:content-fn :vendor/name}))
|
||||
(com/field {:label "Account"}
|
||||
(com/typeahead {:name "account"
|
||||
:id "account"
|
||||
:url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||
:value (:account (:query-params request))
|
||||
:value-fn :db/id
|
||||
:content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %))
|
||||
(:db/id (:client request))))}))
|
||||
(date-range-field* request)
|
||||
(com/field {:label "Check #"}
|
||||
(com/text-input {:name "check-number"
|
||||
@@ -100,7 +108,7 @@
|
||||
:value (:invoice-number (:query-params request))
|
||||
:placeholder "e.g., ABC-456"
|
||||
:size :small}))
|
||||
|
||||
|
||||
(com/field {:label "Amount"}
|
||||
[:div.flex.space-x-4.items-baseline
|
||||
(com/money-input {:name "amount-gte"
|
||||
@@ -171,6 +179,10 @@
|
||||
(merge-query {:query {:in ['?import-status]
|
||||
:where ['[?e :invoice/import-status ?import-status]]}
|
||||
:args [(:import-status query-params)]})
|
||||
|
||||
(not (:import-status query-params))
|
||||
(merge-query {:query { :where ['[?e :invoice/import-status :import-status/imported]]} })
|
||||
|
||||
(:status route-params)
|
||||
(merge-query {:query {:in ['?status]
|
||||
:where ['[?e :invoice/status ?status]]}
|
||||
@@ -180,11 +192,11 @@
|
||||
:where ['[?e :invoice/vendor ?vendor-id]]}
|
||||
:args [(:db/id (:vendor query-params))]})
|
||||
|
||||
(:account-id query-params)
|
||||
(:account query-params)
|
||||
(merge-query {:query {:in ['?account-id]
|
||||
:where ['[?e :invoice/expense-accounts ?iea ?]
|
||||
'[?iea :invoice-expense-account/account ?account-id]]}
|
||||
:args [(:account-id query-params)]})
|
||||
:args [(:db/id (:account query-params))]})
|
||||
|
||||
(:amount-gte query-params)
|
||||
(merge-query {:query {:in ['?amount-gte]
|
||||
@@ -297,6 +309,7 @@
|
||||
[:amount-gte {:optional true} [:maybe :double]]
|
||||
[:amount-lte {:optional true} [:maybe :double]]
|
||||
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]
|
||||
[:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]]
|
||||
[:check-number {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:status {:optional true} [:maybe (ref->enum-schema "invoice-status")]]
|
||||
@@ -539,6 +552,7 @@
|
||||
(link-dropdown
|
||||
(concat (->> i
|
||||
:invoice/payments
|
||||
(map :invoice-payment/payment)
|
||||
(filter (fn [p]
|
||||
(not= :payment-status/voided
|
||||
(:payment/status p))))
|
||||
|
||||
@@ -364,7 +364,10 @@
|
||||
:name "Links"
|
||||
:class "w-8"
|
||||
:render (fn [p]
|
||||
(link-dropdown (concat (->> p :payment/invoices (map (fn [invoice]
|
||||
(link-dropdown (concat (->> p :invoice-payment/_payment
|
||||
(map :invoice-payment/invoice)
|
||||
(filter identity)
|
||||
(map (fn [invoice]
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/all-page)
|
||||
{:exact-match-id (:db/id invoice)})
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
[:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/styles/choices.min.css"}]
|
||||
[:script {:src "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/scripts/choices.min.js"}]
|
||||
[:script {:src "https://unpkg.com/htmx.org/dist/ext/response-targets.js"}]
|
||||
[:script {:src "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js" :integrity "sha512-CQBWl4fJHWbryGE+Pc7UAxWMUMNMWzWxF4SQo9CgkJIN1kx6djDQZjh3Y8SZ1d+6I+1zze6Z7kHXO7q3UyZAWw==" :crossorigin "anonymous" :referrerpolicy "no-referrer"}]
|
||||
|
||||
[:script {:src "https://unpkg.com/dropzone@5.9.3/dist/min/dropzone.min.js"}]
|
||||
[:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}]
|
||||
@@ -69,9 +70,9 @@ input::-webkit-inner-spin-button {
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
} "]
|
||||
[:body {:hx-ext "disable-submit, class-tools"
|
||||
[:body {:hx-ext "disable-submit, class-tools"
|
||||
:x-data (hx/json {:globalClientSelection (or (:client-selection request)
|
||||
:all )}) ;; TODO remove once session is used
|
||||
:all)}) ;; TODO remove once session is used
|
||||
:x-hx-header:x-clients "JSON.stringify(globalClientSelection)"}
|
||||
contents
|
||||
[:script {:src "/js/flowbite.min.js"}]
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
(if date-range-value
|
||||
(-> (condp = date-range-value
|
||||
"week"
|
||||
(let [last-monday (atime/last-monday)]
|
||||
(let [last-monday (coerce/to-date-time (atime/last-monday))]
|
||||
(assoc m
|
||||
start-date-key (time/plus last-monday (time/days -7))
|
||||
end-date-key last-monday))
|
||||
@@ -248,16 +248,16 @@
|
||||
"month"
|
||||
(assoc m
|
||||
start-date-key (time/plus (time/now) (time/months -1))
|
||||
end-date-key (time/now))
|
||||
end-date-key nil)
|
||||
|
||||
"year"
|
||||
(assoc m
|
||||
start-date-key (time/plus (time/now) (time/years -1))
|
||||
end-date-key (time/now))
|
||||
end-date-key nil)
|
||||
|
||||
"all"
|
||||
(assoc m start-date-key (time/plus (time/now) (time/years -6))
|
||||
end-date-key (time/now))
|
||||
end-date-key nil)
|
||||
|
||||
m)
|
||||
(dissoc date-range-key))
|
||||
@@ -439,10 +439,14 @@
|
||||
:explain
|
||||
(me/humanize {:errors (assoc me/default-errors
|
||||
::mc/missing-key {:error/message {:en "required"}})}))
|
||||
(map (fn [[k v]]
|
||||
(str (if (keyword? k)
|
||||
(name k)
|
||||
k) ": " (str/join ", " v))))
|
||||
(map (fn [x]
|
||||
(if (and (sequential? x)
|
||||
(= (count x) 2))
|
||||
(let [[k v] x]
|
||||
(str (if (keyword? k)
|
||||
(name k)
|
||||
k) ": " (str/join ", " v))
|
||||
(str x)))))
|
||||
(str/join ", "))
|
||||
{:type :schema-validation
|
||||
:decoded (:value (:data (ex-data e)))
|
||||
@@ -539,7 +543,8 @@
|
||||
{:path (:in e)
|
||||
:message (get-in humanized (:in e))})
|
||||
(:errors (:explain (:error e))))]
|
||||
(alog/warn ::form-4xx :errors errors)
|
||||
(alog/warn ::form-4xx :errors errors
|
||||
:data e)
|
||||
(form-handler (assoc request
|
||||
:form-params (:decoded e)
|
||||
:field-validation-errors errors
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
|
||||
(defn last-monday []
|
||||
(loop [current (local-now)]
|
||||
(loop [current (local-today)]
|
||||
(if (= 1 (time/day-of-week current))
|
||||
current
|
||||
(recur (time/minus current (time/days 1))))))
|
||||
|
||||
22
src/clj/auto_ap/yodlee/core2.fiddle
Normal file
22
src/clj/auto_ap/yodlee/core2.fiddle
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
(in-ns 'auto-ap.yodlee.core2)
|
||||
|
||||
(map :postDate (get-specific-transactions "NGGG" 17203328))
|
||||
|
||||
(->> (dc/q '[:find ?ba (count ?ya)
|
||||
:in $
|
||||
:where [?ba :bank-account/yodlee-account ?ya]
|
||||
]
|
||||
(dc/db conn))
|
||||
(filter (comp #(> % 1) second)))
|
||||
|
||||
(dc/q '[:find ?ya ?ba ?cd ?ud
|
||||
:in $ ?cd
|
||||
:where
|
||||
[?ba :bank-account/yodlee-account ?y]
|
||||
[(get-else $ ?ba :bank-account/use-date-instead-of-post-date? false) ?ud]
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?c :client/code ?cd]
|
||||
[?y :yodlee-account/id ?ya]]
|
||||
(dc/db conn)
|
||||
"NGGG")
|
||||
@@ -329,9 +329,10 @@
|
||||
(defn tx-detail [i]
|
||||
(map (juxt :e #(pull-attr (dc/db conn) :db/ident (:a %)) :v)
|
||||
(:data (first
|
||||
(dc/tx-range conn
|
||||
{:start i
|
||||
:end (inc i)})))))
|
||||
(dc/tx-range (dc/log conn)
|
||||
i
|
||||
(inc i))))))
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn tx-range-detail [i]
|
||||
(map (juxt :e #(pull-attr (dc/db conn) :db/ident (:a %)) :v)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
(init-repl)
|
||||
|
||||
(comment
|
||||
(defn setup-randy-queries []
|
||||
(import '[java.util UUID])
|
||||
@@ -324,4 +325,146 @@
|
||||
@(dc/transact conn [{:bank-account/code "NGKG-AMEX81007" :bank-account/visible true}])
|
||||
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
(clojure.data.csv/write-csv *out*
|
||||
(let [db (dc/db conn)]
|
||||
|
||||
(dc/q '[:find ?d4 ?s (sum ?total)
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where
|
||||
[(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/source ?s]
|
||||
[?e :sales-order/total ?total]
|
||||
[?e :sales-order/date ?d]
|
||||
[(iol-ion.query/excel-date ?d) ?d4]]
|
||||
|
||||
db
|
||||
[
|
||||
[ (pull-attr db :db/id [:client/code "NGA1"])]
|
||||
#inst "2023-01-01" #inst "2024-01-01"]))
|
||||
:separator \tab)
|
||||
|
||||
(pull-attr (dc/db conn) :db/id [:client/code "NGRV"])
|
||||
|
||||
(clojure.data.csv/write-csv *out*
|
||||
(let [db (dc/db conn)]
|
||||
|
||||
(dc/q '[:find ?d4 (sum ?a)
|
||||
:in $ [?clients ?start-date ?end-date] ?v
|
||||
:where
|
||||
[(iol-ion.query/scan-transactions $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :transaction/date ?d]
|
||||
[(iol-ion.query/excel-date ?d) ?d4]
|
||||
[?e :transaction/amount ?a]
|
||||
[?e :transaction/vendor ?v]]
|
||||
|
||||
db
|
||||
[
|
||||
[ (pull-attr db :db/id [:client/code "NGA1"])]
|
||||
#inst "2023-01-01" #inst "2024-01-01"]
|
||||
(ffirst (dc/q '[:find ?v :where [?v :vendor/name "CCP Doordash"]] db))))
|
||||
:separator \tab)
|
||||
|
||||
|
||||
|
||||
|
||||
(def taptelis-clients (->> (dc/q '[:find ?c
|
||||
:where [?u :user/name ?n]
|
||||
[?u :user/clients ?c]
|
||||
[?c :client/code ?cc]
|
||||
[(clojure.string/includes? ?n "Nick Taptelis")]]
|
||||
(dc/db conn))
|
||||
(map first)
|
||||
set))
|
||||
|
||||
(clojure.data.csv/write-csv *out* (dc/q '[:find ?v ?cnt
|
||||
:in $ [?c ...]
|
||||
:where [?vu :vendor-usage/vendor ?v]
|
||||
[?vu :vendor-usage/client ?c]
|
||||
[?vu :vendor-usage/count ?cnt]
|
||||
[(> ?cnt 0)]]
|
||||
(dc/db conn)
|
||||
taptelis-clients)
|
||||
:separator \tab)
|
||||
|
||||
|
||||
|
||||
(comment
|
||||
|
||||
(defn cleanup-duplicate-vendors-get-merge []
|
||||
(with-open [i (io/reader (io/resource "duplicate_vendors.csv"))]
|
||||
(let [[header & rest] (clojure.data.csv/read-csv i)]
|
||||
|
||||
(->> rest
|
||||
(map (fn [h r]
|
||||
(into {} (map vector h r))) (repeat header))
|
||||
|
||||
(filter (fn [row]
|
||||
(not= (get row "Ben says don't merge?")
|
||||
"TRUE")))
|
||||
(map (fn [row]
|
||||
{:from (Long/parseLong (get row "vendor_id"))
|
||||
:to (Long/parseLong (get row "master_vendor_id"))}))
|
||||
(filter (fn [row]
|
||||
(not= (:from row) (:to row))))
|
||||
|
||||
(into []))))
|
||||
|
||||
)
|
||||
|
||||
(cleanup-duplicate-vendors-get-merge)
|
||||
|
||||
(defn merge-vendor [{:keys [from to]}]
|
||||
(let [valid-keys #{:transaction/recommended-vendor :cash-drawer-shift/vendor :payment/vendor :journal-entry/vendor :sales-refund/vendor :transaction/vendor :sales-order/vendor :vendor-usage/vendor :transaction-rule/vendor :invoice/vendor :expected-deposit/vendor}
|
||||
transaction (->> (dc/q {:find '[?x ?a2]
|
||||
:in '[$ [ ?a2 ...] ?vendor-from]
|
||||
:where ['[?x ?a2 ?vendor-from] ]}
|
||||
(dc/db conn)
|
||||
valid-keys
|
||||
from)
|
||||
(mapcat (fn [[src attr]]
|
||||
|
||||
[[:db/retract src attr from]
|
||||
[:db/add src attr to]]))
|
||||
(into []))]
|
||||
|
||||
(auto-ap.datomic/audit-transact-batch transaction {:user/role "VENDOR-DEDUPE-CLEANUP"})
|
||||
(auto-ap.datomic/audit-transact [[:db/retractEntity from]] {:user/role "VENDOR-DEDUPE-CLEANUP"})))
|
||||
|
||||
|
||||
|
||||
(doseq [v (cleanup-duplicate-vendors-get-merge)]
|
||||
(println v)
|
||||
(merge-vendor v))
|
||||
|
||||
(dc/q {:find '[?a2]
|
||||
:in '[$]
|
||||
:where ['[?v :vendor/name]
|
||||
'[_ ?a ?v]
|
||||
'[?a :db/ident ?a2]]}
|
||||
(dc/db conn))
|
||||
|
||||
|
||||
)
|
||||
|
||||
(dc/q '[:find ?ba (pull ?pa [* {:bank-account/_plaid-account [:db/id { :bank-account/integration-status [*]}]
|
||||
}])
|
||||
:in $ ?ba
|
||||
:where [?ba :bank-account/plaid-account ?pa]]
|
||||
(dc/db conn)
|
||||
[:bank-account/code "VS-BA6149"])
|
||||
|
||||
(init-repl)
|
||||
|
||||
(filter (fn [[_ x]]
|
||||
(> x 1))
|
||||
(dc/q '[:find ?pa (count ?ba)
|
||||
:where [?ba :bank-account/plaid-account ?pa]]
|
||||
(dc/db conn)))
|
||||
|
||||
|
||||
(dc/pull (dc/db conn) '[* {:bank-account/_plaid-account [:bank-account/code]}] 17592310327452)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
(ns auto-ap.permissions)
|
||||
(ns auto-ap.permissions
|
||||
#?(:clj
|
||||
(:require [cemerick.url :as url])))
|
||||
|
||||
;; TODO after getting rid of cljs, use malli schemas to decode this
|
||||
(defn get-client-id [client]
|
||||
@@ -113,3 +115,13 @@
|
||||
|
||||
:else
|
||||
false)))
|
||||
|
||||
#? (:clj
|
||||
(defn wrap-must [handler policy]
|
||||
(fn [request]
|
||||
(if (can? (:identity request) policy)
|
||||
(handler request)
|
||||
{:status 302
|
||||
:headers {"Location" (str "/login?"
|
||||
(url/map->query {"redirect-to" (:uri request)}))}}))))
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
(ns auto-ap.routes.admin.sales-summaries)
|
||||
(def routes {"" {:get ::page}
|
||||
"/table" ::table})
|
||||
(def routes {"" {:get ::page
|
||||
:put ::edit-wizard-submit}
|
||||
"/table" ::table
|
||||
|
||||
["/" [#"\d+" :db/id]] {:get ::edit-wizard }
|
||||
|
||||
"/edit/navigate" ::edit-wizard-navigate
|
||||
"/edit/sales-summary-item" ::new-summary-item})
|
||||
8
src/cljc/auto_ap/routes/dashboard.cljc
Normal file
8
src/cljc/auto_ap/routes/dashboard.cljc
Normal file
@@ -0,0 +1,8 @@
|
||||
(ns auto-ap.routes.dashboard)
|
||||
(def routes {""
|
||||
{:get ::page }
|
||||
"/expense-card" ::expense-card
|
||||
"/pnl-card" ::pnl-card
|
||||
"/sales-card" ::sales-card
|
||||
"/bank-accounts-card" ::bank-accounts-card
|
||||
"/tasks-card" ::tasks-card})
|
||||
@@ -6,6 +6,7 @@
|
||||
[auto-ap.routes.admin.vendors :as v-routes]
|
||||
[auto-ap.routes.outgoing-invoice :as oi-routes]
|
||||
[auto-ap.routes.payments :as p-routes]
|
||||
[auto-ap.routes.dashboard :as d-routes]
|
||||
[auto-ap.routes.invoice :as i-routes]
|
||||
[auto-ap.routes.admin.clients :as ac-routes]
|
||||
[auto-ap.routes.admin.sales-summaries :as ss-routes]
|
||||
@@ -16,6 +17,7 @@
|
||||
"search" :search
|
||||
"indicators" indicator-routes/routes
|
||||
|
||||
"dashboard" d-routes/routes
|
||||
"account" {"/search" {:get :account-search}}
|
||||
"admin" {"" :auto-ap.routes.admin/page
|
||||
"/client" ac-routes/routes
|
||||
@@ -84,7 +86,13 @@
|
||||
:post :company-1099-vendor-save}}
|
||||
"/reports" {"" {:get :company-reports
|
||||
:delete :company-reports-delete}
|
||||
"/table" :company-reports-table}
|
||||
"/table" :company-reports-table
|
||||
"/expense" {:get :company-expense-report
|
||||
"/card" :company-expense-report-breakdown-card
|
||||
"/invoice-total-card" :company-expense-report-invoice-total-card}
|
||||
"/reconciliation"
|
||||
{:get :company-reconciliation-report
|
||||
"/card" :company-reconciliation-report-card}}
|
||||
"/yodlee" {"" {:get :company-yodlee}
|
||||
"/table" {:get :company-yodlee-table}
|
||||
"/fastlink" {:get :company-yodlee-fastlink-dialog}
|
||||
|
||||
@@ -77,12 +77,10 @@
|
||||
(and token
|
||||
last-client-id
|
||||
(not last-selected-clients))
|
||||
[(js/parseInt last-client-id)]
|
||||
{:selected [(js/parseInt last-client-id)]}
|
||||
|
||||
:else
|
||||
nil)]
|
||||
|
||||
|
||||
(cond
|
||||
(= :login handler)
|
||||
{:db (cond-> (assoc db/default-db
|
||||
@@ -135,11 +133,11 @@
|
||||
::received-initial
|
||||
(fn [{:keys [db]} [_ {clients :client}]]
|
||||
(let [only-one-client (when (= 1 (count clients))
|
||||
(->> clients first :id))]
|
||||
(->> clients first :id js/parseInt))]
|
||||
(when only-one-client
|
||||
(.setItem js/localStorage "last-client-id" only-one-client)
|
||||
(.setItem js/localStorage "last-selected-clients"
|
||||
(pr-str [(js/parseInt only-one-client)])))
|
||||
(.stringify js/JSON (clj->js {:selected [only-one-client]}))))
|
||||
{:db (cond-> (-> db
|
||||
(assoc :clients (by :id clients))
|
||||
(assoc :is-initial-loading? false)
|
||||
@@ -149,7 +147,7 @@
|
||||
(filter #(= % (:last-client-id db)))
|
||||
first))))
|
||||
only-one-client (assoc :last-client-id only-one-client
|
||||
:selected-clients [only-one-client]))
|
||||
:selected-clients {:selected [only-one-client]}))
|
||||
:interval {:action :start
|
||||
:id :refresh-clients
|
||||
:frequency 600000
|
||||
|
||||
@@ -222,9 +222,8 @@
|
||||
(map r/as-element (r/children (r/current-component)))))
|
||||
|
||||
(defn cell [params]
|
||||
(apply r/create-element "td" #js {:className (:class params)}
|
||||
(map r/as-element (r/children (r/current-component))))
|
||||
)
|
||||
(apply r/create-element "td" #js {:className (:class params) :style (some-> (:style params) clj->js)}
|
||||
(map r/as-element (r/children (r/current-component)))))
|
||||
|
||||
(defn body []
|
||||
(let [children (r/children (r/current-component))
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
"Home" ]
|
||||
(when (p/can? @user {:subject :invoice-page})
|
||||
[:a.navbar-item {:class [(active-when ap #{:unpaid-invoices :paid-invoices})]
|
||||
:href (str (bidi/path-for ssr-routes/only-routes ::invoice-routes/unpaid-page) "?date-range=month")}
|
||||
:href (str (bidi/path-for ssr-routes/only-routes ::invoice-routes/unpaid-page) "?date-range=year")}
|
||||
"Invoices" ])
|
||||
(when (p/can? @user {:subject :payment-page})
|
||||
[:a.navbar-item {:class [(active-when ap = :payments)]
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
[auto-ap.status :as status]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::client
|
||||
:<- [::subs/clients]
|
||||
:<- [::subs/client]
|
||||
(fn [[ clients client]]
|
||||
(or client
|
||||
(first clients))))
|
||||
|
||||
(def pie-chart (r/adapt-react-class recharts/PieChart))
|
||||
(def pie (r/adapt-react-class recharts/Pie))
|
||||
(def bar-chart (r/adapt-react-class recharts/BarChart))
|
||||
@@ -239,8 +247,8 @@
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
[(re-frame/inject-cofx ::inject/sub [::subs/client])]
|
||||
(fn [{:keys [db] ::subs/keys [client]} _]
|
||||
[(re-frame/inject-cofx ::inject/sub [::client]) ]
|
||||
(fn [{:keys [db] ::keys [client]} _]
|
||||
(cond->
|
||||
{:db (assoc db ::top-expense-categories nil
|
||||
::cash-flow nil
|
||||
@@ -302,14 +310,19 @@
|
||||
[grid/cell {:class "has-text-right"} (->$ amount)]])]]]))
|
||||
|
||||
(defn home-content []
|
||||
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)
|
||||
(let [client @(re-frame/subscribe [::client])
|
||||
client-id (-> client :id)
|
||||
one-client (not (-> @(re-frame/subscribe [::subs/client]) :id))
|
||||
chart-options @(re-frame/subscribe [::chart-options])
|
||||
state @(re-frame/subscribe [::status/single ::page])]
|
||||
^{:key client-id}
|
||||
[side-bar-layout {:side-bar [:div]
|
||||
:main [:div [:h1.title "Home"]
|
||||
(if client-id
|
||||
(if (= :loading (:state state))
|
||||
(when one-client
|
||||
|
||||
[:h2.title.is-6 "Note: these reports are for "
|
||||
(:name client) ". Please choose a specific customer for their report."])
|
||||
(if (= :loading (:state state))
|
||||
[:div.loader.is-loading.big.is-centered]
|
||||
|
||||
[:<>
|
||||
@@ -350,17 +363,16 @@
|
||||
(make-cash-flow-chart {:width 800 :height 500
|
||||
:data (clj->js @(re-frame/subscribe [::cash-flow]))})
|
||||
|
||||
[cash-flow-grid]])
|
||||
[:h2.title.is-6 "Please select a customer to see reports."])]}]))
|
||||
[cash-flow-grid]])]}]))
|
||||
|
||||
|
||||
(defn home-page []
|
||||
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)]
|
||||
(let [client-id (-> @(re-frame/subscribe [::client]) :id)]
|
||||
(re-frame/dispatch [::mounted])
|
||||
^{:key client-id} [home-content]))
|
||||
|
||||
(defn home-page-with-vendor []
|
||||
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)
|
||||
(let [client-id (-> @(re-frame/subscribe [::client]) :id)
|
||||
user @(re-frame/subscribe [::subs/user])]
|
||||
(re-frame/dispatch [::mounted])
|
||||
(when (p/can? user {:subject :vendor
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
(ns auto-ap.views.pages.transactions
|
||||
(:require [auto-ap.effects.forward :as forward]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.layouts
|
||||
:refer
|
||||
[appearing-side-bar side-bar-layout]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[auto-ap.views.pages.transactions.common :refer [transaction-read data-params->query-params]]
|
||||
[auto-ap.views.pages.transactions.bulk-updates :as bulk]
|
||||
[auto-ap.views.pages.transactions.common :refer [data-params->query-params
|
||||
transaction-read]]
|
||||
[auto-ap.views.pages.transactions.form :as edit]
|
||||
[auto-ap.views.pages.transactions.manual :as manual]
|
||||
[auto-ap.views.pages.transactions.bulk-updates :as bulk]
|
||||
[auto-ap.views.pages.transactions.side-bar :as side-bar]
|
||||
[auto-ap.views.pages.transactions.table :as table]
|
||||
[auto-ap.views.utils :refer [dispatch-event with-user date->str standard]]
|
||||
[auto-ap.views.utils :refer [date->str dispatch-event standard
|
||||
with-user]]
|
||||
[auto-ap.views.utils :as u]
|
||||
[cljs-time.core :as time]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as reagent]
|
||||
[vimsical.re-frame.fx.track :as track]
|
||||
[auto-ap.status :as status]
|
||||
[clojure.string :as str]))
|
||||
[vimsical.re-frame.fx.track :as track]))
|
||||
|
||||
|
||||
|
||||
@@ -122,35 +125,38 @@
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
(fn [{:keys [db]} _]
|
||||
{:db (assoc-in db [::data-page/settled-filters ::page :date-range] {:start (date->str (time/plus (time/now) (time/months -1))
|
||||
standard)})
|
||||
::track/register {:id ::params
|
||||
:subscription [::data-page/params ::page]
|
||||
:event-fn (fn [params]
|
||||
[::params-change params])}
|
||||
::forward/register [{:id ::updated
|
||||
:events #{::edit/edited}
|
||||
:event-fn (fn [[_ edited-transaction]]
|
||||
[::data-page/updated-entity ::page edited-transaction])}
|
||||
{:id ::manual-import
|
||||
:events #{::manual/import-completed}
|
||||
:event-fn (fn [[_ result]]
|
||||
[::status/info ::manual-import
|
||||
(str "Successfully "
|
||||
(str/join ", "
|
||||
[(when-let [imported (:import-batch/imported result)]
|
||||
(str "imported " imported))
|
||||
(when-let [extant (:import-batch/extant result)]
|
||||
(str "extant " extant))
|
||||
(when-let [suppressed (:import-batch/suppressed result)]
|
||||
(str "suppressed " suppressed))
|
||||
(when-let [not-ready (:import-batch/not-ready result)]
|
||||
(str "too early " not-ready))
|
||||
(when-let [error (:validation-error result)]
|
||||
(str "errored " error))])
|
||||
" transactions."
|
||||
(when (:sample-error result)
|
||||
(str " Sample error: " (:info (:sample-error result)))))])}]}))
|
||||
(let [db (if (:date-range (u/query-params))
|
||||
db
|
||||
(assoc-in db [::data-page/settled-filters ::page :date-range] {:start (date->str (time/plus (time/now) (time/months -1))
|
||||
standard)}))]
|
||||
{:db db
|
||||
::track/register {:id ::params
|
||||
:subscription [::data-page/params ::page]
|
||||
:event-fn (fn [params]
|
||||
[::params-change params])}
|
||||
::forward/register [{:id ::updated
|
||||
:events #{::edit/edited}
|
||||
:event-fn (fn [[_ edited-transaction]]
|
||||
[::data-page/updated-entity ::page edited-transaction])}
|
||||
{:id ::manual-import
|
||||
:events #{::manual/import-completed}
|
||||
:event-fn (fn [[_ result]]
|
||||
[::status/info ::manual-import
|
||||
(str "Successfully "
|
||||
(str/join ", "
|
||||
[(when-let [imported (:import-batch/imported result)]
|
||||
(str "imported " imported))
|
||||
(when-let [extant (:import-batch/extant result)]
|
||||
(str "extant " extant))
|
||||
(when-let [suppressed (:import-batch/suppressed result)]
|
||||
(str "suppressed " suppressed))
|
||||
(when-let [not-ready (:import-batch/not-ready result)]
|
||||
(str "too early " not-ready))
|
||||
(when-let [error (:validation-error result)]
|
||||
(str "errored " error))])
|
||||
" transactions."
|
||||
(when (:sample-error result)
|
||||
(str " Sample error: " (:info (:sample-error result)))))])}]})))
|
||||
|
||||
(defn action-buttons []
|
||||
(let [is-admin? @(re-frame/subscribe [::subs/is-admin?])
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
:location
|
||||
:approval-status
|
||||
:check-number
|
||||
:is-locked
|
||||
|
||||
[:matched-rule [:note :id]]
|
||||
[:vendor [:name :id]]
|
||||
[:accounts [:id :amount :location [:account [:name :id :location :numeric-code]]]]
|
||||
|
||||
@@ -59,6 +59,12 @@
|
||||
(fn [db]
|
||||
(::table-params db)))
|
||||
|
||||
(defn lock-icon []
|
||||
[:div {:style {:position "absolute" :width "1em" :height "1em" :left "-1.25rem" :background-color "#E0E0E0" :padding "5px" :box-sizing "content-box" :border-radius "999px" :display "flex" :justify-content "center" :align-content "center" :text-align "center"}}
|
||||
[:div
|
||||
[:i.fa.fa-lock {:style {:color "#333"}}]]])
|
||||
|
||||
|
||||
(defn table [{:keys [data-page check-boxes? action-buttons]}]
|
||||
(let [selected-client @(re-frame/subscribe [::subs/client])
|
||||
{:keys [data params]} @(re-frame/subscribe [::data-page/page data-page])
|
||||
@@ -85,9 +91,16 @@
|
||||
^{:key id}
|
||||
[grid/row {:class (:class i) :id id :entity i}
|
||||
(when-not selected-client
|
||||
[grid/cell {} (:name client)])
|
||||
[grid/cell {:style {:overflow "visible" :position "relative" }}
|
||||
|
||||
(when (:is-locked i)
|
||||
[lock-icon])
|
||||
(:name client)])
|
||||
#_[:td description-original]
|
||||
[grid/cell {}
|
||||
|
||||
(when (and selected-client (:is-locked i))
|
||||
[lock-icon])
|
||||
(:name bank-account)]
|
||||
[grid/cell {} (cond vendor
|
||||
(:name vendor)
|
||||
@@ -130,7 +143,7 @@
|
||||
[:td (date->str (:date payment) pretty)]
|
||||
[:td
|
||||
[buttons/fa-icon {:icon "fa-external-link"
|
||||
:href (str (bidi/path-for ssr-routes/only-routes ::payment-route/page)
|
||||
:href (str (bidi/path-for ssr-routes/only-routes ::payment-route/all-page)
|
||||
"?"
|
||||
(url/map->query {:exact-match-id (:id payment)}))}]]])
|
||||
(when expected-deposit
|
||||
|
||||
Reference in New Issue
Block a user