diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 6034a9da..7daf6ba1 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -420,6 +420,7 @@ :import_ledger_result {:fields {:successful {:type '(list :import_ledger_entry_result)} :existing {:type '(list :import_ledger_entry_result)} + :ignored {:type '(list :import_ledger_entry_result)} :errors {:type '(list :import_ledger_entry_result)} }} diff --git a/src/clj/auto_ap/graphql/ledger.clj b/src/clj/auto_ap/graphql/ledger.clj index a4c36567..438f2498 100644 --- a/src/clj/auto_ap/graphql/ledger.clj +++ b/src/clj/auto_ap/graphql/ledger.clj @@ -201,7 +201,9 @@ (try (f entry) (catch Exception e - (assoc entry :error (.getMessage e)))))) + (assoc entry :error (.getMessage e) + :status (or (:status (ex-data e)) + :error)))))) (defn delete-external-ledger [context args value] (let [_ (assert-admin (:id context)) @@ -274,91 +276,104 @@ lis))))] (let [vendor (all-vendors (:vendor_name entry))] (when-not (all-clients (:client_code entry)) - (throw (Exception. (str "Client '" (:client_code entry )"' not found.")) )) + (throw (ex-info (str "Client '" (:client_code entry )"' not found.") {:status :error}) )) (when-not vendor - (throw (Exception. (str "Vendor '" (:vendor_name entry) "' not found.")))) + (throw (ex-info (str "Vendor '" (:vendor_name entry) "' not found.") {:status :error}))) (when-not (re-find #"\d{1,2}/\d{1,2}/\d{4}" (:date entry)) - (throw (Exception. (str "Date must be MM/dd/yyyy")))) + (throw (ex-info (str "Date must be MM/dd/yyyy") {:status :error}))) (when-not (dollars= (reduce + 0.0 (map :debit (:line_items entry))) (reduce + 0.0 (map :credit (:line_items entry)))) - (throw (Exception. (str "Debits '" - (reduce + 0 (map :debit (:line_items entry))) - "' and credits '" - (reduce + 0 (map :credit (:line_items entry))) - "' do not add up.")))) + (throw (ex-info (str "Debits '" + (reduce + 0 (map :debit (:line_items entry))) + "' and credits '" + (reduce + 0 (map :credit (:line_items entry))) + "' do not add up.") + {:status :error}))) (when (dollars= (reduce + 0.0 (map :debit (:line_items entry))) 0.0) - (throw (Exception. (str "Cannot have ledger entries that total $0.00")))) - (remove-nils - {:journal-entry/source (:source entry) - :journal-entry/client [:client/code (:client_code entry)] - :journal-entry/date (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))) - :journal-entry/external-id (:external_id entry) - :journal-entry/vendor (:db/id (doto (all-vendors (:vendor_name entry)) - println)) - :journal-entry/amount (:amount entry) - :journal-entry/note (:note entry) - :journal-entry/cleared-against (:cleared_against entry) + (throw (ex-info (str "Cannot have ledger entries that total $0.00") + {:status :ignored}))) + (assoc entry + :status :success + :tx + (remove-nils + {:journal-entry/source (:source entry) + :journal-entry/client [:client/code (:client_code entry)] + :journal-entry/date (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))) + :journal-entry/external-id (:external_id entry) + :journal-entry/vendor (:db/id (all-vendors (:vendor_name entry))) + :journal-entry/amount (:amount entry) + :journal-entry/note (:note entry) + :journal-entry/cleared-against (:cleared_against entry) - :journal-entry/line-items - (mapv (fn [ea] - (when-not (get - (get all-client-locations (:client_code entry)) - (:location ea)) - (throw (Exception. (str "Location '" (:location ea) "' not found.")))) - (when (and (<= (:debit ea 0.0) 0.0) - (<= (:credit ea 0.0) 0.0)) - (throw (Exception. (str "Line item amount " (or (:debit ea) (:credit ea)) " must be greater than 0.")))) - (when (and (not (all-accounts (:account_identifier ea))) - (not (get - (get all-client-bank-accounts (:client_code entry)) - (:account_identifier ea)))) - (throw (Exception. (str "Account '" (:account_identifier ea) "' not found.")))) - (let [matching-account (when (re-matches #"^[0-9]+$" (:account_identifier ea)) - (a/get-account-by-numeric-code-and-sets (Integer/parseInt (:account_identifier ea)) ["default"]))] - (when (and matching-account - (:account/location matching-account) - (not= (:account/location matching-account) - (:location ea))) - (throw (Exception. (str "Account '" - (:account/numeric-code matching-account) - "' requires location '" - (:account/location matching-account) - "' but got '" - (:location ea) - "'" - )))) + :journal-entry/line-items + (mapv (fn [ea] + (when-not (get + (get all-client-locations (:client_code entry)) + (:location ea)) + (throw (ex-info (str "Location '" (:location ea) "' not found.") + {:status :error}))) + (when (and (<= (:debit ea 0.0) 0.0) + (<= (:credit ea 0.0) 0.0)) + (throw (ex-info (str "Line item amount " (or (:debit ea) (:credit ea)) " must be greater than 0.") + {:status :error}))) + (when (and (not (all-accounts (:account_identifier ea))) + (not (get + (get all-client-bank-accounts (:client_code entry)) + (:account_identifier ea)))) + (throw (ex-info (str "Account '" (:account_identifier ea) "' not found.") + {:status :error}))) + (let [matching-account (when (re-matches #"^[0-9]+$" (:account_identifier ea)) + (a/get-account-by-numeric-code-and-sets (Integer/parseInt (:account_identifier ea)) ["default"]))] + (when (and matching-account + (:account/location matching-account) + (not= (:account/location matching-account) + (:location ea))) + (throw (ex-info (str "Account '" + (:account/numeric-code matching-account) + "' requires location '" + (:account/location matching-account) + "' but got '" + (:location ea) + "'") + {:status :error}))) - (when (and matching-account - (not (:account/location matching-account)) - (= "A" (:location ea))) - (throw (Exception. (str "Account '" - (:account/numeric-code matching-account) - "' cannot use location '" - (:location ea) - "'")))) - (remove-nils (cond-> {:journal-entry-line/location (:location ea) - :journal-entry-line/debit (when (> (:debit ea) 0) - (:debit ea)) - :journal-entry-line/credit (when (> (:credit ea) 0) - (:credit ea))} - matching-account (assoc :journal-entry-line/account (:db/id matching-account)) - (not matching-account) (assoc :journal-entry-line/account [:bank-account/code (:account_identifier ea)]))))) - (:line_items entry)) - - :journal-entry/cleared true}))))) + (when (and matching-account + (not (:account/location matching-account)) + (= "A" (:location ea))) + (throw (ex-info (str "Account '" + (:account/numeric-code matching-account) + "' cannot use location '" + (:location ea) + "'") + {:status :error}))) + (remove-nils (cond-> {:journal-entry-line/location (:location ea) + :journal-entry-line/debit (when (> (:debit ea) 0) + (:debit ea)) + :journal-entry-line/credit (when (> (:credit ea) 0) + (:credit ea))} + matching-account (assoc :journal-entry-line/account (:db/id matching-account)) + (not matching-account) (assoc :journal-entry-line/account [:bank-account/code (:account_identifier ea)]))))) + (:line_items entry)) + + :journal-entry/cleared true})))))) (:entries args))) - errors (filter :error transaction) - success (filter (comp not :error) transaction) - retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:journal-entry/external-id x)]]) + errors (filter #(= (:status %) :error) transaction) + ignored (filter #(= (:status %) :ignored) transaction) + success (filter #(= (:status %) :success) transaction) + retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:external_id x)]]) success)] (log/info "manual ledger import has " (count success) " new rows") (audit-transact-batch retraction (:id context)) - (audit-transact-batch success (:id context)) + (log/info (map :tx success)) + (audit-transact-batch (map :tx success) (:id context)) - {:successful (map (fn [x] {:external_id (:journal-entry/external-id x)}) success) + {:successful (map (fn [x] {:external_id (:external_id x)}) success) + :ignored (map (fn [x] + {:external_id (:external_id x)}) + ignored) :existing [] :errors (map (fn [x] {:external_id (:external_id x) :error (:error x)}) errors)}))) diff --git a/src/clj/auto_ap/graphql/transaction_rules.clj b/src/clj/auto_ap/graphql/transaction_rules.clj index 031f4ff0..864bcd92 100644 --- a/src/clj/auto_ap/graphql/transaction_rules.clj +++ b/src/clj/auto_ap/graphql/transaction_rules.clj @@ -82,9 +82,10 @@ (let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)] (throw (ex-info err {:validation-error err}) ))) - (when (not (get (into #{"Shared"} (:client/locations client)) - (:location a))) - (let [err (str "Account " name " uses location " (:location a) ", but doesn't belong to the client " location)] + (when (and (not location) + (not (get (into #{"Shared"} (:client/locations client)) + (:location a)))) + (let [err (str "Account " name " uses location " (:location a) ", but doesn't belong to the client.")] (throw (ex-info err {:validation-error err}) )))) rule-id (if id id diff --git a/src/cljs/auto_ap/views/pages/ledger/external_import.cljs b/src/cljs/auto_ap/views/pages/ledger/external_import.cljs index f14fbb6c..40613405 100644 --- a/src/cljs/auto_ap/views/pages/ledger/external_import.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/external_import.cljs @@ -62,10 +62,11 @@ (fn [{:keys [db]} [_ result]] (let [successful-set (set (map :external-id (:successful (:import-ledger result)))) error-set (into {} (map (juxt :external-id :error) (:errors (:import-ledger result)))) - existing-set (set (map :external-id (:existing (:import-ledger result))))] + existing-set (set (map :external-id (:existing (:import-ledger result)))) + ignored-set (set (map :external-id (:ignored (:import-ledger result))))] {:db (-> (forms/save-succeeded db ::form ) - (assoc-in [::forms/forms ::form :result] {:errors error-set :success successful-set :existing existing-set}) + (assoc-in [::forms/forms ::form :result] {:errors error-set :success successful-set :existing existing-set :ignored ignored-set}) (update-in [::forms/forms ::form :data :line-items] (fn [lis] (mapv @@ -73,6 +74,9 @@ (cond (successful-set (line->id %)) [:span.icon [:i.fa.fa-check]] + (ignored-set (line->id %)) + [:span.icon [:i.fa.fa-minus-circle]] + (existing-set (line->id %)) "" @@ -91,6 +95,10 @@ (cond (successful-set (line->id %)) :success + + (ignored-set (line->id %)) + :ignored + (existing-set (line->id %)) :existing @@ -111,6 +119,7 @@ {:entries @(re-frame/subscribe [::request])} [[:successful [:external-id]] [:existing [:external-id]] + [:ignored [:external-id]] [:errors [:external-id :error]]]]}]} :on-success [::imported] :on-error [::forms/save-error ::form]}}))) @@ -191,7 +200,10 @@ :on-click (dispatch-event [::importing])} "Import"]]] (when result [:div.notification - "Imported with " (count (:errors result)) " errors, " (count (:success result)) " successful."]) + "Imported with " + (count (:errors result)) " errors, " + (count (:ignored result)) " ignored, " + (count (:success result)) " successful."]) (if @(re-frame/subscribe [::forms/is-loading? ::form]) [:div [:i.icon.fa.fa-spin.fa-spinner]] [:div diff --git a/src/cljs/auto_ap/views/pages/ledger/external_ledger_table.cljs b/src/cljs/auto_ap/views/pages/ledger/external_ledger_table.cljs index f0c838cf..d72931f0 100644 --- a/src/cljs/auto_ap/views/pages/ledger/external_ledger_table.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/external_ledger_table.cljs @@ -38,7 +38,8 @@ [grid/cell {} ] [grid/cell {} ] [grid/cell {} ] - [grid/cell {} (if (:name account) + [grid/cell {} + (if (:name account) [:span.has-tooltip-arrow.has-tooltip-right {:data-tooltip (str "Balance as of this entry: " (nf running-balance ))} (str location ": " (:name account)) ] [:i "unknown"])] diff --git a/test/clj/auto_ap/graphql.clj b/test/clj/auto_ap/graphql.clj index a05ef4f5..e9c9fce1 100644 --- a/test/clj/auto_ap/graphql.clj +++ b/test/clj/auto_ap/graphql.clj @@ -5,17 +5,18 @@ [clojure.test :as t :refer [deftest is testing use-fixtures]] [clj-time.core :as time] [datomic.api :as d] - [auto-ap.datomic :refer [uri]] + [auto-ap.datomic :refer [uri conn]] [buddy.sign.jwt :as jwt] [config.core :refer [env]])) (defn wrap-setup [f] (with-redefs [auto-ap.datomic/uri "datomic:mem://datomic-transactor:4334/invoice"] (d/create-database uri) - (m/-main false) - (f) - (d/release (d/connect uri)) - (d/delete-database uri))) + (with-redefs [auto-ap.datomic/conn (d/connect uri)] + (m/migrate conn) + (f) + (d/release conn) + (d/delete-database uri)))) (defn admin-token [] {:user "TEST ADMIN"