diff --git a/iol_ion/src/iol_ion/tx/upsert_entity.clj b/iol_ion/src/iol_ion/tx/upsert_entity.clj index d2efee39..a6175e16 100644 --- a/iol_ion/src/iol_ion/tx/upsert_entity.clj +++ b/iol_ion/src/iol_ion/tx/upsert_entity.clj @@ -76,7 +76,7 @@ ;; reset relationships if it's refs, and not a lookup (i.e., seq of maps, or empty seq) - (and (sequential? v) (= :db.type/tuple (ident->value-type a))) + (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) diff --git a/resources/functions.edn b/resources/functions.edn index 1e9ef5eb..3d7425eb 100644 --- a/resources/functions.edn +++ b/resources/functions.edn @@ -1 +1 @@ -[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (> new-outstanding-balance 0) :invoice-status/unpaid :invoice-status/paid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice))))] (if existing? [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (assert (every? :db/id vs) (format \"In order to reset attribute %s, every value must have :db/id\" a)) (let [ids (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) new-id-set (set (map :db/id vs)) retract-ids (filter (complement new-id-set) ids) {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [:db/retract e a i])) retract-ids)) (into (map (fn [i] [:db/add e a i]) new-rels)) (into (map (fn [i] [:upsert-entity i]) vs)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) retracts (filter (complement (set vs)) extant) new (filter (complement (set extant)) vs)] (-> [] (into (map (fn [i] [:db/retract e a i]) retracts)) (into (map (fn [i] [:db/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__94738# p2__94739#] (assoc p1__94738# (f p2__94739#) (fv p2__94739#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) invoice-id) credit-invoice? (< (:invoice/total entity 0.0) 0.0)] (when-not (or (not (:invoice/total entity)) (= true (:invoice/exclude-from-ledger entity)) (= :import-status/pending (:db/ident (:invoice/import-status entity))) (= :invoice-status/voided (:db/ident (:invoice/status entity))) (< -0.001 (:invoice/total entity) 0.001)) (-remove-nils #:journal-entry{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :journal-entry-line/location \"A\"} credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] (map-indexed (fn [i ea] (cond-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :journal-entry-line/location (or (:invoice-expense-account/location ea) \"HQ\")} credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) (:invoice/expense-accounts entity))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__94741#] (= :payment-status/cleared (:payment/status p1__94741#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) invoice-id (or (-> with-invoice :tempids (get (:db/id invoice))) (:db/id invoice)) journal-entry (invoice->journal-entry (:db-after with-invoice) invoice-id (:db/id invoice))] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]]]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [:db/id :journal-entry/date :journal-entry/client #:journal-entry{:line-items [:journal-entry-line/account :journal-entry-line/location :db/id :journal-entry-line/client+account+location+date]}])] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (calc-client+account+location+date [je jel] (do [(or (:db/id (:journal-entry/client je)) (:journal-entry/client je)) (or (:db/id (:journal-entry-line/account jel)) (:journal-entry-line/account jel)) (-> jel :journal-entry-line/location) (-> je :journal-entry/date)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) \"Must at least provide date when updating ledger\") (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (let [extant-entry (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)] (dc/pull db extant-read [:journal-entry/original-entity original-entity])) (when-let [external-id (:journal-entry/external-id ledger-entry)] (dc/pull db extant-read [:journal-entry/external-id external-id]))) extant-entry-exists? (:db/id extant-entry)] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__94745#] (-> p1__94745# (assoc :journal-entry-line/dirty true) (assoc :journal-entry-line/client+account+location+date (calc-client+account+location+date ledger-entry p1__94745#)))) lis)))))]] extant-entry-exists? (into (map (fn [li] {:db/id li, :journal-entry-line/dirty true}) (get-line-items-after db extant-entry))))))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}}] \ No newline at end of file +[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (> new-outstanding-balance 0) :invoice-status/unpaid :invoice-status/paid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice))))] (if existing? [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (assert (every? :db/id vs) (format \"In order to reset attribute %s, every value must have :db/id\" a)) (let [ids (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) new-id-set (set (map :db/id vs)) retract-ids (filter (complement new-id-set) ids) {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [:db/retract e a i])) retract-ids)) (into (map (fn [i] [:db/add e a i]) new-rels)) (into (map (fn [i] [:upsert-entity i]) vs)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) retracts (filter (complement (set vs)) extant) new (filter (complement (set extant)) vs)] (-> [] (into (map (fn [i] [:db/retract e a i]) retracts)) (into (map (fn [i] [:db/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__164280# p2__164281#] (assoc p1__164280# (f p2__164281#) (fv p2__164281#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) invoice-id) credit-invoice? (< (:invoice/total entity 0.0) 0.0)] (when-not (or (not (:invoice/total entity)) (= true (:invoice/exclude-from-ledger entity)) (= :import-status/pending (:db/ident (:invoice/import-status entity))) (= :invoice-status/voided (:db/ident (:invoice/status entity))) (< -0.001 (:invoice/total entity) 0.001)) (-remove-nils #:journal-entry{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :journal-entry-line/location \"A\"} credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] (map-indexed (fn [i ea] (cond-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :journal-entry-line/location (or (:invoice-expense-account/location ea) \"HQ\")} credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) (:invoice/expense-accounts entity))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__164283#] (= :payment-status/cleared (:payment/status p1__164283#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) invoice-id (or (-> with-invoice :tempids (get (:db/id invoice))) (:db/id invoice)) journal-entry (invoice->journal-entry (:db-after with-invoice) invoice-id (:db/id invoice))] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]]]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [:db/id :journal-entry/date :journal-entry/client #:journal-entry{:line-items [:journal-entry-line/account :journal-entry-line/location :db/id :journal-entry-line/client+account+location+date]}])] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (calc-client+account+location+date [je jel] (do [(or (:db/id (:journal-entry/client je)) (:journal-entry/client je)) (or (:db/id (:journal-entry-line/account jel)) (:journal-entry-line/account jel)) (-> jel :journal-entry-line/location) (-> je :journal-entry/date)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) (format \"Must at least provide date when updating ledger: %s\" (pr-str ledger-entry))) (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (let [extant-entry (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)] (dc/pull db extant-read [:journal-entry/original-entity original-entity])) (when-let [external-id (:journal-entry/external-id ledger-entry)] (dc/pull db extant-read [:journal-entry/external-id external-id]))) extant-entry-exists? (:db/id extant-entry)] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__164287#] (-> p1__164287# (assoc :journal-entry-line/dirty true) (assoc :journal-entry-line/client+account+location+date (calc-client+account+location+date ledger-entry p1__164287#)))) lis)))))]] extant-entry-exists? (into (map (fn [li] {:db/id li, :journal-entry-line/dirty true}) (get-line-items-after db extant-entry))))))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}}] \ No newline at end of file diff --git a/resources/public/output.css b/resources/public/output.css index ab9396c7..18cd16cb 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1236,6 +1236,10 @@ input:checked + .toggle-bg { margin-top: 1.25rem; } +.ml-auto { + margin-left: auto; +} + .block { display: block; } @@ -1520,6 +1524,10 @@ input:checked + .toggle-bg { flex-wrap: wrap; } +.content-center { + align-content: center; +} + .items-start { align-items: flex-start; } @@ -1573,6 +1581,11 @@ input:checked + .toggle-bg { column-gap: 0.5rem; } +.gap-x-4 { + -moz-column-gap: 1rem; + column-gap: 1rem; +} + .-space-x-px > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(-1px * var(--tw-space-x-reverse)); @@ -1632,6 +1645,10 @@ input:checked + .toggle-bg { border-color: rgb(243 244 246 / var(--tw-divide-opacity)); } +.justify-self-end { + justify-self: end; +} + .overflow-auto { overflow: auto; } @@ -1870,6 +1887,11 @@ input:checked + .toggle-bg { background-color: rgb(253 246 178 / var(--tw-bg-opacity)); } +.bg-blue-50 { + --tw-bg-opacity: 1; + background-color: rgb(230 245 253 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -2014,6 +2036,10 @@ input:checked + .toggle-bg { text-align: center; } +.text-right { + text-align: right; +} + .text-2xl { font-size: 1.5rem; line-height: 2rem; @@ -2150,6 +2176,11 @@ input:checked + .toggle-bg { color: rgb(114 59 19 / var(--tw-text-opacity)); } +.text-blue-400 { + --tw-text-opacity: 1; + color: rgb(51 176 238 / var(--tw-text-opacity)); +} + .underline { text-decoration-line: underline; } @@ -2736,6 +2767,11 @@ input:checked + .toggle-bg { color: rgb(250 202 21 / var(--tw-text-opacity)); } +:is(.dark .dark\:text-blue-400) { + --tw-text-opacity: 1; + color: rgb(51 176 238 / var(--tw-text-opacity)); +} + :is(.dark .dark\:placeholder-gray-400)::-moz-placeholder { --tw-placeholder-opacity: 1; color: rgb(156 163 175 / var(--tw-placeholder-opacity)); diff --git a/resources/schema.edn b/resources/schema.edn index d912ae3f..89664350 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -2127,4 +2127,67 @@ :db/doc "A url to the pdf on s3" :db/valueType :db.type/string :db/cardinality :db.cardinality/one} - ] + + {:db/ident :textract-invoice/total + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/double] + :db/cardinality :db.cardinality/one} + + {:db/ident :textract-invoice/total-options + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/double] + :db/cardinality :db.cardinality/many} + + {:db/ident :textract-invoice/account-number + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/ref] + :db/cardinality :db.cardinality/one} + + {:db/ident :textract-invoice/account-number-options + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/ref] + :db/cardinality :db.cardinality/many} + + {:db/ident :textract-invoice/customer-identifier + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/ref] + :db/cardinality :db.cardinality/one} + + {:db/ident :textract-invoice/customer-identifier-options + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/ref] + :db/cardinality :db.cardinality/many} + + {:db/ident :textract-invoice/vendor-name + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/ref] + :db/cardinality :db.cardinality/one} + + {:db/ident :textract-invoice/vendor-name-options + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/ref] + :db/cardinality :db.cardinality/many} + + {:db/ident :textract-invoice/date + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/instant] + :db/cardinality :db.cardinality/one} + + {:db/ident :textract-invoice/date-options + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/instant] + :db/cardinality :db.cardinality/many} + + {:db/ident :textract-invoice/invoice-number + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/string] + :db/cardinality :db.cardinality/one} + + {:db/ident :textract-invoice/invoice-number-options + :db/valueType :db.type/tuple + :db/tupleTypes [:db.type/string :db.type/string] + :db/cardinality :db.cardinality/many} + + {:db/ident :textract-invoice/invoice + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one}] diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index 0225dfe9..fe967446 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -21,6 +21,8 @@ (def modal-card dialog/modal-card-) (def text-input inputs/text-input-) +(def money-input inputs/money-input-) +(def date-input inputs/date-input-) (def select inputs/select-) (def field inputs/field-) diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index b66049f9..e781045a 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -17,6 +17,23 @@ :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") ]) +(defn money-input- [params] + [:input + (-> params + (update + :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 text-right" + ) + (assoc :type "number" + :step "0.01")) + ]) + +(defn date-input- [params] + [:input + (-> params + (update + :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") + (assoc :type "date"))]) + (defn field- [params & rest] (into [:div diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index a7f6f4bf..426d1ee4 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -13,7 +13,22 @@ (left-aside- {:nav nav :page-specific page-specific}) [:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content " - "_" (hiccup/raw "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")} + "_" (hiccup/raw " + on notification 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" + )} + [:div#notification-holder.hidden + [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg + [:div.relative + [:button.absolute.right-2.top-2.w-6.h-6.z-50.text-blue-400 + {"_" (hiccup/raw "on click add .hidden to #notification-holder")} + svg/filled-x]] + + [:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-blue-800.bg-blue-50.dark:bg-gray-800.dark:text-blue-400.border-blue-300.rounded-lg.border.transition-all.duration-500.fade-in.slide-up.max-h-96 + + [:div {:class "p-4 text-lg w-full" :role "alert"} + [:div.text-sm + [:pre#notification-details.text-xs]]]]]] [:div#error-holder.hidden [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg [:div.relative diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 7313810b..6a337daa 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -43,6 +43,8 @@ :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-job (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/job-progress)) + :invoice-glimpse-create (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create)) + :invoice-glimpse-update-job (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-job)) :transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page)) :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table)) :transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows)) diff --git a/src/clj/auto_ap/ssr/invoice/glimpse.clj b/src/clj/auto_ap/ssr/invoice/glimpse.clj index e4584ba4..397f50af 100644 --- a/src/clj/auto_ap/ssr/invoice/glimpse.clj +++ b/src/clj/auto_ap/ssr/invoice/glimpse.clj @@ -14,13 +14,16 @@ [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]]) + [iol-ion.tx :refer [random-tempid]] + [auto-ap.client-routes :as client-routes]) (:import (java.util UUID))) @@ -33,99 +36,89 @@ (map (fn [sf] (-> sf (update :label-detection dissoc :geometry) - (update :value-detection dissoc :geometry)))) - #_(group-by (fn [sf] - [(get-in sf ["Type" "Text"]) - (get-in sf ["LabelDetection" "Text"])] - )))) + (update :value-detection dissoc :geometry)))))) + (defn stack-rank [valid-values field-descriptors] (->> field-descriptors (filter (comp valid-values :text :type)) (sort-by #(* (-> % :type :confidence) (-> % :value-detection :confidence))) (reverse) - (map (comp :text :value-detection)) - (reduce - (fn [[result seen?] new] - (if (seen? new) - [result seen?] - [(conj result new) (conj seen? new)])) - [[] #{}]) - first)) - -(defn textract->textract-invoice [tx] - (let [lookup (lookup tx) - total-options (stack-rank #{"TOTAL"} lookup) - account-number-options (stack-rank #{"CUSTOMER_NUMBER"} lookup) - customer-identifier-options (stack-rank #{"RECEIVER_NAME"} lookup) - vendor-name-options (stack-rank #{"VENDOR_NAME"} lookup) - date-options (stack-rank #{"ORDER_DATE"} lookup) - invoice-number-options (stack-rank #{"INVOICE_RECEIPT_ID"} lookup) - ] - #:textract-invoice - {:total (first total-options) - :total-options total-options - :account-number (first account-number-options) - :account-number-options account-number-options - :customer-identifier (first customer-identifier-options) - :customer-identifier-options customer-identifier-options - :vendor-name (first vendor-name-options) - :vendor-name-options (rest vendor-name-options) - :date (first date-options) - :date-options date-options - :invoice-number (first invoice-number-options) - :invoice-number-options invoice-number-options - })) + (map (comp :text :value-detection)))) (defn clean-customer [c] (clojure.string/replace c #"\W+" " ")) -(defn coalesced->invoice [i] - (mu/with-context {:inference i} - (let [vendor-id (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", (:best (:vendor-name i))) "fields" "score, *"}) - (filter (fn [d] (> (:score d) 4.0))) - (map (comp #(Long/parseLong %) :id)) - first) - account-number (:best (:account-number i)) - customer-identifier (:best (:customer-identifier i)) - client-id (or - (when (not-empty account-number) - (:db/id (d-clients/exact-match (:best (:account-number i))))) - (when (:best (:customer-identifier i)) - (->> (solr/query solr/impl "clients" {"query" (format "name:(%s) ", (clean-customer customer-identifier)) "fields" "score, *"}) - #_(filter (fn [d] (> (:score d) 4.0))) - (map (comp #(Long/parseLong %) :id)) - first))) - location (when client-id - (->> (dc/pull (dc/db conn) '[:client/locations] client-id) - :client/locations - first)) - invoice-number (:best (:invoice-number i)) - total (Double/parseDouble (some->> i - :total - :best - (re-find #"([0-9.\-]+)") - second) ) - date (or (atime/parse (:best (:date i)) "MM/dd/yyyy") - (atime/parse (:best (:date i)) "MM/dd/yy"))] - (when-not vendor-id - (alog/warn ::cant-find-vendor - :search-results (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", (:best (:vendor-name i))) "fields" "score, *"}) - :vendor-name (:vendor-name i))) - (when-not client-id - (alog/warn ::cant-find-customer)) - (when (and client-id date invoice-number vendor-id total) - {:db/id (random-tempid) - :invoice/client client-id - :invoice/client-identifier (or account-number customer-identifier) - :invoice/vendor vendor-id - :invoice/invoice-number invoice-number - :invoice/total total - :invoice/date date - :invoice/location location - :invoice/import-status :import-status/pending - :invoice/outstanding-balance total - :invoice/status :invoice-status/unpaid})))) +(defn deduplicate [xs] + (first + (reduce + (fn [[so-far seen-parsed?] [raw parsed]] + (if (seen-parsed? parsed) + [so-far seen-parsed?] + [(conj so-far [raw parsed]) + (conj seen-parsed? parsed)])) + [[] #{}] + xs))) + +(defn textract->textract-invoice [job-id tx] + (let [lookup (lookup tx) + total-options (->> (stack-rank #{"AMOUNT_DUE"} lookup) + (map (fn [t] + [t (some->> t + (re-find #"([0-9.\-]+)") + second + Double/parseDouble)])) + (concat (->> (stack-rank #{"TOTAL"} lookup) + (map (fn [t] + [t (some->> t + (re-find #"([0-9.\-]+)") + second + Double/parseDouble)])))) + (deduplicate)) + customer-identifier-options (->> (stack-rank #{"CUSTOMER_NUMBER"} lookup) + (map (fn [t] + [t (:db/id (d-clients/exact-match t))])) + (filter second) + (concat (->> (stack-rank #{"RECEIVER_NAME"} lookup) + (map (fn [t] + [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)])))) + deduplicate) + vendor-name-options (->> (stack-rank #{"VENDOR_NAME"} lookup) + (map (fn [t] + [t (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", t) "fields" "score, *"}) + (filter (fn [d] (> (:score d) 4.0))) + (map (comp #(Long/parseLong %) :id)) + first)])) + (deduplicate)) + date-options (->> (stack-rank #{"ORDER_DATE" "DELIVERY_DATE"} lookup) + (map (fn [t] + [t (or (some-> (and (re-find #"\d{1,2}\/\d{1,2}/\d{4,4}" t) (atime/parse t "MM/dd/yyyy")) + (coerce/to-date)) + (some-> (and (re-find #"\d{1,2}\/\d{1,2}/\d{2,2}" t) (atime/parse t "MM/dd/yy")) + (coerce/to-date)))])) + (deduplicate)) + invoice-number-options (->> (stack-rank #{"INVOICE_RECEIPT_ID" "PO_NUMBER"} lookup) + (map (fn [t] + [t t])) + (deduplicate))] + #:textract-invoice + {:db/id [:textract-invoice/job-id job-id] + :textract-status "SUCCEEDED" + :total (first total-options) + :total-options (seq total-options) + :customer-identifier (first customer-identifier-options) + :customer-identifier-options (seq customer-identifier-options) + :vendor-name (first vendor-name-options) + :vendor-name-options (seq vendor-name-options) + :date (first date-options) + :date-options (seq date-options) + :invoice-number (first invoice-number-options) + :invoice-number-options (seq invoice-number-options)})) + + (defn upload-form* [] [:div @@ -145,80 +138,138 @@ invoice_dropzone = new Dropzone(\"#invoice\", { disablePreviews: true }); ")]]) +(defn customer-identifier-id->customer-identifier-client [[ci client]] + (when client + (let [real-client (dc/pull (dc/db conn) + [:client/name :db/id] + client)] + [ci [(:db/id real-client) (:client/name real-client)]]))) + +(defn vendor-name-tuple->vendor-tuple [[vn vendor]] + (when vendor + (let [real-vendor (dc/pull (dc/db conn) + [:vendor/name :db/id] + vendor)] + [vn [(:db/id real-vendor) (:vendor/name real-vendor)]]))) + +(defn get-job [job-id] + (-> (dc/pull (dc/db conn) '[*] [:textract-invoice/job-id job-id]) + (update :textract-invoice/customer-identifier customer-identifier-id->customer-identifier-client) + (update :textract-invoice/customer-identifier-options #(map customer-identifier-id->customer-identifier-client %) ) + (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 [job-id] (let [{:keys [:db/id :textract-invoice/textract-status]} (dc/pull (dc/db conn) '[:db/id :textract-invoice/textract-status] [:textract-invoice/job-id job-id])] (when (= "IN_PROGRESS" textract-status) - (let [result (textract/get-expense-analysis {:job-id job-id})] - @(dc/transact conn [{:db/id id :textract-invoice/textract-status (:job-status result)}]))) - (dc/pull (dc/db conn) '[*] [:textract-invoice/job-id job-id]))) + (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 job-id result)]]) + :else + @(dc/transact conn [{:db/id id :textract-invoice/textract-status new-status}])))) + (get-job job-id))) -(defn pill-list* [{:keys [selected options class]}] + +(defn pill-list* [{:keys [selected options class ->text ->value job-id field]}] (let [options (->> options (filter (complement #{selected})) - (map (fn [x] [:div.shrink (com/pill {:color :secondary} (com/link {:href "#"} x))]) ))] + (map (fn [x] + [:div.shrink (com/pill {:color :secondary} (com/link {:hx-patch (str (bidi/path-for ssr-routes/only-routes + :invoice-glimpse-update-job + :job-id job-id) + "?" + (url/map->query {field (if ->value + (->value x) + (->text x))})) + :hx-target "closest form" + :href "#"} (->text x)))]) ))] (when (seq options) [:div.col-span-6.col-start-1.text-xs "Alternates: " [:div.flex.gap-2.flex-wrap {:class class} options]]))) -(defn textract->invoice-form* [job-id] - (let [coalesced (-> (textract/get-expense-analysis {:job-id job-id}) - (textract->textract-invoice)) - #_#_candidate-invoice (-> coalesced - (coalesced->invoice))] - [:form - [:div.grid.grid-cols-6.gap-4 - [:div.col-span-6 - (com/field {:label "Client"} - (com/text-input {:name (path->name [:invoice/client]) - :value (:textract-invoice/customer-identifier coalesced) - :placeholder "Client" - :disabled true - :autofocus true}))] - (pill-list* {:selected (:textract-invoice/customer-identifier coalesced) - :options (:textract-invoice/customer-identifier-options coalesced) - :class "flex-col"}) - - [:div.col-span-6 - (com/field {:label "Vendor"} - (com/text-input {:name (path->name [:invoice/vendor]) - :value (:textract-invoice/vendor-name coalesced) - :placeholder "Vendor" - :disabled true - :autofocus true}))] - (pill-list* {:selected (:textract-invoice/vendor-name coalesced) - :options (:textract-invoice/vendor-name-options coalesced) - :class "flex-row"}) - - [:div.col-span-3 - (com/field {:label "Date"} - (com/text-input {:name (path->name [:invoice/date]) - :value (:textract-invoice/date coalesced) - :placeholder "Date" - :disabled true - :autofocus true}))] - (pill-list* {:selected (:textract-invoice/date coalesced) - :options (:textract-invoice/date-options coalesced)}) - [:div.col-span-2.col-start-1 - (com/field {:label "Total"} - (com/text-input {:name (path->name [:invoice/total]) - :value (:textract-invoice/total coalesced) - :placeholder "Total" - :disabled true - :autofocus true}))] - (pill-list* {:selected (:textract-invoice/total coalesced) - :options (:textract-invoice/total-options coalesced)}) - - [:div.col-span-2.col-start-1 - (com/field {:label "Invoice Number"} - (com/text-input {:name (path->name [:invoice/invoice-number]) - :value (:textract-invoice/invoice-number coalesced) - :placeholder "Invoice Number" - :disabled true - :autofocus true}))] - (pill-list* {:selected (:textract-invoice/invoice-number coalesced) - :options (:textract-invoice/invoice-number-options coalesced)})]])) +(defn textract->invoice-form* [textract-invoice] + [:form {:hx-post (bidi/path-for ssr-routes/only-routes + :invoice-glimpse-create + :job-id (:textract-invoice/job-id textract-invoice))} + [:div.grid.grid-cols-6.gap-4.mb-4 + [:div.col-span-6 + (com/field {:label "Client"} + (com/text-input {:name (path->name [:invoice/client]) + :value (-> textract-invoice :textract-invoice/customer-identifier second second) + :placeholder "Client" + :disabled true + :autofocus true}))] + (pill-list* {:selected (:textract-invoice/customer-identifier textract-invoice) + :options (:textract-invoice/customer-identifier-options textract-invoice) + :job-id (:textract-invoice/job-id textract-invoice) + :class "flex-col" + :field "client" + :->text (fn [[customer-identifier [id client-name]]] + (format "%s (%s)" client-name customer-identifier)) + :->value (fn [[client-identifier [id client-name]]] + id)}) + [:div.col-span-6 + (com/field {:label "Vendor"} + (com/text-input {:name (path->name [:invoice/vendor]) + :value (-> textract-invoice :textract-invoice/vendor-name second second) + :disabled true + :placeholder "Vendor"}))] + (pill-list* {:selected (:textract-invoice/vendor-name textract-invoice) + :options (:textract-invoice/vendor-name-options textract-invoice) + :job-id (:textract-invoice/job-id textract-invoice) + :class "flex-row" + :field "vendor" + :->text (fn [[vendor-identifier [id vendor-name]]] + (format "%s (%s)" vendor-name vendor-identifier)) + :->value (fn [[vendor-identifier [id vendor-name]]] + id)}) + [:div.col-span-3 + (com/field {:label "Date"} + (com/date-input {:name "date" + :value (-> textract-invoice + :textract-invoice/date + second + (coerce/to-date-time) + (atime/unparse-local atime/iso-date)) + :placeholder "Date"}))] + (pill-list* {:selected (:textract-invoice/date textract-invoice) + :options (:textract-invoice/date-options textract-invoice) + :job-id (:textract-invoice/job-id textract-invoice) + :field "date" + :->text (fn [[_ date]] + (-> date + (coerce/to-date-time) + (atime/unparse-local atime/iso-date)))}) + [:div.col-span-2.col-start-1 + (com/field {:label "Total"} + (com/money-input {:name "total" + :value (-> textract-invoice + :textract-invoice/total + second) + :placeholder "Total"}))] + (pill-list* {:selected (:textract-invoice/total textract-invoice) + :options (:textract-invoice/total-options textract-invoice) + :job-id (:textract-invoice/job-id textract-invoice) + :field "total" + :->text (fn [[_ amount]] + (str amount))}) + [:div.col-span-2.col-start-1 + (com/field {:label "Invoice Number"} + (com/text-input {:name "invoice-number" + :value (-> textract-invoice + :textract-invoice/invoice-number + first) + :placeholder "Invoice Number"}))] + (pill-list* {:selected (:textract-invoice/invoice-number textract-invoice) + :field "invoice-number" + :job-id (:textract-invoice/job-id textract-invoice) + :options (:textract-invoice/invoice-number-options textract-invoice) + :->text (fn [[_ invoice-number]] + (str invoice-number))})] + (com/button {:color :primary} "Save")]) (defn job-progress* [job-id] (when (pull-id (dc/db conn) [:textract-invoice/job-id job-id]) @@ -227,17 +278,14 @@ invoice_dropzone = new Dropzone(\"#invoice\", { (= "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 {:hx-get (str (bidi/path-for ssr-routes/only-routes - :invoice-glimpse-job) - "?" (url/map->query {:job-id job-id})) + :invoice-glimpse-job + :job-id (:textract-invoice/job-id textract-invoice))) :hx-trigger "load delay:5s" :hx-swap "outerHTML"} "Analyzing job " (subs (:textract-invoice/job-id textract-invoice) 0 8) "..."] - (= "SUCCEEDED" (:textract-invoice/textract-status textract-invoice)) [:div.px-4 - [:a.mb-2 {:href (bidi/path-for ssr-routes/only-routes - :invoice-glimpse)} - (com/button {:color :secondary} "New import")] + [:div.flex.flex-row.space-x-4 [:div {:style {:width "805"}} (com/card {} @@ -245,22 +293,21 @@ invoice_dropzone = new Dropzone(\"#invoice\", { [:div {:class "basis-1/4"} (com/card {} [:div.p-4 - (textract->invoice-form* job-id)])]]])))) - -(defn job-progress [request] - (html-response (job-progress* (get (:query-params request) "job-id")))) + (textract->invoice-form* textract-invoice)])]]])))) (defn page* [job-id] - [:div.mt-4 + [:div#invoice-glimpse-content.mt-4 (com/card {} [:div.px-4.py-3.space-y-4.flex.flex-col - [:h1.text-2xl.mb-3.font-bold "Invoice Glimpse"] - [:p.text-sm.italic "Import your invoices with the power of AI."] - [:div.flex.flex-row.space-x-4 (com/pill {:color :primary} "Beta") - [:span "Note: This upload is expirimental. Please only use PDFs with a single invoice in them."]] + [:div.flex.gap-x-4 [:h1.text-2xl.font-bold "Invoice Glimpse"] [:div (com/pill {:color :primary} "Beta")] + (when job-id + [:div.ml-auto [:a.mb-2 {:href (bidi/path-for ssr-routes/only-routes + :invoice-glimpse)} + (com/button {:color :secondary} "New glimpse")]])] + [:p.text-sm.italic "Import your invoices with the power of AI. Please only use PDFs with a single invoice in them."] + (when job-id (job-progress* job-id)) - (when-not job-id (upload-form*))])]) @@ -272,6 +319,39 @@ invoice_dropzone = new Dropzone(\"#invoice\", { @(dc/transact conn [textract-invoice]) textract-invoice)) +(defn textract-invoice->invoice [textract-invoice] + (mu/with-context {:textract-invoice textract-invoice} + (let [[_ [vendor-id]] (:textract-invoice/vendor-name textract-invoice) + [_ [client-id]] (:textract-invoice/customer-identifier textract-invoice) + [_ total] (:textract-invoice/total textract-invoice) + [_ date] (:textract-invoice/date textract-invoice) + [_ invoice-number] (:textract-invoice/invoice-number textract-invoice) + location (when client-id + (->> (dc/pull (dc/db conn) '[:client/locations] client-id) + :client/locations + first))] + (when (and client-id date invoice-number vendor-id total) + {:db/id (random-tempid) + :invoice/client client-id + :invoice/client-identifier (first (:textract-invoice/customer-identifier textract-invoice)) + :invoice/vendor vendor-id + :invoice/invoice-number invoice-number + :invoice/total total + :invoice/date date + :invoice/location location + :invoice/import-status :import-status/imported + :invoice/outstanding-balance total + :invoice/status :invoice-status/unpaid})))) + +(defn update-textract-invoice [job-id {:strs [date total invoice-number client vendor]}] + @(dc/transact-async conn [[:upsert-entity (cond-> {:db/id [:textract-invoice/job-id job-id]} + date (assoc :textract-invoice/date [date (coerce/to-date (atime/parse date atime/iso-date))]) + total (assoc :textract-invoice/total [total (Double/parseDouble total)]) + invoice-number (assoc :textract-invoice/invoice-number [invoice-number invoice-number]) + client (assoc :textract-invoice/customer-identifier [(pull-attr (dc/db conn) :client/name (Long/parseLong client)) (Long/parseLong client)]) + vendor (assoc :textract-invoice/vendor-name [(pull-attr (dc/db conn) :vendor/name (Long/parseLong vendor)) (Long/parseLong vendor)]))]]) + (get-job job-id)) + (defn upload [{:keys [identity] :as request}] (let [file (or (get (:params request) :file) (get (:params request) "file"))] @@ -287,17 +367,41 @@ invoice_dropzone = new Dropzone(\"#invoice\", { {:content-type "application/pdf" :content-length (.length (:tempfile file))})) textract-invoice (begin-textract-file s3-location)] - {:headers {"Location" (str (bidi/path-for ssr-routes/only-routes - :invoice-glimpse) - "?" (url/map->query {:job-id (:textract-invoice/job-id textract-invoice)}))} + :invoice-glimpse-job + :job-id (:textract-invoice/job-id textract-invoice)))} :status 302}) (catch Exception e (alog/error ::cant-begin-textract :error e) (html-response [:div (.getMessage e)])))))) +(defn update-job [{:as request}] + (let [current-job (update-textract-invoice (:job-id (:route-params request)) (:query-params request))] + (html-response (textract->invoice-form* current-job)))) + +(defn create [request] + (let [current-job (update-textract-invoice (:job-id (:route-params request)) (:form-params request)) + new-invoice (textract-invoice->invoice current-job) + new-invoice-id (get-in @(dc/transact conn [[:propose-invoice new-invoice]]) + [:tempids (:db/id new-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) + :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 " + (com/link {:href (str (bidi/path-for client-routes/routes + :invoices) + "?exact-match-id=" + new-invoice-id)} + (format "invoice %s" (:invoice/invoice-number new-invoice))) + "."]))})}) + (html-response [:div "This invoice already exists."] + :status 400)))) + (defn page [{:keys [matched-route request-method] :as request}] (mu/log ::method :method request-method) @@ -318,7 +422,11 @@ invoice_dropzone = new Dropzone(\"#invoice\", { [:a {:href (bidi/path-for ssr-routes/only-routes :invoice-glimpse)} "Glimpse"]) - (page* (get (:query-params request) "job-id"))) + (page* (:job-id (:route-params request)))) "Invoice Glimpse")) +(defn job-progress [request] + (if (get-in request [:headers "hx-request"]) + (html-response (job-progress* (:job-id (:route-params request)))) + (page request))) diff --git a/src/clj/auto_ap/yodlee/core2.clj b/src/clj/auto_ap/yodlee/core2.clj index 9e05bdeb..072dde91 100644 --- a/src/clj/auto_ap/yodlee/core2.clj +++ b/src/clj/auto_ap/yodlee/core2.clj @@ -360,17 +360,19 @@ (reauthenticate client-code pa data) (refresh-provider-account client-code pa)) -(defn force-pull-all [] - (doseq [[client-code yodlee-provider-account] (seq (dc/q '[:find ?cd ?pai - :where [?c :client/code ?cd] - [?yap :yodlee-provider-account/client ?c] - [?yap :yodlee-provider-account/id ?pai]] - (dc/db conn)))] +(defn force-pull-all + ([] (force-pull-all (seq (dc/q '[:find ?cd ?pai + :where [?c :client/code ?cd] + [?yap :yodlee-provider-account/client ?c] + [?yap :yodlee-provider-account/id ?pai]] + (dc/db conn))))) + ([options] + (doseq [[client-code yodlee-provider-account] options] - (println "Trying " client-code "account" yodlee-provider-account) - (clojure.pprint/pprint (reauthenticate client-code yodlee-provider-account {})) - (println "waiting") - (Thread/sleep 15000) - (println "refreshing") - (clojure.pprint/pprint (refresh-provider-account client-code yodlee-provider-account)))) + (println "Trying " client-code "account" yodlee-provider-account) + (clojure.pprint/pprint (reauthenticate client-code yodlee-provider-account {})) + (println "waiting") + (Thread/sleep 15000) + (println "refreshing") + (clojure.pprint/pprint (refresh-provider-account client-code yodlee-provider-account))))) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 6694c2fb..d0e7ed0b 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -4,7 +4,9 @@ "search" :search "invoice" {"/glimpse" {"" {:get :invoice-glimpse :post :invoice-glimpse-upload} - "/job" {:get :invoice-glimpse-job}}} + "/job" {["/" [#"\w+" :job-id]] {:get :invoice-glimpse-job + "/create" {:post :invoice-glimpse-create} + "/update" {:patch :invoice-glimpse-update-job}}}}} "admin" {"/history" {"" :admin-history "/" :admin-history #"/search/?" :admin-history-search