diff --git a/src/clj/auto_ap/graphql/invoices.clj b/src/clj/auto_ap/graphql/invoices.clj index 48d0aa59..c16e9194 100644 --- a/src/clj/auto_ap/graphql/invoices.clj +++ b/src/clj/auto_ap/graphql/invoices.clj @@ -9,10 +9,11 @@ [auto-ap.time :refer [parse iso-date]] [auto-ap.utils :refer [dollars=]] [datomic.api :as d] - [auto-ap.datomic :refer [uri remove-nils audit-transact]] + [auto-ap.datomic :refer [uri remove-nils audit-transact conn]] [clj-time.coerce :as coerce] [clj-time.core :as time] - [clojure.set :as set])) + [clojure.set :as set] + [clojure.tools.logging :as log])) (defn get-invoice-page [context args value] @@ -51,6 +52,8 @@ (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number :validation-error (str "Invoice '" invoice_number "' already exists.")})))) (defn expense-account->entity [{:keys [id account_id amount location]}] + + (remove-nils #:invoice-expense-account {:amount (Double/parseDouble amount) :db/id id :account account_id @@ -90,16 +93,37 @@ set)] (set/difference existing-ids specified-ids))) +(defn assert-valid-expense-accounts [expense_accounts] + (doseq [expense-account expense_accounts + :let [account (d/entity (d/db conn) (:account_id expense-account))]] + (log/info "ACCOUNT" (:account/location account) ) + (when (empty? (:location expense-account)) + (throw (ex-info "Expense account is missing location" {:validation-error "Expense account is missing location"}))) + + (when (and (not (empty? (:account/location account))) + (not= (:location expense-account) + (:account/location account))) + (let [err (str "Account uses location '" (:location expense-account) "' but expects '" (:account/location account) "'")] + (throw (ex-info err + {:validation-error err})))) + + (when (and (empty? (:account/location account)) + (= "A" (:location expense-account))) + (let [err (str "Account uses location '" (:location expense-account) "', which is reserved for liabilities, equities, and assets.")] + (throw (ex-info err + {:validation-error err})))) + + + (when (nil? (:account_id expense-account)) + (throw (ex-info "Expense account is missing account" {:validation-error "Expense account is missing account"}))))) + (defn add-invoice [context {{:keys [total expense_accounts invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value] (assert-no-conflicting in) (assert-can-see-client (:id context) client_id) - (doseq [expense-account expense_accounts] - (when (empty? (:location expense-account)) - (throw (ex-info "Expense account is missing location" {:validation-error "Expense account is missing location"}))) - (when (nil? (:account_id expense-account)) - (throw (ex-info "Expense account is missing account" {:validation-error "Expense account is missing account"})))) + (assert-valid-expense-accounts expense_accounts) + (let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))] (-> (d-invoices/get-by-id (get-in transaction-result [:tempids "invoice"])) (->graphql)))) @@ -112,6 +136,7 @@ (assert-no-conflicting in) (assert-can-see-client (:id context) client_id) (assert-bank-account-belongs client_id bank-account-id) + (assert-valid-expense-accounts (:expense_accounts in)) (let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))] (-> (gq-checks/print-checks [{:invoice-id (get-in transaction-result [:tempids "invoice"]) :amount total}] @@ -137,6 +162,8 @@ paid-amount (- (:invoice/total invoice) (:invoice/outstanding-balance invoice)) _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice))) deleted (deleted-expense-accounts invoice expense_accounts) + _ (assert-valid-expense-accounts expense_accounts) + updated-invoice (cond-> {:db/id id :invoice/invoice-number invoice_number diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index f98f924d..9a67e5e9 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -1,24 +1,28 @@ (ns auto-ap.graphql.transactions - (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin ident->enum-f snake->kebab enum->keyword]] - [auto-ap.datomic.transactions :as d-transactions] - [auto-ap.datomic.vendors :as d-vendors] + (:require [auto-ap.datomic + :refer + [audit-transact audit-transact-batch conn remove-nils]] + [auto-ap.datomic.accounts :as a] [auto-ap.datomic.checks :as d-checks] + [auto-ap.datomic.transaction-rules :as tr] + [auto-ap.datomic.transactions :as d-transactions] [auto-ap.graphql.transaction-rules :as g-tr] - [datomic.api :as d] - [auto-ap.datomic :refer [uri remove-nils audit-transact audit-transact-batch]] - [com.walmartlabs.lacinia :refer [execute]] - [com.walmartlabs.lacinia.executor :as executor] - [com.walmartlabs.lacinia.resolve :as resolve] - [auto-ap.utils :refer [by dollars=]] - [auto-ap.time :refer [parse normal-date]] - [auto-ap.datomic.clients :as d-clients] + [auto-ap.graphql.utils + :refer + [->graphql + <-graphql + assert-admin + assert-can-see-client + enum->keyword + ident->enum-f + snake->kebab]] + [auto-ap.rule-matching :as rm] + [auto-ap.utils :refer [dollars=]] + [clj-time.coerce :as coerce] [clojure.set :as set] [clojure.string :as str] - [auto-ap.datomic.accounts :as a] - [auto-ap.datomic.transaction-rules :as tr] - [auto-ap.rule-matching :as rm] - [clj-time.coerce :as coerce] - [clojure.tools.logging :as log])) + [clojure.tools.logging :as log] + [datomic.api :as d])) (def approval-status->graphql (ident->enum-f :transaction/approval-status)) @@ -93,9 +97,33 @@ set)] (set/difference existing-ids specified-ids))) +(defn assert-valid-expense-accounts [accounts] + (doseq [trans-account accounts + :let [account (d/entity (d/db conn) (:account_id trans-account))]] + (when (empty? (:location trans-account)) + (throw (ex-info "Account is missing location" {:validation-error "Account is missing location"}))) + + (when (and (not (empty? (:account/location account))) + (not= (:location trans-account) + (:account/location account))) + (let [err (str "Account uses location '" (:location trans-account) "' but expects '" (:account/location account) "'")] + (throw (ex-info err + {:validation-error err})))) + + (when (and (empty? (:account/location account)) + (= "A" (:location trans-account))) + (let [err (str "Account uses location '" (:location trans-account) "', which is reserved for liabilities, equities, and assets.")] + (throw (ex-info err + {:validation-error err})))) + + + (when (nil? (:account_id trans-account)) + (throw (ex-info "Account is missing account" {:validation-error "Account is missing account"}))))) + (defn edit-transaction [context {{:keys [id accounts vendor_id approval_status forecast_match] :as transaction} :transaction} value] (let [existing-transaction (d-transactions/get-by-id id) _ (assert-can-see-client (:id context) (:transaction/client existing-transaction) ) + _ (assert-valid-expense-accounts accounts) deleted (deleted-accounts existing-transaction accounts) account-total (reduce + 0 (map (fn [x] (Double/parseDouble (:amount x))) accounts)) missing-locations (seq (set/difference diff --git a/src/cljs/auto_ap/views/pages/transactions/form.cljs b/src/cljs/auto_ap/views/pages/transactions/form.cljs index 6cb0b3f7..be8ee2ae 100644 --- a/src/cljs/auto_ap/views/pages/transactions/form.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/form.cljs @@ -190,113 +190,114 @@ (let [change-event [::forms/change ::form] {:keys [data] } @(re-frame/subscribe [::forms/form ::form]) locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))]) - {:keys [form field raw-field error-notification submit-button ]} transaction-form + {:keys [form-inline form field raw-field error-notification submit-button ]} transaction-form is-admin? @(re-frame/subscribe [::subs/is-admin?]) should-disable-for-client? (and (not is-admin?) (not= :requires-feedback (:original-status data)))] - [form {:title "Transaction"} + (form-inline {:title "Transaction"} + [:<> - (when (and @(re-frame/subscribe [::subs/is-admin?]) - (get-in data [:yodlee-merchant])) - [:div.control - [:p.help "Merchant"] - [:input.input {:type "text" - :disabled true - :value (str (get-in data [:yodlee-merchant :name]) - " - " - (get-in data [:yodlee-merchant :yodlee-id]))}]]) + (when (and @(re-frame/subscribe [::subs/is-admin?]) + (get-in data [:yodlee-merchant])) + [:div.control + [:p.help "Merchant"] + [:input.input {:type "text" + :disabled true + :value (str (get-in data [:yodlee-merchant :name]) + " - " + (get-in data [:yodlee-merchant :yodlee-id]))}]]) - (when is-admin? - [field "Matched Rule" - [:input.input {:type "text" - :field [:matched-rule :note] - :disabled "disabled"}]]) - [field "Amount" - [:input.input {:type "text" - :field [:amount] - :disabled "disabled"}]] - [field "Description" - [:input.input {:type "text" - :field [:description-original] - :disabled "disabled"}]] + (when is-admin? + (field "Matched Rule" + [:input.input {:type "text" + :field [:matched-rule :note] + :disabled "disabled"}])) + (field "Amount" + [:input.input {:type "text" + :field [:amount] + :disabled "disabled"}]) + (field "Description" + [:input.input {:type "text" + :field [:description-original] + :disabled "disabled"}]) - (cond - (and (seq (:potential-transaction-rule-matches data)) - (not (:matched-rule data)) - (not (:payment data)) - is-admin?) - [potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}] + (cond + (and (seq (:potential-transaction-rule-matches data)) + (not (:matched-rule data)) + (not (:payment data)) + is-admin?) + [potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}] - (and (seq (:potential-payment-matches data)) - (not (:payment data)) - is-admin?) - [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}] + (and (seq (:potential-payment-matches data)) + (not (:payment data)) + is-admin?) + [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}] - (and (not (seq (:potential-payment-matches data))) - (not (seq (:potential-transaction-rule-matches data)))) - [:div - [field "Vendor" - [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) - :match->text :name - :type "typeahead-entity" - :auto-focus true - :field [:vendor] - :disabled (or (boolean (:payment data)) - should-disable-for-client?)}]] - [field nil - [expense-accounts-field - {:type "expense-accounts" - :field [:accounts] - :max (Math/abs (js/parseFloat (:amount data))) - :descriptor "credit account" - :disabled (or (boolean (:payment data)) - should-disable-for-client?) - :locations locations}]] + (and (not (seq (:potential-payment-matches data))) + (not (seq (:potential-transaction-rule-matches data)))) + [:div + (field "Vendor" + [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) + :match->text :name + :type "typeahead-entity" + :auto-focus true + :field [:vendor] + :disabled (or (boolean (:payment data)) + should-disable-for-client?)}]) + (field nil + [expense-accounts-field + {:type "expense-accounts" + :field [:accounts] + :max (Math/abs (js/parseFloat (:amount data))) + :descriptor "credit account" + :disabled (or (boolean (:payment data)) + should-disable-for-client?) + :locations locations}]) - + - [field "Approval Status" - [button-radio - {:type "button-radio" - :field [:approval-status] - :options [[:unapproved "Unapproved"] - [:requires-feedback "Client Review"] - [:approved "Approved"] - [:excluded "Excluded from Ledger"]] - :disabled should-disable-for-client?}]] + (field "Approval Status" + [button-radio + {:type "button-radio" + :field [:approval-status] + :options [[:unapproved "Unapproved"] + [:requires-feedback "Client Review"] + [:approved "Approved"] + [:excluded "Excluded from Ledger"]] + :disabled should-disable-for-client?}]) - [field "Forecasted-transaction" - [typeahead-entity {:matches (doto @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) println) - :match->text :identifier - :type "typeahead-entity" - :field [:forecast-match]}]] + (field "Forecasted-transaction" + [typeahead-entity {:matches @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) + :match->text :identifier + :type "typeahead-entity" + :field [:forecast-match]}]) - - [error-notification] - (when-not should-disable-for-client? - [submit-button "Save"])] + + (error-notification) + (when-not should-disable-for-client? + (submit-button "Save"))] - :else - - [:div - [field "Approval Status" - [button-radio - {:type "button-radio" - :field [:approval-status] - :options [[:unapproved "Unapproved"] - [:requires-feedback "Client Review"] - [:approved "Approved"] - [:excluded "Excluded from Ledger"]] - :disabled should-disable-for-client?}]] + :else + + [:div + (field "Approval Status" + [button-radio + {:type "button-radio" + :field [:approval-status] + :options [[:unapproved "Unapproved"] + [:requires-feedback "Client Review"] + [:approved "Approved"] + [:excluded "Excluded from Ledger"]] + :disabled should-disable-for-client?}]) - [field "Forecasted-transaction" - [typeahead-entity {:matches (doto @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) println) - :match->text :identifier - :type "typeahead-entity" - :field [:forecast-match]}]] + (field "Forecasted-transaction" + [typeahead-entity {:matches (doto @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) println) + :match->text :identifier + :type "typeahead-entity" + :field [:forecast-match]}]) - [error-notification] - (when-not should-disable-for-client? - [submit-button "Save"])])])]) + (error-notification) + (when-not should-disable-for-client? + (submit-button "Save"))])]))])