From 4fe52cad5a1f19383d04d518185e8686c6bd2984 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Wed, 17 Apr 2019 18:35:41 -0700 Subject: [PATCH] supports validation and multiple account entering. --- .../datomic/migrate/add_general_ledger.clj | 20 +++++- src/clj/auto_ap/datomic/transactions.clj | 10 ++- src/clj/auto_ap/graphql.clj | 5 +- src/clj/auto_ap/graphql/transactions.clj | 67 ++++++++++++------ src/clj/auto_ap/ledger.clj | 38 +++++----- .../components/expense_accounts_field.cljs | 12 ++-- .../auto_ap/views/components/typeahead.cljs | 18 ++--- .../views/pages/transactions/common.cljs | 2 +- .../views/pages/transactions/form.cljs | 70 +++++++++++-------- src/cljs/auto_ap/views/utils.cljs | 1 + 10 files changed, 156 insertions(+), 87 deletions(-) diff --git a/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj b/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj index c50cebf1..0fa3f4cf 100644 --- a/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj +++ b/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj @@ -76,10 +76,26 @@ ] ) (def add-transaction-account - [[{:db/ident :transaction/account + [[{:db/ident :transaction/accounts + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/isComponent true + :db/doc "The debit(s)/credit(s) for this transaction"} + + {:db/ident :transaction-account/account :db/valueType :db.type/ref :db/cardinality :db.cardinality/one - :db/doc "The debit/credit for this transaction"}]]) + :db/doc "Which account to debit/credit for this transaction"} + + {:db/ident :transaction-account/location + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "Location for this expense account"} + + {:db/ident :transaction-account/amount + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one + :db/doc "How much to debit/credit - must be positive"}]]) (def add-yodlee-merchant [[{:db/ident :yodlee-merchant/name diff --git a/src/clj/auto_ap/datomic/transactions.clj b/src/clj/auto_ap/datomic/transactions.clj index 224704ba..83375bd8 100644 --- a/src/clj/auto_ap/datomic/transactions.clj +++ b/src/clj/auto_ap/datomic/transactions.clj @@ -75,7 +75,10 @@ (->> (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] :transaction/vendor [:db/id :vendor/name] - :transaction/account [:db/id :account/name :account/numeric-code] + :transaction/accounts [:transaction-account/amount + :db/id + :transaction-account/location + {:transaction-account/account [:db/id :account/name :account/numeric-code]}] :transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name]}] ids) (map #(update % :transaction/date c/from-date)) @@ -96,7 +99,10 @@ '[* {:transaction/client [:client/name :db/id :client/code :client/locations] :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id] :transaction/vendor [:db/id :vendor/name] - :transaction/account [:db/id :account/name :account/numeric-code] + :transaction/accounts [:transaction-account/amount + :db/id + :transaction-account/location + { :transaction-account/account [:db/id :account/name :account/numeric-code]}] :transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name]}] id) (update :transaction/date c/from-date) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 66f203fb..3f0c551c 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -163,7 +163,7 @@ :status {:type 'String} :yodlee_merchant {:type :yodlee_merchant} :client {:type :client} - :account {:type :account} + :accounts {:type '(list :invoices_expense_accounts)} :payment {:type :payment} :vendor {:type :vendor} :bank_account {:type :bank_account} @@ -423,8 +423,7 @@ :edit_transaction {:fields {:id {:type :id} :vendor_id {:type :id} - :location {:type 'String} - :account_id {:type :id}}} + :accounts {:type '(list :edit_expense_account)}}} :edit_account {:fields {:id {:type :id} diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index aed4da08..e51dd90f 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -3,13 +3,15 @@ [auto-ap.datomic.transactions :as d-transactions] [auto-ap.datomic.vendors :as d-vendors] [datomic.api :as d] - [auto-ap.datomic :refer [uri]] + [auto-ap.datomic :refer [uri remove-nils]] [com.walmartlabs.lacinia :refer [execute]] [com.walmartlabs.lacinia.executor :as executor] [com.walmartlabs.lacinia.resolve :as resolve] - [auto-ap.utils :refer [by]] + [auto-ap.utils :refer [by dollars=]] [auto-ap.time :refer [parse normal-date]] - [auto-ap.datomic.clients :as d-clients])) + [auto-ap.datomic.clients :as d-clients] + [clojure.set :as set] + [clojure.string :as str])) (defn get-transaction-page [context args value] (let [args (assoc args :id (:id context)) @@ -21,27 +23,50 @@ :start (:start args 0) :end (+ (:start args 0) (count transactions))}])) +(defn transaction-account->entity [{:keys [id account_id amount location]}] + (doto (remove-nils #:transaction-account {:amount (Double/parseDouble amount) + :db/id id + :account account_id + :location location}) + println)) +(defn deleted-accounts [transaction accounts] + (let [current-accounts (:transaction/accounts transaction) + specified-ids (->> accounts + (map :id) + set) + existing-ids (->> current-accounts + (map :db/id) + set)] + (set/difference existing-ids specified-ids))) - -(defn edit-transaction [context {{:keys [id location account_id vendor_id] :as transaction} :transaction} value] - (let [transaction (d-transactions/get-by-id id)] +(defn edit-transaction [context {{:keys [id accounts vendor_id] :as transaction} :transaction} value] + (let [transaction (d-transactions/get-by-id id) + deleted (deleted-accounts transaction accounts) + account-total (reduce + 0 (map (fn [x] (Double/parseDouble (:amount x))) accounts)) + missing-locations (seq (set/difference + (->> (:transaction/accounts transaction) + (map :transaction-account/location) + set) + (-> (:transaction/client transaction) + :client/locations + set + (conj "A") + (conj "HQ"))))] (assert-can-see-client (:id context) (:transaction/client transaction) ) - (when-not (-> (:transaction/client transaction) - :client/locations - set - (conj "A") - (conj "HQ") - (get location)) - (throw (ex-info (str "Location '" location "' not found on client.") {})) - ) + (when-not (dollars= (Math/abs (:transaction/amount transaction)) account-total) + (let [error (str "Expense account total (" account-total ") does not equal transaction total (" (Math/abs (:transaction/amount transaction)) ")")] + (throw (ex-info error {:validation-error error})))) + (when missing-locations + (throw (ex-info (str "Location '" (str/join ", " missing-locations) "' not found on client.") {})) ) @(d/transact (d/connect uri) - [{:db/id id - :transaction/vendor vendor_id - :transaction/location location - :transaction/account account_id}]) - (->graphql (d-transactions/get-by-id id))) - #_(->graphql {:id id - :vendor (d-vendors/get-by-id vendor_id) })) + (concat [(remove-nils {:db/id id + :transaction/vendor vendor_id + :transaction/accounts (map transaction-account->entity accounts) + })] + (map (fn [d] + [:db/retract id :transaction/accounts d]) + deleted))) + (->graphql (d-transactions/get-by-id id)))) diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index 1d94fe51..f116da81 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -5,7 +5,7 @@ (defn datums->impacted-entity [db [e changes]] - (let [entity (d/pull db '[* {:invoice/_expense-accounts [*]}] e) + (let [entity (d/pull db '[* {:invoice/_expense-accounts [:db/id] :transaction/_accounts [:db/id]}] e) namespaces (->> changes (map :a) (map namespace) @@ -13,6 +13,7 @@ (cond (namespaces "invoice" ) [[:invoice e]] (namespaces "invoice-expense-account" ) [[:invoice (:db/id (:invoice/_expense-accounts entity))]] + (namespaces "transaction-account" ) [[:transaction (:db/id (:transaction/_accounts entity))]] (namespaces "transaction" ) [[:transaction e]] :else nil))) @@ -26,6 +27,7 @@ (cond (namespaces "invoice" ) :invoice (namespaces "invoice-expense-account" ) :invoice-expense-account + (namespaces "transaction-account" ) :transaction-account :else nil))) (defmulti entity-change->ledger (fn [_ [type]] @@ -57,7 +59,8 @@ (defmethod entity-change->ledger :transaction [db [type id]] - (let [entity (d/pull db ['* {:transaction/vendor '[*] :transaction/client '[*] :transaction/account '[*]}] id)] + (let [entity (d/pull db ['* {:transaction/vendor '[*] :transaction/client '[*] :transaction/accounts '[* {:transaction-account/account [*]}] }] id)] + (println "processing entity" entity) (when (:transaction/vendor entity) (remove-nils {:journal-entry/source "transaction" @@ -67,20 +70,23 @@ :journal-entry/vendor (:db/id (:transaction/vendor entity)) :journal-entry/amount (Math/abs (:transaction/amount entity)) - :journal-entry/line-items [(remove-nils{:journal-entry-line/account (:db/id (:transaction/account entity)) - :journal-entry-line/location (:transaction/location entity) - :journal-entry-line/debit (when (< (:transaction/amount entity) 0.0) - (Math/abs (:transaction/amount entity))) - :journal-entry-line/credit (when (>= (:transaction/amount entity) 0.0) - (Math/abs (:transaction/amount entity)))}) - - (remove-nils {:journal-entry-line/account (:db/id (:transaction/bank-account entity)) - :journal-entry-line/location "A" - :journal-entry-line/credit (when (< (:transaction/amount entity) 0.0) - (Math/abs (:transaction/amount entity))) - :journal-entry-line/debit (when (>= (:transaction/amount entity) 0.0) - (Math/abs (:transaction/amount entity)))}) - ] + :journal-entry/line-items (into [ + (remove-nils {:journal-entry-line/account (:db/id (:transaction/bank-account entity)) + :journal-entry-line/location "A" + :journal-entry-line/credit (when (< (:transaction/amount entity) 0.0) + (Math/abs (:transaction/amount entity))) + :journal-entry-line/debit (when (>= (:transaction/amount entity) 0.0) + (Math/abs (:transaction/amount entity)))}) + ] + (map + (fn [a] + (remove-nils{:journal-entry-line/account (:db/id (:transaction-account/account a)) + :journal-entry-line/location (:transaction-account/location a) + :journal-entry-line/debit (when (< (:transaction/amount entity) 0.0) + (Math/abs (:transaction-account/amount a))) + :journal-entry-line/credit (when (>= (:transaction/amount entity) 0.0) + (Math/abs (:transaction-account/amount a)))})) + (:transaction/accounts entity))) :journal-entry/cleared true})))) diff --git a/src/cljs/auto_ap/views/components/expense_accounts_field.cljs b/src/cljs/auto_ap/views/components/expense_accounts_field.cljs index faa8d58a..7850ac6d 100644 --- a/src/cljs/auto_ap/views/components/expense_accounts_field.cljs +++ b/src/cljs/auto_ap/views/components/expense_accounts_field.cljs @@ -29,10 +29,14 @@ (re-frame/reg-event-fx ::expense-account-changed (fn [_ [_ event expense-accounts field value]] - {:dispatch (into event [(assoc-in expense-accounts field value) - (if (= (list :account :id) (drop 1 field)) - (if-let [location (:location @(re-frame/subscribe [::subs/account value]))] - [[(first field) :location] location]))])})) + (let [updated-accounts (cond-> expense-accounts + true (assoc-in field value) + (= (list :account :id) (drop 1 field)) (assoc-in [(first field) :account] @(re-frame/subscribe [::subs/account value])) + ) + updated-accounts (if-let [location (get-in updated-accounts [(first field) :account :location])] + (assoc-in updated-accounts [(first field) :location] location) + updated-accounts)] + {:dispatch (into event [updated-accounts])}))) ;; VIEWS diff --git a/src/cljs/auto_ap/views/components/typeahead.cljs b/src/cljs/auto_ap/views/components/typeahead.cljs index cb181187..245ebc6c 100644 --- a/src/cljs/auto_ap/views/components/typeahead.cljs +++ b/src/cljs/auto_ap/views/components/typeahead.cljs @@ -17,17 +17,17 @@ (let [text (r/atom (or (second (first (filter #(= (first %) value) matches))) "")) highlighted (r/atom nil) selected (r/atom (first (first (filter #(= (first %) value) matches)))) - select (fn [[id text-description text-value]] - (reset! selected id) - (reset! text text-description) - (when on-change - (if (= :not-found id) - (on-change nil text-description text-value) - (on-change id text-description (or text-value text-description)))))] + ] (r/create-class {:reagent-render (fn [{:keys [matches on-change disabled field text-field value class not-found-description]}] - - (let [text @text + (let [ select (fn [[id text-description text-value]] + (reset! selected id) + (reset! text text-description) + (when on-change + (if (= :not-found id) + (on-change nil text-description text-value) + (on-change id text-description (or text-value text-description))))) + text @text valid-matches (get-valid-matches matches not-found-description not-found-value text)] [:div.typeahead (if disabled diff --git a/src/cljs/auto_ap/views/pages/transactions/common.cljs b/src/cljs/auto_ap/views/pages/transactions/common.cljs index 5cae7593..8cd30d67 100644 --- a/src/cljs/auto_ap/views/pages/transactions/common.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/common.cljs @@ -5,7 +5,7 @@ :amount :location [:vendor [:name :id]] - [:account [:id :name]] + [:accounts [:id :amount :location [:account [:name :id :location]]]] :date [:yodlee_merchant [:name :yodlee-id]] :post_date diff --git a/src/cljs/auto_ap/views/pages/transactions/form.cljs b/src/cljs/auto_ap/views/pages/transactions/form.cljs index 23d9ba3a..7221734c 100644 --- a/src/cljs/auto_ap/views/pages/transactions/form.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/form.cljs @@ -5,32 +5,24 @@ [auto-ap.views.components.expense-accounts-field :refer [expense-accounts-field]] [auto-ap.views.pages.transactions.common :refer [transaction-read]] [auto-ap.views.utils :refer [bind-field]] - [re-frame.core :as re-frame])) + [re-frame.core :as re-frame] + [clojure.string :as str])) -(re-frame/reg-event-db - ::editing - (fn [db [_ which]] - (-> db - (forms/start-form ::edit-transaction {:id (:id which) - :yodlee-merchant (:yodlee-merchant which) - :description-original (:description-original which) - :location (:location which) - :client-id (:id (:client which)) - :account-id (:id (:account which)) - :account-name (:name (:account which)) - :vendor-id (:id (:vendor which)) - :vendor-name (:name (:vendor which)) - :expense-accounts (or (:expense-accounts which) - [{:id (str "new-" (random-uuid)) - :amount (Math/abs (:amount which))}])})))) +;; SUBS (re-frame/reg-sub ::request :<- [::forms/form ::edit-transaction] - (fn [{{:keys [id vendor-id account-id location]} :data}] + (fn [{{:keys [id vendor-id accounts]} :data}] {:transaction {:id id - :location location :vendor-id vendor-id - :account-id account-id}})) + :accounts (map + (fn [{:keys [id account amount location]}] + {:id (when-not (str/starts-with? id "new-") + id) + :account-id (:id account) + :location location + :amount amount}) + accounts)}})) (re-frame/reg-sub ::can-submit @@ -39,12 +31,30 @@ (not= :loading status))) +;; EVENTS (re-frame/reg-event-fx ::edited (fn [{:keys [db]} [_ edit-completed {:keys [edit-transaction]}]] {:db (-> db (forms/stop-form ::edit-transaction)) + :dispatch (conj edit-completed edit-transaction)})) +(re-frame/reg-event-db + ::editing + (fn [db [_ which]] + + (-> db + (forms/start-form ::edit-transaction {:id (:id which) + :yodlee-merchant (:yodlee-merchant which) + :description-original (:description-original which) + :location (:location which) + :client-id (:id (:client which)) + :vendor-id (:id (:vendor which)) + :vendor-name (:name (:vendor which)) + :accounts (or (vec (:accounts which)) + [{:id (str "new-" (random-uuid)) + :amount (Math/abs (:amount which))}])})))) + (re-frame/reg-event-fx ::saving @@ -72,6 +82,8 @@ {:dispatch [::forms/change ::edit-transaction f a]}))) +;; VIEWS + (defn form [{:keys [edit-completed]}] [forms/side-bar-form {:form ::edit-transaction } (let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::edit-transaction]) @@ -121,15 +133,15 @@ :event change-event :subscription data}]]]] - [:div.field - [bind-field - [expense-accounts-field - {:type "expense-accounts" - :field [:expense-accounts] - :descriptor "credit account" - :locations locations - :event change-event - :subscription data}]]] + [:div.field] + [bind-field + [expense-accounts-field + {:type "expense-accounts" + :field [:accounts] + :descriptor "credit account" + :locations locations + :event change-event + :subscription data}]] (comment [:div.field diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 76ca020d..78fc98d5 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -150,6 +150,7 @@ (when (and spec (not (s/valid? spec (get-in subscription field)))) " is-danger"))) keys (dissoc keys :field :subscription :spec)] + (into [dom keys] (with-keys rest)))) (defmethod do-bind :default [dom {:keys [field event subscription class spec] :as keys} & rest]