diff --git a/src/clj/auto_ap/datomic/transactions.clj b/src/clj/auto_ap/datomic/transactions.clj index c0db2345..d180438c 100644 --- a/src/clj/auto_ap/datomic/transactions.clj +++ b/src/clj/auto_ap/datomic/transactions.clj @@ -18,15 +18,40 @@ :else (keyword "transaction" sort-by))) +(defn potential-duplicate-ids [db args] + (if (and (:potential-duplicates args) + (:bank-account-id args)) + (->> (d/q '[:find ?tx ?amount ?date + :in $ ?ba + :where + [?tx :transaction/bank-account ?ba] + [?tx :transaction/amount ?amount] + [?tx :transaction/date ?date]] + db + (:bank-account-id args)) + (group-by (fn [[tx amount date]] + [amount date])) + (filter (fn [[g txes]] + (> (count txes) 1))) + + (vals) + (mapcat identity) + (map first) + set))) + (defn raw-graphql-ids - ([args] (raw-graphql-ids (d/db (d/connect uri)) args)) + ([args] (raw-graphql-ids (d/db conn) args)) ([db args] - (let [query (cond-> {:query {:find [] + (let [potential-duplicates (potential-duplicate-ids db args) + query (cond-> {:query {:find [] :in ['$ ] :where []} :args [db]} + (:potential-duplicates args) + (merge-query {:query {:in '[[?e ...]]} + :args [potential-duplicates]}) (:exact-match-id args) (merge-query {:query {:in ['?e] diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index c142c840..ed6aacc3 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -343,24 +343,7 @@ :forecast_match {:fields {:id {:type :id} :identifier {:type 'String}}} - :transaction {:fields {:id {:type :id} - :amount {:type 'String} - :description_original {:type 'String} - :description_simple {:type 'String} - :location {:type 'String} - :forecast_match {:type :forecast_match} - :status {:type 'String} - :yodlee_merchant {:type :yodlee_merchant} - :client {:type :client} - :accounts {:type '(list :invoices_expense_accounts)} - :payment {:type :payment} - :expected_deposit {:type :expected_deposit} - :vendor {:type :vendor} - :bank_account {:type :bank_account} - :date {:type 'String} - :post_date {:type 'String} - :approval_status {:type :transaction_approval_status} - :matched_rule {:type :transaction_rule}}} + :transaction_rule {:fields {:id {:type :id} :note {:type 'String} @@ -455,11 +438,7 @@ :start {:type 'Int} :end {:type 'Int}}} - :transaction_page {:fields {:data {:type '(list :transaction)} - :count {:type 'Int} - :total {:type 'Int} - :start {:type 'Int} - :end {:type 'Int}}} + :transaction_rule_page {:fields {:transaction_rules {:type '(list :transaction_rule)} :count {:type 'Int} @@ -531,6 +510,12 @@ {:expense_account_stats {:type '(list :expense_account_stat) :args {:client_id {:type :id}} :resolve :get-expense-account-stats} + :potential_transaction_rule_matches {:type '(list :transaction_rule) + :args {:transaction_id {:type :id}} + :resolve :get-transaction-rule-matches} + :potential_payment_matches {:type '(list :payment) + :args {:transaction_id {:type :id}} + :resolve :get-potential-payments} :test_transaction_rule {:type '(list :transaction) :args {:transaction_rule {:type :edit_transaction_rule}} @@ -547,21 +532,6 @@ :cash_flow {:type :cash_flow_result :args {:client_id {:type :id}} :resolve :get-cash-flow} - - :potential_payment_matches {:type '(list :payment) - :args {:transaction_id {:type :id}} - :resolve :get-potential-payments} - - :potential_autopay_invoices_matches {:type '(list (list :invoice)) - :args {:transaction_id {:type :id}} - :resolve :get-potential-autopay-invoices-matches} - :potential_unpaid_invoices_matches {:type '(list (list :invoice)) - :args {:transaction_id {:type :id}} - :resolve :get-potential-unpaid-invoices-matches} - - :potential_transaction_rule_matches {:type '(list :transaction_rule) - :args {:transaction_id {:type :id}} - :resolve :get-transaction-rule-matches} :balance_sheet {:type :balance_sheet :args {:client_id {:type :id} :include_comparison {:type 'Boolean} @@ -637,10 +607,7 @@ :args {} :resolve :get-intuit-bank-accounts} - :transaction_page {:type :transaction_page - :args {:filters {:type :transaction_filters}} - - :resolve :get-transaction-page} + @@ -715,23 +682,7 @@ :sort_name {:type 'String} :asc {:type 'Boolean}}} - :transaction_filters {:fields {:client_id {:type :id} - :exact_match_id {:type :id} - :import_batch_id {:type :id} - - :vendor_id {:type :id} - :bank_account_id {:type :id} - :account_id {:type :id} - :date_range {:type :date_range} - :location {:type 'String} - :amount_lte {:type :money} - :amount_gte {:type :money} - :description {:type 'String} - :start {:type 'Int} - :per_page {:type 'Int} - :sort {:type '(list :sort_item)} - :approval_status {:type :transaction_approval_status} - :unresolved {:type 'Boolean}}} + :ledger_filters {:fields {:client_id {:type :id} @@ -902,12 +853,7 @@ :scheduled_payment {:type :iso_date} :due {:type :iso_date} :total {:type :money}}} - :edit_transaction - {:fields {:id {:type :id} - :vendor_id {:type :id} - :forecast_match {:type :id} - :approval_status {:type :transaction_approval_status} - :accounts {:type '(list :edit_expense_account)}}} + :edit_percentage_account {:fields {:id {:type :id} @@ -976,11 +922,7 @@ {:enum-value :liability} {:enum-value :equity} {:enum-value :revenue}]} - :transaction_approval_status {:values [{:enum-value :approved} - {:enum-value :unapproved} - {:enum-value :suppressed} - {:enum-value :requires_feedback} - {:enum-value :excluded}]}} + } :mutations {:request_import {:type 'String :args {:which {:type 'String}} @@ -994,20 +936,13 @@ :args {:invoices {:type '(list :id)}} :resolve :mutation/approve-invoices} - :bulk_change_transaction_status {:type :message - :args {:filters {:type :transaction_filters} - :status {:type :transaction_approval_status} - :ids {:type '(list :id)}} - :resolve :mutation/bulk-change-transaction-status} + :delete_external_ledger {:type :message :args {:filters {:type :ledger_filters} :ids {:type '(list :id)}} :resolve :mutation/delete-external-ledger} - :delete_transactions {:type :message - :args {:filters {:type :transaction_filters} - :ids {:type '(list :id)}} - :resolve :mutation/delete-transactions} + :delete_transaction_rule {:type :id :args {:transaction_rule_id {:type :id}} :resolve :mutation/delete-transaction-rule} @@ -1063,34 +998,7 @@ :upsert_account {:type :account :args {:account {:type :edit_account}} :resolve :mutation/upsert-account} - :edit_transaction {:type :transaction - :args {:transaction {:type :edit_transaction}} - :resolve :mutation/edit-transaction} - :match_transaction {:type :transaction - :args {:transaction_id {:type :id} - :payment_id {:type :id}} - :resolve :mutation/match-transaction} - - :match_transaction_autopay_invoices {:type :transaction - :args {:transaction_id {:type :id} - :autopay_invoice_ids {:type '(list :id)}} - :resolve :mutation/match-transaction-autopay-invoices} - - :match_transaction_unpaid_invoices {:type :transaction - :args {:transaction_id {:type :id} - :unpaid_invoice_ids {:type '(list :id)}} - :resolve :mutation/match-transaction-unpaid-invoices} - - :match_transaction_rules {:type '(list :transaction) - :args {:transaction_ids {:type '(list :id)} - :all {:type 'Boolean} - :transaction_rule_id {:type :id}} - :resolve :mutation/match-transaction-rules} - - :unlink_transaction {:type :transaction - :args {:transaction_id {:type :id}} - :resolve :mutation/unlink-transaction} :void_invoice {:type :invoice :args {:invoice_id {:type :id}} :resolve :mutation/void-invoice} @@ -1346,10 +1254,7 @@ :get-all-sales-orders get-all-sales-orders :get-payment-page gq-checks/get-payment-page :get-potential-payments gq-checks/get-potential-payments - :get-potential-autopay-invoices-matches gq-transactions/get-potential-autopay-invoices-matches - :get-potential-unpaid-invoices-matches gq-transactions/get-potential-unpaid-invoices-matches :get-accounts gq-accounts/get-accounts - :get-transaction-page gq-transactions/get-transaction-page :get-ledger-page gq-ledger/get-ledger-page :get-sales-order-page gq-sales-orders/get-sales-orders-page :get-balance-sheet gq-ledger/get-balance-sheet @@ -1372,18 +1277,10 @@ :mutation/add-invoice gq-invoices/add-invoice :mutation/add-and-print-invoice gq-invoices/add-and-print-invoice :mutation/edit-invoice gq-invoices/edit-invoice - :mutation/edit-transaction gq-transactions/edit-transaction - :mutation/unlink-transaction gq-transactions/unlink-transaction - :mutation/bulk-change-transaction-status gq-transactions/bulk-change-status :mutation/delete-external-ledger gq-ledger/delete-external-ledger - :mutation/delete-transactions gq-transactions/delete-transactions :mutation/upsert-transaction-rule gq-transaction-rules/upsert-transaction-rule :test-transaction-rule gq-transaction-rules/test-transaction-rule :run-transaction-rule gq-transaction-rules/run-transaction-rule - :mutation/match-transaction gq-transactions/match-transaction - :mutation/match-transaction-autopay-invoices gq-transactions/match-transaction-autopay-invoices - :mutation/match-transaction-unpaid-invoices gq-transactions/match-transaction-unpaid-invoices - :mutation/match-transaction-rules gq-transactions/match-transaction-rules :mutation/edit-client gq-clients/edit-client :mutation/upsert-vendor gq-vendors/upsert-vendor :mutation/upsert-account gq-accounts/upsert-account @@ -1398,6 +1295,7 @@ :get-vendor gq-vendors/get-graphql}) gq-plaid/attach gq-import-batches/attach + gq-transactions/attach schema/compile)) diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index 02e3840d..3c1ec465 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -4,33 +4,54 @@ [audit-transact audit-transact-batch conn remove-nils]] [auto-ap.datomic.accounts :as a] [auto-ap.datomic.checks :as d-checks] + [auto-ap.datomic.invoices :as d-invoices] [auto-ap.datomic.transaction-rules :as tr] [auto-ap.datomic.transactions :as d-transactions] [auto-ap.graphql.transaction-rules :as g-tr] - [auto-ap.import.transactions :as i-transactions] [auto-ap.graphql.utils :refer [->graphql <-graphql assert-admin - assert-power-user assert-can-see-client + assert-power-user enum->keyword ident->enum-f snake->kebab]] + [auto-ap.import.transactions :as i-transactions] [auto-ap.rule-matching :as rm] [auto-ap.utils :refer [dollars=]] [clj-time.coerce :as coerce] [clojure.set :as set] [clojure.string :as str] [clojure.tools.logging :as log] - [datomic.api :as d] - [auto-ap.datomic.invoices :as d-invoices])) + [com.walmartlabs.lacinia.util :refer [attach-resolvers]] + [datomic.api :as d])) (def approval-status->graphql (ident->enum-f :transaction/approval-status)) +(defn assert-filtered-enough [filters] + (when (:potential_duplicates filters) + (assert-admin (:id filters)) + (when-not (:bank_account_id filters) + (throw (ex-info "In order to select potential duplicates, you must choose a bank account." + {:validation-error "In order to select potential duplicates, you must choose a bank account."}))) + (when-not (seq (->> filters + (filter (fn [[k v]] + (not (nil? v)))) + (filter (comp (complement #{:id :start :sort :client_id :bank_account_id :potential_duplicates :per_page}) + first)) + (filter (fn [[k v]] + (if (= :date_range k) + (and (some? (:start v)) + (some? (:end v))) + true))))) + (throw (ex-info "In order to select potential duplicates, you must filter your view more." + {:validation-error "In order to select potential duplicates, you must filter your view more."}))))) + (defn get-transaction-page [context args value] (let [args (assoc (:filters args) :id (:id context)) + _ (assert-filtered-enough args) [transactions transactions-count] (d-transactions/get-graphql (update (<-graphql args) :approval-status enum->keyword "transaction-approval-status")) transactions (map ->graphql (map approval-status->graphql transactions))] {:data transactions @@ -81,13 +102,16 @@ (mapcat (fn [i] (let [transaction (d/entity db i) payment-id (-> transaction :transaction/payment :db/id) - expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)] - (cond->> [{:db/id i - :transaction/approval-status :transaction-approval-status/suppressed} + expected-deposit-id (-> transaction :transaction/expected-deposit :db/id) + transaction-tx (if (:suppress args) + {:db/id i + :transaction/approval-status :transaction-approval-status/suppressed} + [:db/retractEntity i])] + (cond->> [transaction-tx [:db/retractEntity [:journal-entry/original-entity i]]] payment-id (into [{:db/id payment-id - :payment/status :payment-status/pending} - [:db/retract (:db/id transaction) :transaction/payment payment-id]]) + :payment/status :payment-status/pending} + [:db/retract (:db/id transaction) :transaction/payment payment-id]]) expected-deposit-id (into [{:db/id expected-deposit-id :expected-deposit/status :expected-deposit-status/pending} [:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]])))) @@ -440,3 +464,135 @@ conj [] transaction_ids )) + +(def objects + {:transaction {:fields {:id {:type :id} + :amount {:type 'String} + :description_original {:type 'String} + :description_simple {:type 'String} + :location {:type 'String} + :forecast_match {:type :forecast_match} + :status {:type 'String} + :yodlee_merchant {:type :yodlee_merchant} + :client {:type :client} + :accounts {:type '(list :invoices_expense_accounts)} + :payment {:type :payment} + :expected_deposit {:type :expected_deposit} + :vendor {:type :vendor} + :bank_account {:type :bank_account} + :date {:type 'String} + :post_date {:type 'String} + :approval_status {:type :transaction_approval_status} + :matched_rule {:type :transaction_rule}}} + :transaction_page {:fields {:data {:type '(list :transaction)} + :count {:type 'Int} + :total {:type 'Int} + :start {:type 'Int} + :end {:type 'Int}}}}) + +(def queries + {:potential_autopay_invoices_matches {:type '(list (list :invoice)) + :args {:transaction_id {:type :id}} + :resolve :get-potential-autopay-invoices-matches} + :potential_unpaid_invoices_matches {:type '(list (list :invoice)) + :args {:transaction_id {:type :id}} + :resolve :get-potential-unpaid-invoices-matches} + :transaction_page {:type :transaction_page + :args {:filters {:type :transaction_filters}} + :resolve :get-transaction-page}}) + +(def mutations + {:bulk_change_transaction_status {:type :message + :args {:filters {:type :transaction_filters} + :status {:type :transaction_approval_status} + :ids {:type '(list :id)}} + :resolve :mutation/bulk-change-transaction-status} + :delete_transactions {:type :message + :args {:filters {:type :transaction_filters} + :ids {:type '(list :id)} + :suppress {:type 'Boolean}} + :resolve :mutation/delete-transactions} + :edit_transaction {:type :transaction + :args {:transaction {:type :edit_transaction}} + :resolve :mutation/edit-transaction} + + :match_transaction {:type :transaction + :args {:transaction_id {:type :id} + :payment_id {:type :id}} + :resolve :mutation/match-transaction} + + :match_transaction_autopay_invoices {:type :transaction + :args {:transaction_id {:type :id} + :autopay_invoice_ids {:type '(list :id)}} + :resolve :mutation/match-transaction-autopay-invoices} + + :match_transaction_unpaid_invoices {:type :transaction + :args {:transaction_id {:type :id} + :unpaid_invoice_ids {:type '(list :id)}} + :resolve :mutation/match-transaction-unpaid-invoices} + + :unlink_transaction {:type :transaction + :args {:transaction_id {:type :id}} + :resolve :mutation/unlink-transaction} + + :match_transaction_rules {:type '(list :transaction) + :args {:transaction_ids {:type '(list :id)} + :all {:type 'Boolean} + :transaction_rule_id {:type :id}} + :resolve :mutation/match-transaction-rules}}) + +(def input-objects + {:transaction_filters {:fields {:client_id {:type :id} + :exact_match_id {:type :id} + :import_batch_id {:type :id} + :potential_duplicates {:type 'Boolean} + :vendor_id {:type :id} + :bank_account_id {:type :id} + :account_id {:type :id} + :date_range {:type :date_range} + :location {:type 'String} + :amount_lte {:type :money} + :amount_gte {:type :money} + :description {:type 'String} + :start {:type 'Int} + :per_page {:type 'Int} + :sort {:type '(list :sort_item)} + :approval_status {:type :transaction_approval_status} + :unresolved {:type 'Boolean}}} + :edit_transaction + {:fields {:id {:type :id} + :vendor_id {:type :id} + :forecast_match {:type :id} + :approval_status {:type :transaction_approval_status} + :accounts {:type '(list :edit_expense_account)}}}}) + +(def enums + {:transaction_approval_status {:values [{:enum-value :approved} + {:enum-value :unapproved} + {:enum-value :suppressed} + {:enum-value :requires_feedback} + {:enum-value :excluded}]}}) + +(def resolvers + {:get-transaction-page get-transaction-page + :get-potential-autopay-invoices-matches get-potential-autopay-invoices-matches + :get-potential-unpaid-invoices-matches get-potential-unpaid-invoices-matches + :mutation/edit-transaction edit-transaction + :mutation/unlink-transaction unlink-transaction + :mutation/bulk-change-transaction-status bulk-change-status + :mutation/delete-transactions delete-transactions + :mutation/match-transaction match-transaction + :mutation/match-transaction-autopay-invoices match-transaction-autopay-invoices + :mutation/match-transaction-unpaid-invoices match-transaction-unpaid-invoices + :mutation/match-transaction-rules match-transaction-rules}) + + +(defn attach [schema] + (-> + (merge-with merge schema + {:objects objects + :queries queries + :mutations mutations + :input-objects input-objects + :enums enums}) + (attach-resolvers resolvers))) diff --git a/src/cljs/auto_ap/reload.cljs b/src/cljs/auto_ap/reload.cljs index 3a487445..123bd257 100644 --- a/src/cljs/auto_ap/reload.cljs +++ b/src/cljs/auto_ap/reload.cljs @@ -1,5 +1,5 @@ (ns ^:figwheel-hooks auto-ap.reload) (defn ^:after-load reload [] - (println "HERE") + (println "RELOADING") (@(resolve 'auto-ap.core/mount-root))) diff --git a/src/cljs/auto_ap/views/pages/transactions.cljs b/src/cljs/auto_ap/views/pages/transactions.cljs index d323efd1..44a84143 100644 --- a/src/cljs/auto_ap/views/pages/transactions.cljs +++ b/src/cljs/auto_ap/views/pages/transactions.cljs @@ -34,6 +34,7 @@ :amount-gte (:amount-gte (:amount-range params)) :exact-match-id (some-> (:exact-match-id params) str) :unresolved (:unresolved params) + :potential-duplicates (:potential-duplicates params) :location (:location params) :import-batch-id (some-> (:import-batch-id params) str) :amount-lte (:amount-lte (:amount-range params)) @@ -92,7 +93,7 @@ (re-frame/reg-event-fx ::delete-selected - (fn [cofx [_ params]] + (fn [cofx [_ params suppress]] (let [checked @(re-frame/subscribe [::data-page/checked ::page]) checked-params (get checked "header") specific-transactions (map :id (vals (dissoc checked "header")))] @@ -106,7 +107,8 @@ :venia/queries [{:query/data [:delete-transactions {:filters (some-> checked-params data-params->query-params) - :ids specific-transactions} + :ids specific-transactions + :suppress suppress} [:message]]}]} :on-success (fn [result] [::params-change params])} @@ -192,7 +194,12 @@ :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::bulk-change-transaction-status])) (not (seq checked)))} "Client Review"] - [:button.button.is-danger {:on-click (dispatch-event [::delete-selected params]) + [:button.button.is-danger {:on-click (dispatch-event [::delete-selected params false]) + :class (status/class-for @(re-frame/subscribe [::status/single ::delete-selected])) + :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::delete-selected])) + (not (seq checked)))} + "Delete selected"] + [:button.button.is-danger {:on-click (dispatch-event [::delete-selected params true]) :class (status/class-for @(re-frame/subscribe [::status/single ::delete-selected])) :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::delete-selected])) (not (seq checked)))} diff --git a/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs b/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs index 242fbc75..03678058 100644 --- a/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs @@ -119,12 +119,7 @@ (when (= "admin" (:user/role user)) [:<> - (when-let [import-batch-id @(re-frame/subscribe [::data-page/filter data-page :import-batch-id])] - [:div - [:p.menu-label "Import Batch"] - [:span.tag.is-medium import-batch-id " " - [:button.delete.is-small {:on-click - (dispatch-event [::data-page/filter-changed data-page :import-batch-id nil])}]]]) + [:p.menu-label "Admin only"] [:div [switch-field {:id "unresolved-only" @@ -132,5 +127,20 @@ :on-change (fn [e] (re-frame/dispatch [::data-page/filter-changed data-page :unresolved (.-checked (.-target e))])) :label "Unresolved only" + :type "checkbox"}]] + + (when-let [import-batch-id @(re-frame/subscribe [::data-page/filter data-page :import-batch-id])] + [:div + [:p.menu-label "Import Batch"] + [:span.tag.is-medium import-batch-id " " + [:button.delete.is-small {:on-click + (dispatch-event [::data-page/filter-changed data-page :import-batch-id nil])}]]]) + + [:div + [switch-field {:id "potentially-duplicate" + :checked (boolean @(re-frame/subscribe [::data-page/filter data-page :potential-duplicates])) + :on-change (fn [e] + (re-frame/dispatch [::data-page/filter-changed data-page :potential-duplicates (.-checked (.-target e))])) + :label "Same Amount + Date" :type "checkbox"}]]])]))