diff --git a/src/clj/auto_ap/datomic/checks.clj b/src/clj/auto_ap/datomic/checks.clj index f1e5a7b1..393c80eb 100644 --- a/src/clj/auto_ap/datomic/checks.clj +++ b/src/clj/auto_ap/datomic/checks.clj @@ -22,8 +22,9 @@ (def default-read '(pull ?e [* {:invoice-payment/_payment [* {:invoice-payment/invoice [*]}]} - {:payment/client [:client/name :db/id]} - {:payment/vendor [:vendor/name :db/id]} + {:payment/client [:client/name :db/id :client/code]} + {:payment/bank-account [*]} + {:payment/vendor [:vendor/name :vendor/default-expense-account :db/id {:vendor/primary-contact [*]} {:vendor/address [*]}]} {:payment/status [:db/ident]} {:payment/type [:db/ident]}])) diff --git a/src/clj/auto_ap/datomic/invoices.clj b/src/clj/auto_ap/datomic/invoices.clj index 58932a42..c4de6557 100644 --- a/src/clj/auto_ap/datomic/invoices.clj +++ b/src/clj/auto_ap/datomic/invoices.clj @@ -1,7 +1,8 @@ (ns auto-ap.datomic.invoices (:require [datomic.api :as d] - [auto-ap.datomic :refer [uri]] + [auto-ap.datomic :refer [uri remove-nils]] [auto-ap.graphql.utils :refer [limited-clients]] + [auto-ap.parse :as parse] [clj-time.coerce :as c] [clojure.set :refer [rename-keys]] [clojure.string :as str])) @@ -15,8 +16,8 @@ (reduce #(update-in %1 [:query :where] conj %2) query rest))) (def default-read '(pull ?e [* - {:invoice/client [:client/name :db/id :client/locations]} - {:invoice/vendor [:vendor/name :db/id]} + {:invoice/client [:client/name :db/id :client/locations :client/code]} + {:invoice/vendor [* {:vendor/address [*]}]} {:invoice/status [:db/ident]} {:invoice-payment/_invoice [* {:invoice-payment/payment [* {:payment/status [*]} {:payment/bank-account [*]} @@ -49,6 +50,8 @@ (:original-id args) (add-arg '?original-id (cond-> (:original-id args) (string? (:original-id args)) Long/parseLong ) '[?e :invoice/client ?c] '[?c :client/original-id ?original-id]) + (:import-status args) (add-arg '?import-status (keyword "import-status" (:import-status args)) + '[?e :invoice/import-status ?import-status]) (:status args) (add-arg '?status (keyword "invoice-status" (:status args)) '[?e :invoice/status ?status]) (:vendor-id args) (add-arg '?vendor-id (:vendor-id args) @@ -127,3 +130,5 @@ (map first) (<-datomic))) + + diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index 7f0441bc..94542de6 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -3,6 +3,7 @@ [datomic.api :as d] [auto-ap.datomic.migrate.add-client-codes :refer [add-client-codes]] [auto-ap.datomic.migrate.add-bank-account-codes :refer [add-bank-account-codes]] + [auto-ap.datomic.migrate.invoice-converter :refer [add-import-status-existing-invoices]] [clojure.java.io :as io] [io.rkn.conformity :as c]) (:import [datomic Util]) @@ -53,6 +54,14 @@ :auto-ap/add-client-codes {:txes-fn 'auto-ap.datomic.migrate.add-client-codes/add-client-codes :requires [:auto-ap/migrate-transactions]} :auto-ap/add-bank-account-codes-schema {:txes-fn 'auto-ap.datomic.migrate.add-bank-account-codes/add-bank-account-codes-schema :requires [:auto-ap/add-client-codes]} :auto-ap/add-bank-account-codes {:txes-fn 'auto-ap.datomic.migrate.add-bank-account-codes/add-bank-account-codes :requires [:auto-ap/add-bank-account-codes-schema]} + :auto-ap/add-nick-the-greek {:txes [[{:client/name "Nick the Greek" :client/code "NGAK" :client/locations ["MH"] :client/bank-accounts [{:bank-account/code "NGAK-0" :bank-account/type :bank-account-type/cash :bank-account/name "Cash"}]}]] :requires [:auto-ap/add-bank-account-codes]} + :auto-ap/rename-codes-1 {:txes-fn 'auto-ap.datomic.migrate.rename-codes/rename-codes-1 :requires [:auto-ap/add-nick-the-greek]} + :auto-ap/invoice-converter {:txes auto-ap.datomic.migrate.invoice-converter/add-matches :requires [:auto-ap/rename-codes-1]} + :auto-ap/starter {:txes auto-ap.datomic.migrate.invoice-converter/add-starter :requires [:auto-ap/invoice-converter]} + :auto-ap/add-default-location {:txes-fn 'auto-ap.datomic.migrate.invoice-converter/add-default-location :requires [:auto-ap/invoice-converter]} + :auto-ap/add-default-location-2 {:txes-fn 'auto-ap.datomic.migrate.invoice-converter/add-default-location-2 :requires [:auto-ap/add-default-location]} + :auto-ap/add-import-status {:txes auto-ap.datomic.migrate.invoice-converter/add-import-status :requires [:auto-ap/add-default-location-2]} + :auto-ap/add-import-status-existing-invoices {:txes-fn 'auto-ap.datomic.migrate.invoice-converter/add-import-status-existing-invoices :requires [:auto-ap/add-import-status]} }] (println "Conforming database...") (println (c/ensure-conforms conn norms-map)) diff --git a/src/clj/auto_ap/datomic/migrate/invoice_converter.clj b/src/clj/auto_ap/datomic/migrate/invoice_converter.clj new file mode 100644 index 00000000..96d5dc63 --- /dev/null +++ b/src/clj/auto_ap/datomic/migrate/invoice_converter.clj @@ -0,0 +1,58 @@ +(ns auto-ap.datomic.migrate.invoice-converter + (:require [datomic.api :as d])) + +(def add-matches + [[{:db/ident :client/matches + :db/valueType :db.type/string + :db/cardinality :db.cardinality/many + :db/doc "The strings that match the client"} + {:db/ident :client/location-matches + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/isComponent true + :db/doc "The mapping from string to location"} + + {:db/ident :location-match/matches + :db/valueType :db.type/string + :db/cardinality :db.cardinality/many + :db/doc "The strings that match the location"} + + {:db/ident :location-match/location + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "The location of the location match"} + ]]) + +(def add-starter + [[{:db/id [:client/code "CBC"] + :client/matches ["campbell brewing company"] + :client/location-matches [{:location-match/location "CB" + :location-match/matches ["campbell brewing company"]}]}]]) + +(defn add-default-location [conn] + [[{:db/ident :client/default-location + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "The default location if one can't be found"}]]) + +(defn add-default-location-2 [conn] + [[{:db/id [:client/code "CBC"] + :client/default-location "CB"}]]) + +(def add-import-status + [[{:db/ident :invoice/import-status + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "The status of importing the transaction"} + + {:db/ident :import-status/pending} + {:db/ident :import-status/imported}]]) + +(defn add-import-status-existing-invoices [conn] + (let [existing-invoices (->> (d/query {:query {:find ['?e] + :in ['$] + :where ['[?e :invoice/invoice-number]]} + :args [(d/db conn)]}))] + [(map (fn [i] {:db/id (first i) + :invoice/import-status :import-status/imported}) + existing-invoices)])) diff --git a/src/clj/auto_ap/datomic/migrate/rename_codes.clj b/src/clj/auto_ap/datomic/migrate/rename_codes.clj new file mode 100644 index 00000000..7226f5e7 --- /dev/null +++ b/src/clj/auto_ap/datomic/migrate/rename_codes.clj @@ -0,0 +1,41 @@ +(ns auto-ap.datomic.migrate.rename-codes + (:require [datomic.api :as d] + [auto-ap.datomic :refer [uri]] + [clojure.string :as str])) + +(defn rename [old-code new-code conn ] + (let [results (->> (d/query {:query {:find ['?e '?b '?bc] + :in ['$ '?old-code] + :where ['[?e :client/code ?old-code] + '[?e :client/bank-accounts ?b] + '[?b :bank-account/code ?bc]]} + :args [(d/db conn) old-code]}) + (group-by first)) + + #_#_[[id]] results] + (for [[id change] results + [_ ba-id ba-code] change] + [{:db/id id + :client/code new-code} + {:db/id ba-id + :bank-account/code (str/replace ba-code #"^.*-" (str new-code "-"))}]) + + #_[{:db/id id + :client/code new-code + #_#_:client/bank-accounts (map)}] + ) + ) + +(defn rename-codes-1 [conn] + (let [result (apply concat [(rename "WE" "WME" (d/connect uri)) + (rename "HM" "HIM" (d/connect uri)) + (rename "BES" "SBE" (d/connect uri)) + (rename "BES" "SBE" (d/connect uri)) + (rename "ORA" "OMG" (d/connect uri)) + (rename "INT" "IGC" (d/connect uri)) + (rename "MV" "MVSC" (d/connect uri))])] + (if (seq (seq result)) + result + [[]])) + ) + diff --git a/src/clj/auto_ap/datomic/transactions.clj b/src/clj/auto_ap/datomic/transactions.clj index dc069b95..d4ffeaeb 100644 --- a/src/clj/auto_ap/datomic/transactions.clj +++ b/src/clj/auto_ap/datomic/transactions.clj @@ -112,8 +112,8 @@ true (apply-pagination args)))) (defn graphql-results [ids db args] - (->> (d/pull-many db '[* {:transaction/client [:client/name :db/id] - :transaction/bank-account [:bank-account/name :bank-account/yodlee-account-id]}] + (->> (d/pull-many db '[* {:transaction/client [:client/name :db/id :client/code] + :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id]}] ids) (map #(update % :transaction/date c/from-date)) (map #(update % :transaction/post-date c/from-date)) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 1f45d5a9..6699e5ea 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -58,6 +58,7 @@ :check_number {:type 'Int} :name {:type 'String} :bank_code {:type 'String} + :routing {:type 'String} :bank_name {:type 'String} :yodlee_account_id {:type 'Int}}} :address @@ -106,6 +107,7 @@ :payment {:fields {:id {:type :id} :type {:type :payment_type} + :original_id {:type 'Int} :amount {:type 'String} :vendor {:type :vendor} :client {:type :client} @@ -161,6 +163,7 @@ :invoice {:fields {:id {:type :id} + :original_id {:type 'Int} :total {:type 'String} :outstanding_balance {:type 'String} :invoice_number {:type 'String} @@ -203,7 +206,7 @@ :queries {:invoice_page {:type '(list :invoice_page) - :args {:imported {:type 'Boolean} + :args {:import_status {:type 'String} :status {:type 'String} :client_id {:type :id} :vendor_id {:type :id} @@ -335,12 +338,20 @@ :bank_account_type {:values [{:enum-value :check} {:enum-value :cash}]}} :mutations - {:print_checks {:type :check_result - :args {:invoice_payments {:type '(list :invoice_payment_amount)} - :bank_account_id {:type :id} - :type {:type :payment_type} - :client_id {:type :id}} - :resolve :mutation/print-checks} + {:reject_invoices {:type '(list :id) + :args {:invoices {:type '(list :id)}} + :resolve :mutation/reject-invoices} + + :approve_invoices {:type '(list :id) + :args {:invoices {:type '(list :id)}} + :resolve :mutation/approve-invoices} + + :print_checks {:type :check_result + :args {:invoice_payments {:type '(list :invoice_payment_amount)} + :bank_account_id {:type :id} + :type {:type :payment_type} + :client_id {:type :id}} + :resolve :mutation/print-checks} :add_handwritten_check {:type :check_result :args {:invoice_id {:type :id} @@ -424,7 +435,7 @@ (map ->graphql (d-checks/get-graphql (assoc (<-graphql args) - :limit Integer/MAX_VALUE)))) + :count Integer/MAX_VALUE)))) @@ -465,6 +476,8 @@ :get-user get-user :mutation/add-handwritten-check gq-checks/add-handwritten-check :mutation/print-checks print-checks + :mutation/reject-invoices gq-invoices/reject-invoices + :mutation/approve-invoices gq-invoices/approve-invoices :mutation/edit-user gq-users/edit-user :mutation/add-invoice gq-invoices/add-invoice :mutation/edit-invoice gq-invoices/edit-invoice diff --git a/src/clj/auto_ap/graphql/checks.clj b/src/clj/auto_ap/graphql/checks.clj index 11cff863..27e0ac88 100644 --- a/src/clj/auto_ap/graphql/checks.clj +++ b/src/clj/auto_ap/graphql/checks.clj @@ -95,7 +95,7 @@ [[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"] [:cell {:colspan 5 :leading 11.5} (split-memo memo) [:line {:line-width 0.15 :color [50 50 50]}]] - [:cell {:colspan 6 } (if (:signature-file client) + [:cell {:colspan 6 } (if (:client/signature-file client) [:image { :top-margin 90 :xscale 0.30 :yscale 0.30 :align :center} (:client/signature-file client)] diff --git a/src/clj/auto_ap/graphql/invoices.clj b/src/clj/auto_ap/graphql/invoices.clj index c0f79ed1..938831b3 100644 --- a/src/clj/auto_ap/graphql/invoices.clj +++ b/src/clj/auto_ap/graphql/invoices.clj @@ -27,7 +27,20 @@ (map ->graphql (d-invoices/get-graphql (assoc (<-graphql args) - :limit Integer/MAX_VALUE)))) + :count Integer/MAX_VALUE)))) + +(defn reject-invoices [context {:keys [invoices] :as in} value] + (assert-admin (:id context)) + + (let [transactions (map (fn [i] [:db/retractEntity i ]) invoices) + transaction-result @(d/transact (d/connect uri) transactions)] + invoices)) + +(defn approve-invoices [context {:keys [invoices] :as in} value] + (assert-admin (:id context)) + (let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices) + transaction-result @(d/transact (d/connect uri) transactions)] + invoices)) (defn add-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value] (when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice_number @@ -44,6 +57,7 @@ :invoice/invoice-number invoice_number :invoice/client client_id :invoice/vendor vendor_id + :invoice/import-status :import-status/imported :invoice/total total :invoice/outstanding-balance total :invoice/status :invoice-status/unpaid @@ -82,12 +96,15 @@ (defn void-invoice [context {id :invoice_id} value] (let [invoice (d-invoices/get-by-id id) + _ (println invoice) _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice))) updated-invoice (d-invoices/update {:db/id id :invoice/total 0.0 :invoice/outstanding-balance 0.0 - :invoice/status :invoice-status/voided})] - ;; TODO void out all expense accounts + :invoice/status :invoice-status/voided + :invoice/expense-accounts (map (fn [ea] {:db/id (:db/id ea) + :invoice-expense-account/amount 0.0}) + (:invoice/expense-accounts invoice))})] (-> updated-invoice (->graphql)))) diff --git a/src/clj/auto_ap/parse.clj b/src/clj/auto_ap/parse.clj index c8e80383..92278a6a 100644 --- a/src/clj/auto_ap/parse.clj +++ b/src/clj/auto_ap/parse.clj @@ -11,6 +11,12 @@ (defmulti parse-value (fn [method _ _] method)) + +(defmethod parse-value :trim-commas + [_ _ value] + (str/replace value #"," "") + ) + (defmethod parse-value :clj-time [_ format value] (time/from-time-zone (f/parse (f/formatter format) value) @@ -30,17 +36,20 @@ #(extract-template % (dissoc template :multi)) (str/split text (:multi template))) - [(->> template - :extract - (reduce-kv - (fn [result k v] - (let [value (some-> (first (map second (re-seq v text))) - str/trim ) - [value-parser parser-params] (-> template :parser k)] - (assoc result k (parse-value value-parser parser-params value)))) - {:vendor-code (:vendor template)}))])) + (when template + [(->> template + :extract + (reduce-kv + (fn [result k v] + (let [value (some-> (first (map second (re-seq v text))) + str/trim ) + [value-parser parser-params] (-> template :parser k)] + (assoc result k (parse-value value-parser parser-params value)))) + {:vendor-code (:vendor template) + :text text}))]))) (defn parse [text] + (println text) (->> t/pdf-templates (filter (partial template-applies? text)) first @@ -67,13 +76,24 @@ [file filename] (excel/parse-file file filename)) -(defn best-match [clients client-identifier] +(defn best-match [clients invoice-client-name] (->> clients - (map (fn [client] - (if-let [matches (:client/matches client)] - [client (apply min (map #(m/jaccard (.toLowerCase client-identifier) %) matches))] - [client 1]))) + + (mapcat (fn [{:keys [:db/id :client/matches :client/name] :as client :or {matches []}}] + (map (fn [m] + [client (m/jaccard (.toLowerCase invoice-client-name) (.toLowerCase m))]) + (conj matches name)))) (filter #(< (second %) 0.25)) (sort-by second) - ffirst)) + +(defn best-location-match [client text] + (or (->> client + :client/location-matches + (mapcat (fn [{:keys [:location-match/location :location-match/matches]}] + (map (fn [match] [location match]) matches))) + (filter (fn [[location match]] (re-find (re-pattern (str "(?i)" match)) text)) ) + first + first) + (:client/default-location client) + (first (:client/locations client)))) diff --git a/src/clj/auto_ap/parse/templates.clj b/src/clj/auto_ap/parse/templates.clj index eb204e11..b2607b64 100644 --- a/src/clj/auto_ap/parse/templates.clj +++ b/src/clj/auto_ap/parse/templates.clj @@ -10,13 +10,14 @@ :invoice-number #"\s+[0-9]+/[0-9]+/[0-9]+\s+([0-9]+)"} :parser {:date [:clj-time "MM/dd/yyyy"]}} - {:vendor "GGM" + {:vendor "Golden Gate Meat Company, Inc" :keywords [#"Golden Gate Meat"] :extract {:total #"Invoice Total\:\s+\$([\d.,]+)" :customer-identifier #"Bill To\s*:\s*([\w ]+)\s{2,}" :date #"Printed:\s+([0-9]+/[0-9]+/[0-9]+)" :invoice-number #"Invoice\s+[^\n]+\n[^\n]+\n\s+([0-9]+)"} - :parser {:date [:clj-time "MM/dd/yyyy"]}} + :parser {:date [:clj-time "MM/dd/yyyy"] + :total [:trim-commas nil]}} {:vendor "CINTAS" :keywords [#"CINTAS CORPORATION"] @@ -25,7 +26,15 @@ :date #"INVOICE DATE\s*\n.*\s+([0-9]+/[0-9]+/[0-9]+)" :total #"INVOICE TOTAL\s+([0-9.]+)"} :parser {:date [:clj-time "MM/dd/yy"]} - :multi #"\f\f"}]) + :multi #"\f\f"} + + {:vendor "Carbonic Service Inc" + :keywords [#"CARBONIC SERVICE INC"] + :extract {:invoice-number #"Invoice #\s*\n\s*[\w\.]+\s+[\w\./]+(.*)\s*\n" + :customer-identifier #"Bill To[^\n]+\n[^\n]*\n([\w ]+)\s{2,}" + :date #"Invoice #\s*\n\s*[\w\.]+\s+([\w\./]+)" + :total #"Total\s+\$([0-9.]+)"} + :parser {:date [:clj-time "MM/dd/yy"]}}]) (def excel-templates [{:vendor "Isp Productions" diff --git a/src/clj/auto_ap/routes/auth.clj b/src/clj/auto_ap/routes/auth.clj index 772ec985..a958e8ac 100644 --- a/src/clj/auto_ap/routes/auth.clj +++ b/src/clj/auto_ap/routes/auth.clj @@ -9,6 +9,14 @@ (def google-client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com") (def google-client-secret "OC-WemHurPXYpuIw5cT-B90g") +(defn make-api-token [] + (jwt/sign {:user "API" + :exp (time/plus (time/now) (time/days 700)) + :user/role "admin" + :user/name "API"} + (:jwt-secret env) + {:alg :hs512})) + (defroutes routes (GET "/oauth" {{:strs [code]} :query-params :keys [scheme] :as r {:strs [host]} :headers} (try @@ -37,14 +45,14 @@ ;; TODO - these namespaces are not being transmitted/deserialized properly (if (and token user) {:status 301 - :headers {"Location" (str "/?jwt=" (jwt/sign {:user (:name profile) - :exp (time/plus (time/now) (time/days 7)) - :user/clients (map (fn [c] - (dissoc c :client/bank-accounts ) - ) - (:user/clients user)) - :user/role (name (:user/role user)) - :user/name (:name profile)} + :headers {"Location" (str "/?jwt=" (jwt/sign (doto {:user (:name profile) + :exp (time/plus (time/now) (time/days 7)) + :user/clients (map (fn [c] + (dissoc c :client/bank-accounts )) + (:user/clients user)) + :user/role (name (:user/role user)) + :user/name (:name profile)} + println) (:jwt-secret env) {:alg :hs512}))}} {:status 401 diff --git a/src/clj/auto_ap/routes/exports.clj b/src/clj/auto_ap/routes/exports.clj index 38d3966d..fde51090 100644 --- a/src/clj/auto_ap/routes/exports.clj +++ b/src/clj/auto_ap/routes/exports.clj @@ -23,12 +23,12 @@ (let [query [[:all_invoices {:client-code (query-params "client-code") :original-id (query-params "original")} - [:id :total :outstanding-balance :invoice-number :date :status - [:payments [:amount [:payment [:check-number :memo [:bank_account [:id :name :number :bank-name :bank-code]]]]]] + [:id :total :outstanding-balance :invoice-number :date :status :original-id + [:payments [:amount [:payment [:check-number :memo [:bank_account [:id :name :number :bank-name :bank-code :code]]]]]] [:vendor [:name :id [:primary_contact [:name]] [:address [:street1 :city :state :zip]]]] [:expense_accounts [:amount :id :expense_account_id :location [:expense_account [:id :name [:parent [:id :name]]]]]] - [:client [:name :id :locations]]]]] + [:client [:name :id :code :locations]]]]] invoices (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] (doto (list (:all-invoices (:data invoices))) clojure.pprint/pprint))) @@ -37,11 +37,11 @@ (let [query [[:all_payments {:client-code (query-params "client-code") :original-id (query-params "original")} - [:id :check-number :amount :memo :date :status :type - [:invoices [[:invoice [:id]] :amount]] - [:bank-account [:number :bank-name :bank-code :id]] + [:id :check-number :amount :memo :date :status :type :original-id + [:invoices [[:invoice [:id :original-id]] :amount]] + [:bank-account [:number :code :bank-name :bank-code :id]] [:vendor [:name :id [:primary-contact [:name :email :phone]] :default-expense-account [:address [:street1 :city :state :zip]]]] - [:client [:id :name]] + [:client [:id :name :code]] ]]] payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] (list (:all-payments (:data payments))))) @@ -57,7 +57,7 @@ (assert-admin identity) (let [[transactions] (d-transactions/get-graphql {:client-code (query-params "client-code") #_#_:original-id (Integer/parseInt (query-params "original")) - :limit Integer/MAX_VALUE}) + :count Integer/MAX_VALUE}) ] (map (comp ->graphql (fn [i] (-> i diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index 8d501ba2..61a4efc0 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -114,6 +114,7 @@ :vendor vendor-id :client client-id :default-location default-location + :import-status :import-status/imported :default-expense-account default-expense-account :total total :outstanding-balance (if (= "Cash" check) @@ -140,7 +141,59 @@ )] [invoice payment]))) (filter identity) - (map remove-nils))) + (map remove-nils) + )) + + +(defn import-uploaded-invoice [imports] + (let [clients (d-clients/get-all) + _ (println imports) + + transactions (reduce (fn [result {:keys [invoice-number customer-identifier total date vendor-code text] :as info}] + (let [[matching-vendor default-expense-account] (->> (d/query + (cond-> {:query {:find ['?vendor '?default-expense-account] + :in ['$ '?vendor-name] + :where ['[?vendor :vendor/name ?vendor-name] + '[?vendor :vendor/default-expense-account ?default-expense-account]]} + :args [(d/db (d/connect uri)) vendor-code]})) + first) + matching-client (parse/best-match clients customer-identifier) + _ (println "New invoice matches client" matching-client) + matching-location (parse/best-location-match matching-client text ) + [existing-id existing-outstanding-balance existing-status import-status] (->> (d/query + (cond-> {:query {:find ['?e '?outstanding-balance '?status '?import-status2] + :in ['$ '?invoice-number '?vendor '?client] + :where '[[?e :invoice/invoice-number ?invoice-number] + [?e :invoice/vendor ?vendor] + [?e :invoice/client ?client] + [?e :invoice/outstanding-balance ?outstanding-balance] + [?e :invoice/status ?status] + [?e :invoice/import-status ?import-status] + [?import-status :db/ident ?import-status2]]} + :args [(d/db (d/connect uri)) invoice-number matching-vendor (:db/id matching-client)]})) + first)] + + (if (= :import-status/imported import-status) + result + (conj result (remove-nils #:invoice {:invoice/client (:db/id matching-client) + :invoice/vendor matching-vendor + :invoice/invoice-number invoice-number + :invoice/total (Double/parseDouble total) + :invoice/date (to-date date) + :invoice/import-status :import-status/pending + :invoice/outstanding-balance (or existing-outstanding-balance (Double/parseDouble total)) + :invoice/status (or existing-status :invoice-status/unpaid) + :invoice/expense-accounts (when-not existing-id [#:invoice-expense-account {:expense-account-id default-expense-account + :location matching-location + :amount (Double/parseDouble total)}]) + :db/id existing-id + }))) + )) + [] + imports)] + + @(d/transact (d/connect uri) transactions) + )) (defroutes routes (wrap-routes @@ -183,6 +236,13 @@ :headers {"Content-Type" "application/edn"}}))) (context "/invoices" [] + (POST "/upload" + {{ files "file"} :params :as params} + (let [{:keys [filename tempfile]} files] + (import-uploaded-invoice (parse/parse-file (.getPath tempfile) filename)) + {:status 200 + :body (pr-str {}) + :headers {"Content-Type" "application/edn"}})) (POST "/upload-integreat" {{:keys [excel-rows]} :edn-params user :identity} (assert-admin user) diff --git a/src/clj/auto_ap/yodlee/import.clj b/src/clj/auto_ap/yodlee/import.clj index dd69e590..942e0485 100644 --- a/src/clj/auto_ap/yodlee/import.clj +++ b/src/clj/auto_ap/yodlee/import.clj @@ -68,23 +68,23 @@ (try (when client-id @(->> [(remove-nils #:transaction - {:post-date (time/parse post-date "YYYY-MM-dd") - :id (sha-256 (str id)) - :account-id account-id - :date (coerce/to-date (time/parse date "YYYY-MM-dd")) - :amount amount - :description-original description-original - :description-simple description-simple - :type type - :status status - :client client-id - :check-number check-number - :bank-account (transaction->bank-account-id transaction) - :payment (when check-id - {:db/id check-id - :payment/status :payment-status/cleared} - ) - })] + {:post-date (coerce/to-date (time/parse post-date "YYYY-MM-dd")) + :id (sha-256 (str id)) + :account-id account-id + :date (coerce/to-date (time/parse date "YYYY-MM-dd")) + :amount (double amount) + :description-original description-original + :description-simple description-simple + :type type + :status status + :client client-id + :check-number check-number + :bank-account (transaction->bank-account-id transaction) + :payment (when check-id + {:db/id check-id + :payment/status :payment-status/cleared} + ) + })] (d/transact (d/connect uri)))) @@ -115,8 +115,8 @@ (defn do-import [] (let [transactions (client/get-transactions) - _ (println "All accounts:" (client/get-accounts)) - _ (println "ALL Transactions:" transactions) + #_#__ (println "All accounts:" (client/get-accounts)) + #_#__ (println "ALL Transactions:" transactions) all-bank-accounts (mapcat (fn [c] (map (fn [{:keys [:db/id :bank-account/yodlee-account-id]}] (when (and id yodlee-account-id) diff --git a/src/cljs/auto_ap/effects.cljs b/src/cljs/auto_ap/effects.cljs index 10f2b804..8c0c8217 100644 --- a/src/cljs/auto_ap/effects.cljs +++ b/src/cljs/auto_ap/effects.cljs @@ -119,6 +119,19 @@ node)) m)) +(defonce timeouts + (atom {})) + +(re-frame/reg-fx + :dispatch-debounce + (fn [{:keys [event time key]}] + (js/clearTimeout (@timeouts key)) + (swap! timeouts assoc key + (js/setTimeout (fn [] + (re-frame/dispatch event) + (swap! timeouts dissoc key)) + time)))) + (re-frame/reg-fx :graphql (fn [{:keys [query on-success on-error token variables query-obj]}] diff --git a/src/cljs/auto_ap/views/components/invoice_table.cljs b/src/cljs/auto_ap/views/components/invoice_table.cljs index ad650072..dc4034f7 100644 --- a/src/cljs/auto_ap/views/components/invoice_table.cljs +++ b/src/cljs/auto_ap/views/components/invoice_table.cljs @@ -133,10 +133,12 @@ } ""]]] [:tbody + (println checked) (if (:loading @status) [:tr [:td {:col-span 5} [:i.fa.fa-spin.fa-spinner]]] + (for [{:keys [client payments expense-accounts invoice-number date total outstanding-balance id vendor] :as i} (:invoices @invoice-page)] ^{:key id} [:tr {:class (:class i)} @@ -146,7 +148,8 @@ "checked" "") :on-change (fn [x e] (when on-check-changed - (on-check-changed id)))} ]]) + (println id i) + (on-check-changed id i)))} ]]) (when-not selected-client [:td (:name client)]) [:td (:name vendor)] @@ -174,7 +177,7 @@ [:div.dropdown-content (for [e expense-accounts] ^{:key (:id e)} - [:span.dropdown-item (:name (:expense-account e)) " "(gstring/format "$%.2f" (:amount e) ) ]) + [:span.dropdown-item (:name (:expense-account e)) " " (gstring/format "$%.2f" (:amount e) ) ]) [:hr.dropdown-divider] diff --git a/src/cljs/auto_ap/views/components/invoices/side_bar.cljs b/src/cljs/auto_ap/views/components/invoices/side_bar.cljs index bda9e7fd..7cbc8bf2 100644 --- a/src/cljs/auto_ap/views/components/invoices/side_bar.cljs +++ b/src/cljs/auto_ap/views/components/invoices/side_bar.cljs @@ -19,15 +19,22 @@ [:li.menu-item [:a.item {:href (bidi/path-for routes/routes :unpaid-invoices) :class [(active-when ap = :unpaid-invoices)]} - [:span {:class "icon icon-accounting-document" :style {:font-size "25px"}}] + [:span {:class "icon icon-accounting-invoice-mail" :style {:font-size "25px"}}] [:span {:class "name"} "Unpaid Invoices"]]] [:li.menu-item [:a.item {:href (bidi/path-for routes/routes :paid-invoices) :class [(active-when ap = :paid-invoices)]} - [:span {:class "icon icon-accounting-invoice-mail" :style {:font-size "25px"}}] + [:span {:class "icon icon-check-payment-give" :style {:font-size "25px"}}] - [:span {:class "name"} "Paid Invoices"]]]]] + [:span {:class "name"} "Paid Invoices"]]] + [:li.menu-item + [:a.item {:href (bidi/path-for routes/routes :import-invoices) + :class [(active-when ap = :import-invoices)]} + + [:span {:class "icon icon-accounting-document" :style {:font-size "25px"}}] + + [:span {:class "name"} "Import Invoices"]]]]] [:div rest] [:div {:class "compose has-text-centered"} diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index 24240c1f..f4de920e 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -8,6 +8,7 @@ [auto-ap.views.utils :refer [active-when active-when= login-url dispatch-event]] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.unpaid-invoices :refer [unpaid-invoices-page]] + [auto-ap.views.pages.import-invoices :refer [import-invoices-page]] [auto-ap.views.pages.paid-invoices :refer [paid-invoices-page]] [auto-ap.views.pages.transactions :refer [transactions-page]] [auto-ap.views.pages.login :refer [login-page]] @@ -25,6 +26,10 @@ (defmethod page :unpaid-invoices [_] (unpaid-invoices-page {:status "unpaid"})) +(defmethod page :import-invoices [_] + (import-invoices-page )) + + (defmethod page :paid-invoices [_] (unpaid-invoices-page {:status "paid"})) diff --git a/src/cljs/auto_ap/views/pages/import_invoices.cljs b/src/cljs/auto_ap/views/pages/import_invoices.cljs index 7d41cbb0..662bcb12 100644 --- a/src/cljs/auto_ap/views/pages/import_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/import_invoices.cljs @@ -4,6 +4,9 @@ [auto-ap.events :as events] [auto-ap.subs :as subs] [auto-ap.entities.clients :as client] + [auto-ap.views.components.layouts :refer [side-bar-layout]] + [auto-ap.views.components.invoices.side-bar :refer [invoices-side-bar]] + [auto-ap.views.utils :refer [dispatch-event]] [auto-ap.entities.vendors :as vendor] [auto-ap.views.components.invoice-table :refer [invoice-table] :as invoice-table] [cljsjs.dropzone :as dropzone] @@ -56,7 +59,7 @@ (assoc-in [:status :loading] true) (assoc-in [::params] params)) :graphql {:token (-> cofx :db :user) - :query-obj (invoice-table/query (assoc params :imported false)) + :query-obj (invoice-table/query (assoc params :import-status "pending")) :on-success [::received]}})) (re-frame/reg-event-db @@ -67,15 +70,32 @@ (assoc-in [:status :loading] false)))) (re-frame/reg-event-fx - ::reject-invoices - (fn [cofx [_ on-success]] - {:http {:method :post - :token (-> cofx :db :user) - :uri (str "/api/invoices/reject" - (when-let [client-id (:id @(re-frame/subscribe [::subs/client]))] - (str "?client=" client-id))) - :on-success on-success - }})) + ::reject-invoices-clicked + (fn [{:keys [db]} [_ invoices on-success]] + {:graphql + {:token (-> db :user) + :query-obj {:venia/operation {:operation/type :mutation + :operation/name "RejectInvoices"} + + :venia/queries [[:reject-invoices + {:invoices (keys invoices)} + []]]} + :on-success [::invalidated]} + })) + +(re-frame/reg-event-fx + ::approve-invoices-clicked + (fn [{:keys [db]} [_ invoices on-success]] + {:graphql + {:token (-> db :user) + :query-obj {:venia/operation {:operation/type :mutation + :operation/name "ApproveInvoices"} + + :venia/queries [[:approve-invoices + {:invoices (keys invoices)} + []]]} + :on-success [::invalidated]} + })) (re-frame/reg-event-fx ::approve-invoices @@ -88,7 +108,46 @@ :on-success on-success }})) -(def import-invoices-page +(re-frame/reg-event-db + ::toggle-check + (fn [db [_ id invoice]] + (-> db + (update-in [::invoice-page :checked] (fn [x] + (let [x (or x {})] + (if (x id) + (dissoc x id) + (assoc x id invoice)))))))) + +(defn approve-reject-button [checked] + [:div.is-pulled-right + + [:button.button.is-success {:on-click (dispatch-event [::approve-invoices-clicked checked]) + :disabled (if (seq checked) + "" + "disabled")} + "Approve " + (when (> (count checked )) + (str + (count checked) + " invoices")) + + + [:span " "]] + [:button.button.is-danger {:on-click (dispatch-event [::reject-invoices-clicked checked]) + :disabled (if (seq checked) + "" + "disabled")} + "Reject " + (when (> (count checked )) + (str + (count checked) + " invoices")) + + + [:span " "] + ]]) + +(def import-invoices-content (with-meta (fn [] (let [invoice-page (re-frame/subscribe [::invoice-page]) @@ -102,30 +161,27 @@ [:div {:class "card-header"} [:span {:class "card-header-title"} "Found Invoices"]] [:div {:class "card-content"} + [approve-reject-button (:checked @invoice-page)] (if (:loading @status) [:h1.title [:i.fa.fa-spin.fa-spinner]] (if (seq (:invoices @invoice-page)) [invoice-table {:id :approved :invoice-page invoice-page + :check-boxes true + :checked (:checked @invoice-page) + :on-check-changed (fn [which invoice] + (re-frame/dispatch [::toggle-check which invoice])) :status (re-frame/subscribe [::subs/status]) :params (re-frame/subscribe [::params]) :on-params-change (fn [params] (re-frame/dispatch [::params-change params])) }] - [:span "No pending invoices"]))] - (if (and (seq (:invoices @invoice-page)) (not (:loading @status))) - [:div.card-footer - [:a.card-footer-item - {:on-click (fn [e] - (.preventDefault e) - (re-frame/dispatch [::approve-invoices - [::invalidated]]))} - "Accept all"] - [:a.card-footer-item - {:on-click (fn [e] - (.preventDefault e) - (re-frame/dispatch [::reject-invoices - [::invalidated]]))} - "Reject all"]])]])) + [:span "No pending invoices"]))]]])) {:component-will-mount (fn [] (re-frame/dispatch-sync [::invalidated]))})) + +(defn import-invoices-page [] + [side-bar-layout {:side-bar [invoices-side-bar {}] + :main [import-invoices-content ]}]) + + diff --git a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs index ec502e5c..cb73742d 100644 --- a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs @@ -17,7 +17,7 @@ [auto-ap.views.components.invoices.side-bar :refer [invoices-side-bar]] [auto-ap.expense-accounts :as expense-accounts] [auto-ap.entities.invoices-expense-accounts :as invoices-expense-accounts] - [auto-ap.views.utils :refer [active-when dispatch-event bind-field horizontal-field date->str str->date pretty standard]] + [auto-ap.views.utils :refer [active-when dispatch-event bind-field horizontal-field date->str date-time->str str->date pretty standard]] [auto-ap.utils :refer [by replace-if]] [auto-ap.views.pages.check :as check] [auto-ap.views.components.invoice-table :refer [invoice-table] :as invoice-table] @@ -72,6 +72,7 @@ (fn [db] (-> db (::params {})))) + (re-frame/reg-event-fx ::params-change (fn [cofx [_ params]] @@ -79,24 +80,30 @@ (assoc-in [:status :loading] true) (assoc-in [::params] params)) :graphql {:token (-> cofx :db :user) - :query-obj (invoice-table/query (doto (assoc params :imported true) println)) + :query-obj (invoice-table/query (-> params (assoc :import-status "imported") (dissoc :invoice-number-like-current)) ) :on-success [::received]}})) +(re-frame/reg-event-db + ::unmount-invoices + (fn [db [_ data]] + (-> db + (dissoc ::invoice-page )))) (re-frame/reg-event-db ::received (fn [db [_ data]] (-> db - (assoc ::invoice-page (first (:invoice-page data))) + (update ::invoice-page merge (first (:invoice-page data))) (assoc-in [:status :loading] false)))) (re-frame/reg-event-db ::toggle-check - (fn [db [_ data]] - (update-in db [::invoice-page :checked] (fn [x] - (let [x (or x #{})] - (if (x data) - (disj x data) - (conj x data))))))) + (fn [db [_ id invoice]] + (-> db + (update-in [::invoice-page :checked] (fn [x] + (let [x (or x {})] + (if (x id) + (dissoc x id) + (assoc x id invoice)))))))) (re-frame/reg-event-db ::print-checks-clicked @@ -111,8 +118,8 @@ (update-in [::invoice-page :print-checks-shown?] #(not %) ) (assoc-in [::advanced-print-checks] {:shown? true :bank-account-id (:id (first (:bank-accounts @(re-frame/subscribe [::subs/clients])))) - :invoices (->> invoices - (filter (comp checked :id)) + :invoices (->> checked + vals (map #(assoc % :amount (:outstanding-balance %))))} ))))) (re-frame/reg-event-fx @@ -171,22 +178,20 @@ (re-frame/reg-event-fx ::print-checks (fn [{:keys [db]} [_ bank-account-id type]] - (let [invoice-amounts (by :id :outstanding-balance (get-in db [::invoice-page :invoices]))] - - {:db (-> db - (assoc-in [::invoice-page :print-checks-shown?] false ) - (assoc-in [::invoice-page :print-checks-loading?] true )) - :graphql - {:token (-> db :user) - - :query-obj (print-checks-query (map (fn [id] - {:invoice-id id - :amount (invoice-amounts id)}) - (get-in db [::invoice-page :checked])) - bank-account-id - type - (:client db)) - :on-success [::checks-created]}}))) + {:db (-> db + (assoc-in [::invoice-page :print-checks-shown?] false ) + (assoc-in [::invoice-page :print-checks-loading?] true )) + :graphql + {:token (-> db :user) + + :query-obj (print-checks-query (map (fn [[id invoice]] + {:invoice-id id + :amount (:outstanding-balance invoice)}) + (get-in db [::invoice-page :checked])) + bank-account-id + type + (:client db)) + :on-success [::checks-created]}})) @@ -675,57 +680,96 @@ (if (and (= key :vendor-id) (not= value (get-in db [::params :vendor-id]))) (do - (re-frame/dispatch [::params-change (assoc (::params updated) :vendor-id value)]) + (re-frame/dispatch [::params-change (assoc (::params updated) :vendor-id value :start 0)]) (assoc-in updated [::params :vendor-id] value)) updated)))) +(re-frame/reg-event-fx + ::invoice-number-like-current-changed + (fn [{:keys [db]} [_ params invoice-like]] + {:db (assoc-in db [::params :invoice-number-like-current] invoice-like ) + :dispatch-debounce {:event [::invoice-number-like-settled invoice-like] + :time 500 + :key ::invoice-nuber-like}})) + +(re-frame/reg-event-fx + ::invoice-number-like-settled + (fn [{:keys [db]} [_ invoice-like]] + {:dispatch [::params-change (assoc (::params db) :invoice-number-like invoice-like :start 0) ]})) + (defn invoice-number-filter [] - (let [{:keys [invoice-number-like] :as params} @(re-frame/subscribe [::params])] + (let [{:keys [invoice-number-like-current] :as params} @(re-frame/subscribe [::params])] [:div.field [:div.control [:input.input {:placeholder "AP-123" - :value invoice-number-like + :value invoice-number-like-current :on-change (fn [x] - (re-frame/dispatch [::params-change (assoc params :invoice-number-like (.. x -target -value)) ]) - )} ]]])) + (re-frame/dispatch [::invoice-number-like-current-changed params (.. x -target -value) ]))} ]]])) (defn pay-button [{:keys [print-checks-shown? checked-invoices print-checks-loading?]}] (let [current-client @(re-frame/subscribe [::subs/client])] - [:div.is-pulled-right - [:button.button.is-danger {:on-click (dispatch-event [::new-invoice])} "New Invoice"] - (when current-client - [:div.dropdown.is-right {:class (if print-checks-shown? - "is-active" - "")} - [:div.dropdown-trigger - [:button.button.is-success {:aria-haspopup true - :on-click (dispatch-event [::print-checks-clicked ]) - :disabled (if (seq checked-invoices) - "" - "disabled") + [:div + + + + [:div.is-pulled-right + + + [:button.button.is-danger {:on-click (dispatch-event [::new-invoice])} "New Invoice"] + + + (when current-client + [:div.dropdown.is-right {:class (if print-checks-shown? + "is-active" + "")} + [:div.dropdown-trigger + [:button.button.is-success {:aria-haspopup true + :on-click (dispatch-event [::print-checks-clicked ]) + :disabled (if (seq checked-invoices) + "" + "disabled") - :class (if print-checks-loading? - "is-loading" - "")} - "Pay " - [:span " "] - [:span.icon.is-small [:i.fa.fa-angle-down {:aria-hidden "true"}]]]] - [:div.dropdown-menu {:role "menu"} - [:div.dropdown-content - (list - (for [{:keys [id number name type]} (:bank-accounts current-client)] - (if (= :cash type) - ^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :cash])} "With cash"] - (list - ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :check])} "Print checks from " name] - ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :debit])} "Debit from " name]))) - ^{:key "advanced-divider"} [:hr.dropdown-divider] + :class (if print-checks-loading? + "is-loading" + "")} + "Pay " + (when (> (count checked-invoices )) + (str + (count checked-invoices) + " invoices " + "(" (->> checked-invoices + vals + (map (comp js/parseFloat :outstanding-balance)) + (reduce + 0) + (gstring/format "$%.2f" )) + ")")) + + + [:span " "] + [:span.icon.is-small [:i.fa.fa-angle-down {:aria-hidden "true"}]]]] + [:div.dropdown-menu {:role "menu"} + [:div.dropdown-content + (list + (for [{:keys [id number name type]} (:bank-accounts current-client)] + (if (= :cash type) + ^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :cash])} "With cash"] + (list + ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :check])} "Print checks from " name] + ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :debit])} "Debit from " name]))) + ^{:key "advanced-divider"} [:hr.dropdown-divider] - (when (= 1 (count checked-invoices)) - ^{:key "handwritten"} [:a.dropdown-item {:on-click (dispatch-event [::handwrite-checks])} "Handwritten Check..."]) - ^{:key "advanced"} [:a.dropdown-item {:on-click (dispatch-event [::advanced-print-checks])} "Advanced..."])]]])])) + (when (= 1 (count checked-invoices)) + ^{:key "handwritten"} [:a.dropdown-item {:on-click (dispatch-event [::handwrite-checks])} "Handwritten Check..."]) + ^{:key "advanced"} [:a.dropdown-item {:on-click (dispatch-event [::advanced-print-checks])} "Advanced..."])]]])] + [:div.is-pulled-right + (into [:div.tags {:style {:margin-right ".5 rem;"}}] (map (fn [[id invoice]] [:span.tag.is-medium (:invoice-number invoice) [:button.delete.is-small {:on-click (dispatch-event [::toggle-check id invoice])}]]) checked-invoices))]] + )) (defn unpaid-invoices-content [{:keys [status]}] (r/create-class {:display-name "unpaid-invoices-content" + :component-will-unmount (fn [this] + (re-frame/dispatch [::unmount-invoices]) + + ) :reagent-render (fn [{:keys [status]}] (let [{:keys [checked print-checks-shown? print-checks-loading? advanced-print-shown? vendor-filter]} @(re-frame/subscribe [::invoice-page]) current-client @(re-frame/subscribe [::subs/client]) @@ -736,6 +780,7 @@ [pay-button {:print-checks-shown? print-checks-shown? :checked-invoices checked :print-checks-loading? print-checks-loading?}]) + [invoice-table {:id :unpaid :params (re-frame/subscribe [::params]) :invoice-page (re-frame/subscribe [::invoice-page]) @@ -749,8 +794,8 @@ (re-frame/dispatch [::params-change params])) :check-boxes (= status "unpaid") :checked checked - :on-check-changed (fn [which] - (re-frame/dispatch [::toggle-check which])) + :on-check-changed (fn [which invoice] + (re-frame/dispatch [::toggle-check which invoice])) :expense-event [::expense-accounts-dialog/change-expense-accounts]}] [print-checks-modal]