This commit is contained in:
2024-05-26 20:22:13 -07:00
111 changed files with 10708 additions and 5015 deletions

View File

@@ -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)))

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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 [])))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))))

View File

@@ -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}))

View File

@@ -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)]

View 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"])

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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 [& _]

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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))}])

View File

@@ -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

View File

@@ -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)))))

View File

@@ -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")))
)

View File

@@ -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

View File

@@ -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]

View File

@@ -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))])

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)))))

View 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]}]]]])))))

View 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]] ])))))

View File

@@ -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

View File

@@ -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 }")

View File

@@ -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))]])

View File

@@ -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

View File

@@ -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})]]]])

View File

@@ -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

View File

@@ -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)))

View 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)))))

View File

@@ -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}]

View File

@@ -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")))))
)

View File

@@ -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]}])

View File

@@ -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)))

View File

@@ -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

View File

@@ -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))))

View File

@@ -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)})

View File

@@ -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"}]

View File

@@ -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

View File

@@ -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))))))

View 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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)}))}}))))

View File

@@ -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})

View 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})

View File

@@ -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}

View File

@@ -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

View File

@@ -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))

View File

@@ -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)]

View File

@@ -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

View File

@@ -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?])

View File

@@ -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]]]]

View File

@@ -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