From 9f46f85330b65cd127b9d2cee53a3d38bba294b9 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Tue, 1 Sep 2020 17:07:03 -0700 Subject: [PATCH] tons of small fixes. --- src/clj/auto_ap/datomic/migrate.clj | 4 +- src/clj/auto_ap/datomic/migrate/ledger.clj | 6 + src/clj/auto_ap/datomic/transactions.clj | 16 - src/clj/auto_ap/datomic/vendors.clj | 45 +- src/clj/auto_ap/graphql.clj | 11 +- src/clj/auto_ap/graphql/ledger.clj | 33 +- src/clj/auto_ap/graphql/vendors.clj | 8 +- src/clj/auto_ap/ledger.clj | 7 +- src/clj/auto_ap/parse/templates.clj | 2 +- src/clj/auto_ap/parse/util.clj | 7 + src/clj/auto_ap/yodlee/core.clj | 20 +- src/cljs/auto_ap/events.cljs | 27 +- src/cljs/auto_ap/subs.cljs | 24 + .../views/components/invoices/side_bar.cljs | 2 +- .../pages/admin/vendors/merge_dialog.cljs | 7 +- .../views/pages/admin/vendors/table.cljs | 6 +- src/cljs/auto_ap/views/pages/ledger.cljs | 1 + .../views/pages/ledger/profit_and_loss.cljs | 414 ++++++++++++------ .../auto_ap/views/pages/ledger/side_bar.cljs | 2 +- .../auto_ap/views/pages/ledger/table.cljs | 5 +- .../views/pages/payments/side_bar.cljs | 2 +- .../views/pages/transactions/side_bar.cljs | 2 +- test/clj/auto_ap/graphql.clj | 9 + test/clj/auto_ap/graphql/vendors.clj | 81 ++++ 24 files changed, 520 insertions(+), 221 deletions(-) create mode 100644 src/clj/auto_ap/datomic/migrate/ledger.clj create mode 100644 test/clj/auto_ap/graphql/vendors.clj diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index e46b78f0..9c538eef 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -5,6 +5,7 @@ [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]] [auto-ap.datomic.migrate.add-general-ledger :as add-general-ledger] + [auto-ap.datomic.migrate.ledger :as ledger] [auto-ap.datomic.migrate.sales :as sales] [auto-ap.datomic.migrate.clients :as clients] [auto-ap.datomic.migrate.audit :as audit] @@ -316,11 +317,12 @@ :auto-ap/fix-reset-rels {:txes-fn `reset-function}} sales/norms-map clients/norms-map + ledger/norms-map audit/norms-map) ] (println "Conforming database...") (c/ensure-conforms conn norms-map) - (when (not (seq args)) + #_(when (not (seq args)) (d/release conn)) (println "Done"))) #_(-main false) diff --git a/src/clj/auto_ap/datomic/migrate/ledger.clj b/src/clj/auto_ap/datomic/migrate/ledger.clj new file mode 100644 index 00000000..3cf5453e --- /dev/null +++ b/src/clj/auto_ap/datomic/migrate/ledger.clj @@ -0,0 +1,6 @@ +(ns auto-ap.datomic.migrate.ledger) + +(def norms-map {::add-alternat-description {:txes [[{:db/ident :journal-entry/alternate-description + :db/doc "The description if there is no vendor" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one}]]}}) diff --git a/src/clj/auto_ap/datomic/transactions.clj b/src/clj/auto_ap/datomic/transactions.clj index bc6848e1..460b868b 100644 --- a/src/clj/auto_ap/datomic/transactions.clj +++ b/src/clj/auto_ap/datomic/transactions.clj @@ -116,7 +116,6 @@ args) true (merge-query {:query {:find ['?e] :where ['[?e :transaction/id]]}}))] - (println query) (cond->> query true (d/query) true (apply-sort-3 args) @@ -179,18 +178,3 @@ (update :transaction/post-date c/from-date) (dissoc :transaction/id))) -(defn unapprove [ids] - (doseq [x (partition-all 100 ids)] - @(d/transact (d/connect uri) - (mapv (fn [i] - {:db/id i - :transaction/approval-status :transaction-approval-status/unapproved}) - x)))) - -(defn delete [ids] - (doseq [x (partition-all 100 ids)] - @(d/transact (d/connect uri) - (mapcat (fn [i] - [[:db/retractEntity i] - [:db/retractEntity [:journal-entry/original-entity i]]]) - x)))) diff --git a/src/clj/auto_ap/datomic/vendors.clj b/src/clj/auto_ap/datomic/vendors.clj index 1b0f8f78..9c21562c 100644 --- a/src/clj/auto_ap/datomic/vendors.clj +++ b/src/clj/auto_ap/datomic/vendors.clj @@ -1,7 +1,8 @@ (ns auto-ap.datomic.vendors (:require [datomic.api :as d] - [auto-ap.graphql.utils :refer [limited-clients]] - [auto-ap.datomic :refer [uri]])) + [auto-ap.graphql.utils :refer [limited-clients ]] + + [auto-ap.datomic :refer [uri conn merge-query]])) (defn cleanse [id vendor] (let [clients (if-let [clients (limited-clients id)] (set (map :db/id clients)) @@ -18,14 +19,42 @@ :vendor/terms-overrides [* {:vendor-terms-override/client [:client/name :client/code :db/id]}] :vendor/automatically-paid-when-due [:db/id :client/name]}]) -(defn get-graphql [args] - (->> (cond-> {:query {:find [(list 'pull '?e default-read)] +(defn get-usages [args] + (->> (cond-> {:query {:find ['?v '?c '(count ?e)] :in ['$] - :where ['[?e :vendor/name]]} - :args [(d/db (d/connect uri))]}) + :where ['[?v :vendor/name] + '(or-join [?v ?c ?e] + (and + [?e :invoice/vendor ?v] + [?e :invoice/client ?c]) + (and + [?e :transaction/vendor ?v] + [?e :transaction/client ?c]) + (and + [?e :journal-entry/vendor ?v] + [?e :journal-entry/client ?c]))]} + :args [(d/db conn)]} + + (limited-clients (:id args)) + (merge-query {:query {:in ['?xx] + :where [['(get ?xx ?c)]]} + :args [(set (map :db/id (limited-clients (:id args))))]})) (d/query) - (map first) - (map #(cleanse (:id args) %)))) + (reduce + (fn [usages [v c cnt]] + (update usages v (fnil conj []) {:client-id c :count cnt})) + {}))) + +(defn get-graphql [args] + (let [usages (time (get-usages args))] + (->> (cond-> {:query {:find [(list 'pull '?e default-read)] + :in ['$] + :where ['[?e :vendor/name]]} + :args [(d/db (d/connect uri))]}) + (d/query) + (map first) + (map #(cleanse (:id args) %)) + (map #(assoc % :usage (get usages (:db/id %))))))) (defn get-by-id [id] diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 76dd2b10..99e70098 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -148,6 +148,9 @@ :client {:type :client} :account {:type :account}}} + :usage {:fields {:client_id {:type :id} + :count {:type 'Int}}} + :vendor {:fields {:id {:type :id} :name {:type 'String} @@ -157,6 +160,7 @@ :automatically_paid_when_due {:type '(list :client)} :terms_overrides {:type '(list :terms_override)} :account_overrides {:type '(list :vendor_account_override)} + :usage {:type '(list :usage)} :print_as {:type 'String} :primary_contact {:type :contact} @@ -189,6 +193,7 @@ :cleared_against {:type 'String} :client {:type :client} :vendor {:type :vendor} + :alternate_description {:type 'String} :date {:type 'String} :line_items {:type '(list :journal_entry_line)}}} @@ -928,9 +933,7 @@ (let [users (d-users/get-graphql args)] (->graphql users))) -(defn get-vendor [context args value] - (->graphql - (d-vendors/get-graphql (assoc args :id (:id context))))) + (defn print-checks [context args value] @@ -1145,7 +1148,7 @@ :mutation/void-payment gq-checks/void-check :mutation/edit-expense-accounts gq-invoices/edit-expense-accounts :mutation/import-ledger gq-ledger/import-ledger - :get-vendor get-vendor}) + :get-vendor gq-vendors/get-graphql}) schema/compile)) diff --git a/src/clj/auto_ap/graphql/ledger.clj b/src/clj/auto_ap/graphql/ledger.clj index 91eef070..26b66791 100644 --- a/src/clj/auto_ap/graphql/ledger.clj +++ b/src/clj/auto_ap/graphql/ledger.clj @@ -76,6 +76,8 @@ (fn [acc [location account-id] {:keys [debit credit count]}] (let [account (lookup-account account-id) account-type (:account_type account)] + + (conj acc (merge {:id (str account-id "-" location) :location (or location "") :amount (if account-type (if (#{:account-type/asset @@ -129,17 +131,26 @@ (defn full-ledger-for-client [client-id] (->> (d/query - {:query {:find ['?d '?jel '?account '?location '?debit '?credit ] - :in ['$ '?client-id] - :where ['[?e :journal-entry/client ?client-id] - '[?e :journal-entry/date ?d] - '[?e :journal-entry/line-items ?jel] - '[(get-else $ ?jel :journal-entry-line/account :account/unknown) ?account] - '[(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit ] - '[(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit] - '[(get-else $ ?jel :journal-entry-line/location "") ?location]] - } - :args [(d/db (d/connect uri)) client-id]}) + {:query {:find ['?d '?jel '?account '?location '?debit '?credit] + :in ['$ '?client-id] + :where '[[?e :journal-entry/client ?client-id] + [?e :journal-entry/date ?d] + [?e :journal-entry/line-items ?jel] + (or-join [?e] + (and [?e :journal-entry/original-entity ?i] + (or-join [?e ?i] + (and + [?i :transaction/bank-account ?b] + (or [?b :bank-account/include-in-reports true] + (not [?b :bank-account/include-in-reports]))) + (not [?i :transaction/bank-account]))) + (not [?e :journal-entry/original-entity ])) + [(get-else $ ?jel :journal-entry-line/account :account/unknown) ?account] + [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit ] + [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit] + [(get-else $ ?jel :journal-entry-line/location "") ?location]] + } + :args [(d/db (d/connect uri)) client-id]}) (sort-by first))) (defn get-balance-sheet [context args value] diff --git a/src/clj/auto_ap/graphql/vendors.clj b/src/clj/auto_ap/graphql/vendors.clj index 668c9528..2cd2bb89 100644 --- a/src/clj/auto_ap/graphql/vendors.clj +++ b/src/clj/auto_ap/graphql/vendors.clj @@ -3,7 +3,7 @@ [auto-ap.datomic.vendors :as d-vendors] [auto-ap.time :refer [parse iso-date]] [datomic.api :as d] - [auto-ap.datomic :refer [uri remove-nils]] + [auto-ap.datomic :refer [uri remove-nils audit-transact]] [clj-time.coerce :as coerce] [clojure.set :as set])) @@ -79,7 +79,7 @@ {:db/id apwd}) (:automatically_paid_when_due in))])) - transaction-result @(d/transact (d/connect uri) transaction)] + transaction-result (audit-transact transaction (:id context))] (-> (d-vendors/get-by-id (or (-> transaction-result :tempids (get "vendor")) id)) @@ -100,3 +100,7 @@ transaction (conj transaction [:db/retractEntity from])] @(d/transact conn transaction) to)) + +(defn get-graphql [context args value] + (->graphql + (d-vendors/get-graphql (assoc args :id (:id context))))) diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index 174a719c..23ca419a 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -3,7 +3,7 @@ [yang.scheduler :as scheduler] [mount.core :as mount] [auto-ap.datomic.accounts :as a] - [auto-ap.datomic :refer [uri remove-nils]] + [auto-ap.datomic :refer [uri remove-nils conn]] [clojure.spec.alpha :as s] [clojure.tools.logging :as log] [auto-ap.logging :refer [info-event]] @@ -84,6 +84,7 @@ :journal-entry/client (:db/id (:transaction/client entity)) :journal-entry/date (:transaction/date entity) :journal-entry/original-entity (:db/id entity) + :journal-entry/alternate-description (:transaction/description-original entity) :journal-entry/vendor (:db/id (:transaction/vendor entity)) :journal-entry/amount (Math/abs (:transaction/amount entity)) :journal-entry/cleared-against (:transaction/cleared-against entity) @@ -122,9 +123,7 @@ (into [[:replace-general-ledger (:journal-entry/original-entity (first entries))]] entries)) -(mount/defstate conn - :start (d/connect uri) - :stop (d/release conn)) + (mount/defstate tx-report-queue :start (d/tx-report-queue conn) diff --git a/src/clj/auto_ap/parse/templates.clj b/src/clj/auto_ap/parse/templates.clj index d4430651..d68db2b2 100644 --- a/src/clj/auto_ap/parse/templates.clj +++ b/src/clj/auto_ap/parse/templates.clj @@ -512,7 +512,7 @@ :date [#"Date" 0 0 #"Date: (.*)"] :invoice-number [#"Invoice #" 0 0 #"Invoice #: (.*)"] :account-number [#"Customer #" 0 0 #"Customer #: (.*)"]} - :parser { :total [:trim-commas-and-remove-dollars nil] + :parser { :total [:trim-commas-and-remove-dollars-and-invert-parentheses nil] :date [:clj-time "MM/dd/yyyy"]}} {:vendor "Mama Lu's Foods" :keywords [#"Mama Lu's Foods"] diff --git a/src/clj/auto_ap/parse/util.clj b/src/clj/auto_ap/parse/util.clj index 617aff8a..9c5ad828 100644 --- a/src/clj/auto_ap/parse/util.clj +++ b/src/clj/auto_ap/parse/util.clj @@ -18,6 +18,13 @@ [_ _ value] (str/replace (str/replace value #"," "") #"\$" "")) +(defmethod parse-value :trim-commas-and-remove-dollars-and-invert-parentheses + [_ _ value] + (let [v (str/replace (str/replace value #"," "") #"\$" "")] + (if-let [[_ a ] (re-find #"\((.*)\)" v)] + (str "-" a) + v))) + (defmethod parse-value :trim-commas-and-negate [_ _ value] (let [[_ raw-value] (re-find #"([\d\.]+)" diff --git a/src/clj/auto_ap/yodlee/core.clj b/src/clj/auto_ap/yodlee/core.clj index 0fa63c0a..46477d9a 100644 --- a/src/clj/auto_ap/yodlee/core.clj +++ b/src/clj/auto_ap/yodlee/core.clj @@ -58,13 +58,18 @@ :account))) (defn get-accounts-for-provider-account [provider-account-id] - (let [cob-session (login-cobrand) - user-session (login-user cob-session)] - (-> (str (:yodlee-base-url env) "/accounts?providerAccountId=" provider-account-id) - (client/get {:headers (merge base-headers {"Authorization" (auth-header cob-session user-session)}) - :as :json}) - :body - :account))) + (try + (let [cob-session (login-cobrand) + user-session (login-user cob-session)] + (-> (str (:yodlee-base-url env) "/accounts?providerAccountId=" provider-account-id) + (client/get {:headers (merge base-headers {"Authorization" (auth-header cob-session user-session)}) + :as :json}) + :body + :account)) + (catch Exception e + (log/error (str "Couldn't get accounts for provider account '" provider-account-id "'") + e) + []))) (defn get-account [i] (let [cob-session (login-cobrand) @@ -234,7 +239,6 @@ output-chan (map (fn [provider-account] (lc/with-context {:provider-account-id (:id provider-account)} - (log/info "fetching details for provider" (:id provider-account)) (get-provider-account-detail (:id provider-account))))) (async/to-chan provider-accounts)) (async/data [token] (js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\." )))))) +(def vendor-query + [:id :name :hidden :terms [:default-account [:name :id :location]] + [:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]] + [:automatically-paid-when-due [:id :name]] + [:terms-overrides [[:client [:id :name]] :id :terms]] + [:usage [:client-id :count]] + [:primary-contact [:name :phone :email :id]] + [:secondary-contact [:id :name :phone :email]] + :print-as :invoice-reminder-schedule :code + [:address [:street1 :street2 :city :state :zip]]]) + (re-frame/reg-event-fx ::initialize-db (fn [{:keys [db]} [_ token]] @@ -48,22 +59,10 @@ [:address [:street1 :street2 :city :state :zip]] [:forecasted-transactions [:id :amount :identifier :day-of-month]]]] [:vendor - [:id :name :hidden [:default-account [:name :id :location]] [:primary-contact [:name :phone :email :id]] [:secondary-contact [:id :name :phone :email]] :print-as :invoice-reminder-schedule :code - [:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]] - - [:automatically-paid-when-due [:id :name]] - [:terms-overrides [[:client [:id :name]] :id :terms]]]] + vendor-query] [:accounts [:numeric-code :location :name :type :account_set :applicability :id [:client-overrides [:name [:client [:name :id]]]]]]]} :on-success [::received-initial]}})))) -(def vendor-query - [:id :name :hidden :terms [:default-account [:name :id :location]] - [:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]] - [:automatically-paid-when-due [:id :name]] - [:terms-overrides [[:client [:id :name]] :id :terms]] - [:primary-contact [:name :phone :email :id]] - [:secondary-contact [:id :name :phone :email]] - :print-as :invoice-reminder-schedule :code - [:address [:street1 :street2 :city :state :zip]]]) + (re-frame/reg-event-db ::toggle-menu diff --git a/src/cljs/auto_ap/subs.cljs b/src/cljs/auto_ap/subs.cljs index 8a2da605..10cb836b 100644 --- a/src/cljs/auto_ap/subs.cljs +++ b/src/cljs/auto_ap/subs.cljs @@ -177,6 +177,30 @@ (filter #(or (not (:hidden %)) is-admin) all-vendors))) +(re-frame/reg-sub + ::searchable-vendors + :<- [::is-admin?] + :<- [::client] + :<- [::all-vendors] + (fn [[is-admin client all-vendors]] + (cond client + (filter (fn [{:keys [hidden usage name] :as vendor}] + (or (not hidden) + (-> (first (filter #(= (:client-id %) + (:id client)) + usage)) + (:count 0) + (> 0)))) + all-vendors) + + is-admin + all-vendors + + :else + + + (filter #(not (:hidden %)) all-vendors)))) + (re-frame/reg-sub ::all-vendors (fn [db] 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 24463f12..f27dfc39 100644 --- a/src/cljs/auto_ap/views/components/invoices/side_bar.cljs +++ b/src/cljs/auto_ap/views/components/invoices/side_bar.cljs @@ -64,7 +64,7 @@ [:div [:p.menu-label "Vendor"] [:div - [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) + [typeahead-entity {:matches @(re-frame/subscribe [::subs/searchable-vendors]) :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %]) :match->text :name :type "typeahead-entity" diff --git a/src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs b/src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs index 8f384b69..763f4b4e 100644 --- a/src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs +++ b/src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs @@ -31,13 +31,16 @@ [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) :type "typeahead-entity" :auto-focus true - :match->text :name + :match->text (fn [x] + (str (:name x) " (" (reduce + 0 (map :count (:usage x))) " usages)") ) :field [:from]}]) + (field "To Vendor" [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) :type "typeahead-entity" - :match->text :name + :match->text (fn [x] + (str (:name x) " (" (reduce + 0 (map :count (:usage x))) " usages)") ) :field [:to]}])]))) (re-frame/reg-event-fx diff --git a/src/cljs/auto_ap/views/pages/admin/vendors/table.cljs b/src/cljs/auto_ap/views/pages/admin/vendors/table.cljs index 0fd09bf2..827e680b 100644 --- a/src/cljs/auto_ap/views/pages/admin/vendors/table.cljs +++ b/src/cljs/auto_ap/views/pages/admin/vendors/table.cljs @@ -40,7 +40,11 @@ (for [v (:data page)] ^{:key (str (:id v))} [grid/row {:class (:class v) :id (:id v)} - [grid/cell {} (:name v)] + [grid/cell {} (:name v) + (let [total-usage (reduce + 0 (map :count (:usage v)))] + (if (> total-usage 0 ) + [:div.mx-2.tag.is-info.is-light total-usage " usages, " (count (:usage v)) " clients" ] + [:div.mx-2.tag.is-warning.is-light "Unused"]))] [grid/cell {} (:email (:primary-contact v))] [grid/cell {} (-> v :default-account :id accounts :name)] [grid/cell {} diff --git a/src/cljs/auto_ap/views/pages/ledger.cljs b/src/cljs/auto_ap/views/pages/ledger.cljs index 7f8153e8..7e8757c6 100644 --- a/src/cljs/auto_ap/views/pages/ledger.cljs +++ b/src/cljs/auto_ap/views/pages/ledger.cljs @@ -48,6 +48,7 @@ :amount :note :cleared-against + :alternate-description [:vendor [:name :id]] [:client diff --git a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs index 4609ce17..42ee1acc 100644 --- a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs @@ -6,7 +6,7 @@ [goog.string :as gstring] [auto-ap.utils :refer [dollars-0? by ]] [auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]] - [auto-ap.views.utils :refer [date->str date-picker bind-field standard pretty dispatch-event local-today ->% ->$ str->date with-user]] + [auto-ap.views.utils :refer [date->str date-picker bind-field standard pretty dispatch-event local-today ->% ->$ str->date with-user dispatch-value-change query-params]] [cljs-time.core :as t] [re-frame.core :as re-frame] [auto-ap.status :as status] @@ -22,6 +22,10 @@ :fixed-overhead [80000 89999] :ownership-controllable [90000 99999]}) +(defn and-last-year [[from to]] + [[from to] + [(t/minus from (t/years 1)) (t/minus to (t/years 1))]]) + ;; SUBS (re-frame/reg-sub ::locations @@ -112,6 +116,11 @@ (fn [db] (-> db ::params))) +(re-frame/reg-sub + ::period-inputs + (fn [db] + (-> db ::period-inputs))) + (re-frame/reg-sub ::periods (fn [db] @@ -124,6 +133,24 @@ (fn [db] (assoc db ::ledger-list-active? false))) +(re-frame/reg-event-fx + ::mounted + (fn [{:keys [db]}] + (let [qp (query-params)] + (if (:periods qp) + (let [periods (mapv (fn [[start end title]] + [ + (str->date start standard) + (str->date end standard) + title]) + (:periods qp))] + {:dispatch [::range-selected periods (:include-deltas qp) nil]}) + {:dispatch [::range-selected (and-last-year [(t/minus (local-today) (t/period :years 1)) (local-today)]) true nil]})))) + +(re-frame/reg-event-db + ::period-inputs-change + (fn [db [_ field value]] + (assoc-in db (into [::period-inputs ] field) value))) (re-frame/reg-event-fx ::params-change @@ -131,7 +158,15 @@ (let [c @(re-frame/subscribe [::subs/client])] (cond-> {:db (-> (:db cofx) (assoc-in [::params] params) - (dissoc ::report))} + (dissoc ::report)) + :set-uri-params (update params + :periods + (fn [p] + (mapv (fn [[start end title]] + [(date->str start standard) + (date->str end standard) + title] + ) p)))} c (assoc :graphql (when @(re-frame/subscribe [::subs/client]) {:token (-> cofx :db :user) :owns-state {:single ::page} @@ -149,15 +184,17 @@ (re-frame/reg-event-fx ::date-picked (fn [cofx [_ [_ period which] date]] - (println date (str->date date standard)) - {:dispatch [::range-selected (assoc-in @(re-frame/subscribe [::periods]) [period which] (str->date date standard)) nil]})) + {:dispatch [::range-selected (assoc-in @(re-frame/subscribe [::periods]) [period which] (str->date date standard)) nil nil]})) (re-frame/reg-event-fx ::range-selected - (fn [{:keys [db]} [_ periods selected]] - {:dispatch [::params-change (assoc @(re-frame/subscribe [::params]) - :periods periods - :selected selected)] + (fn [{:keys [db]} [_ periods include-deltas selected]] + {:dispatch [::params-change (cond-> @(re-frame/subscribe [::params]) + true (assoc + :periods periods + :include-deltas include-deltas + :selected selected) + (not (nil? include-deltas)) (assoc :include-deltas include-deltas))] :db (assoc db ::periods periods)})) @@ -186,6 +223,7 @@ [[:journal-entries [:id :source :amount + :alternate-description [:vendor [:name :id]] [:client @@ -267,72 +305,77 @@ (set) (sort-by :numeric-code))) -(defn map-periods [for-every between periods] +(defn map-periods [for-every between periods include-deltas] (for [[_ i] (map vector periods (range))] ^{:key (str "period-" i)} [:<> (with-meta (for-every i) {:key i}) - (if (not= 0 i) + (if (and include-deltas (not= 0 i)) (with-meta (between i) {:key (str "between-" i)}))])) (defn grouping [{:keys [header type groupings location periods all-accounts]}] - [:<> - (doall - (for [[grouping-name from to] groupings - :let [account-codes (used-accounts all-accounts [from to] location)] - :when (seq account-codes)] - ^{:key grouping-name} - [:<> - [:tr [:td "---" grouping-name "---"] - (map-periods - (fn [i] - [:<> - [:td] - [:td]]) - (fn [i] - [:td]) - periods)] - [:<> - (for [{:keys [numeric-code name]} account-codes] - ^{:key numeric-code} - [:tr - [:td name] - (map-periods - (fn [i] - (let [amount (get-in all-accounts [i [numeric-code location] :amount] 0.0)] - [:<> - [:td.has-text-right [:a {:on-click (dispatch-event [::investigate-clicked location numeric-code numeric-code i :current])} - (->$ amount)]] - [:td.has-text-right (->% (percent-of-sales amount all-accounts i location))]])) - (fn [i] - [:td.has-text-right (->$ (- (get-in all-accounts [i [numeric-code location] :amount] 0.0) - (get-in all-accounts [(dec i) [numeric-code location] :amount] 0.0)))]) - periods)])] - - [:tr - [:th] - (map-periods - (fn [i] - (let [amount (aggregate-accounts (filter-accounts all-accounts i [from to] location))] + (let [params @(re-frame/subscribe [::params])] + [:<> + (doall + (for [[grouping-name from to] groupings + :let [account-codes (used-accounts all-accounts [from to] location)] + :when (seq account-codes)] + ^{:key grouping-name} + [:<> + [:tr [:td "---" grouping-name "---"] + (map-periods + (fn [i] [:<> - [:th.has-text-right.total [:a {:on-click (dispatch-event [::investigate-clicked location from to i])} - (->$ amount)]] - [:th.has-text-right.total (->% (percent-of-sales amount all-accounts i location))]])) - (fn [i] - [:th.has-text-right.total (->$ (- (aggregate-accounts (filter-accounts all-accounts i [from to] location)) - (aggregate-accounts (filter-accounts all-accounts (dec i) [from to] location))))]) - periods)]]))]) + [:td] + [:td]]) + (fn [i] + [:td]) + periods + (:include-deltas params))] + [:<> + (for [{:keys [numeric-code name]} account-codes] + ^{:key numeric-code} + [:tr + [:td name] + (map-periods + (fn [i] + (let [amount (get-in all-accounts [i [numeric-code location] :amount] 0.0)] + [:<> + [:td.has-text-right [:a {:on-click (dispatch-event [::investigate-clicked location numeric-code numeric-code i :current])} + (->$ amount)]] + [:td.has-text-right (->% (percent-of-sales amount all-accounts i location))]])) + (fn [i] + [:td.has-text-right (->$ (- (get-in all-accounts [i [numeric-code location] :amount] 0.0) + (get-in all-accounts [(dec i) [numeric-code location] :amount] 0.0)))]) + periods + (:include-deltas params))])] + + [:tr + [:th] + (map-periods + (fn [i] + (let [amount (aggregate-accounts (filter-accounts all-accounts i [from to] location))] + [:<> + [:th.has-text-right.total [:a {:on-click (dispatch-event [::investigate-clicked location from to i])} + (->$ amount)]] + [:th.has-text-right.total (->% (percent-of-sales amount all-accounts i location))]])) + (fn [i] + [:th.has-text-right.total (->$ (- (aggregate-accounts (filter-accounts all-accounts i [from to] location)) + (aggregate-accounts (filter-accounts all-accounts (dec i) [from to] location))))]) + periods + (:include-deltas params))]]))])) (defn overall-grouping [type title location] (let [all-accounts @(re-frame/subscribe [::all-accounts]) periods @(re-frame/subscribe [::periods]) - [min-numeric-code max-numeric-code] (ranges type)] + [min-numeric-code max-numeric-code] (ranges type) + params @(re-frame/subscribe [::params])] [:<> [:tr [:th.is-size-5 title]] @@ -355,11 +398,13 @@ (fn [i] [:th.has-text-right (->$ (- (aggregate-accounts (filter-accounts all-accounts i [min-numeric-code max-numeric-code] location)) (aggregate-accounts (filter-accounts all-accounts (dec i) [min-numeric-code max-numeric-code] location))))]) - periods)]])) + periods + (:include-deltas params))]])) (defn subtotal [types negs title location] (let [all-accounts @(re-frame/subscribe [::all-accounts]) - periods @(re-frame/subscribe [::periods])] + periods @(re-frame/subscribe [::periods]) + params @(re-frame/subscribe [::params])] [:tr [:th.is-size-5 title] (map-periods @@ -380,7 +425,8 @@ (cond->> (filter-accounts all-accounts (dec i) (ranges t) location) (negs t) (map #(update % :amount -)))) types))))]) - periods)])) + periods + (:include-deltas params))])) (defn location-rows [location] @@ -400,21 +446,24 @@ (defn location-summary [location params] (let [periods @(re-frame/subscribe [::periods])] [:div - [:h2.title.is-4 {:style {:margin-bottom "1rem"}} location " Summary"] - [:table.table.compact.balance-sheet {:style {:margin-bottom "2.5rem"}} + [:h2.title.is-4.mb-4 location " Summary"] + [:table.table.compact.balance-sheet.mb-6 [:tbody [:tr [:td.has-text-right "Period ending"] (map-periods (fn [i] [:<> - [:td.has-text-right (when-let [ date (get-in periods [i 1])] - (date->str date))] + [:td.has-text-right + (or (get-in periods [i 2]) + (when-let [ date (get-in periods [i 1])] + (date->str date)))] [:td] ]) (fn [i] [:td]) - periods)] + periods + (:include-deltas params))] [subtotal [:sales ] #{} "Sales" location] [subtotal [:cogs ] #{} "Cogs" location] [subtotal [:payroll ]#{} "Payroll" location] @@ -424,9 +473,7 @@ ])) -(defn and-last-year [[from to]] - [[from to] - [(t/minus from (t/years 1)) (t/minus to (t/years 1))]]) + (def profit-and-loss-content (with-meta @@ -435,6 +482,7 @@ user @(re-frame/subscribe [::subs/user]) status @(re-frame/subscribe [::status/single ::page]) params @(re-frame/subscribe [::params]) + period-inputs @(re-frame/subscribe [::period-inputs]) periods @(re-frame/subscribe [::periods])] (if-not current-client @@ -450,22 +498,82 @@ [:div [:div.field.is-grouped [:div.control - [:a.button - {:class (when (= (:selected params) "13 periods") "is-active") - :on-click (dispatch-event - [::range-selected - (let [this-month (t/local-date (t/year (local-today)) - (t/month (local-today)) - 1)] - (into - [[this-month (t/minus (t/plus this-month (t/months 1)) (t/days 1))]] - (for [i (range 12)] - [(t/minus this-month (t/months (- 12 i))) - (t/minus (t/minus this-month (t/months (- 11 i))) - (t/days 1))]))) - - "13 periods"])} - "13 periods"]] + [:div.field.has-addons + [:div.control + [bind-field + [date-picker {:class-name "input" + :class "input" + :format-week-number (fn [] "") + :previous-month-button-label "" + :placeholder "End date" + :next-month-button-label "" + :next-month-label "" + :type "date" + :field [:thirteen-periods-end] + :subscription period-inputs + :event [::period-inputs-change]}]]] + [:div.control + [:a.button + {:class (when (= (:selected params) "13 periods") "is-active") + :on-click (dispatch-event + [::range-selected + (let [today (or (some-> (:thirteen-periods-end period-inputs) (str->date standard)) + (local-today))] + (into + [[(t/plus (t/minus today (t/weeks (* 13 4))) + (t/days 1)) + today + "Total"]] + (for [i (range 13)] + [(t/plus (t/minus today (t/weeks (* (inc i) 4))) + (t/days 1)) + (t/minus today (t/weeks (* i 4)))]))) + + false + "13 periods"])} + "13 periods"]]]] + +[:div.control + [:div.field.has-addons + [:div.control + [bind-field + [date-picker {:class-name "input" + :class "input" + :format-week-number (fn [] "") + :previous-month-button-label "" + :placeholder "End date" + :next-month-button-label "" + :next-month-label "" + :type "date" + :field [:twelve-periods-end] + :subscription period-inputs + :event [::period-inputs-change]}]]] + [:div.control + [:a.button + {:class (when (= (:selected params) "13 periods") "is-active") + :on-click (dispatch-event + [::range-selected + (let [end-date (or (some-> (:twelve-periods-end period-inputs) (str->date standard)) + (local-today)) + this-month (t/local-date (t/year end-date) + (t/month end-date) + 1) + ] + (into + [[(t/minus this-month (t/months 11)) + (t/minus (t/plus this-month (t/months 1)) + (t/days 1)) + "Total"]] + (for [i (range 12)] + [(t/minus this-month (t/months (- 11 i))) + (t/minus (t/minus this-month (t/months (- 10 i))) + (t/days 1))]))) + + false + "12 months"] + )} + "12 months"]]]] + [:div.control [:a.button {:class (when (= (:selected params) "Last week") "is-active") @@ -476,6 +584,7 @@ (recur (t/minus current (t/period :days 1)))))] [::range-selected (and-last-year [(t/minus last-sunday (t/period :days 6)) last-sunday]) + true "Last week"]))} "Last week"]] [:div.control @@ -487,6 +596,7 @@ current (recur (t/minus current (t/period :days 1))))) (local-today)]) + true "Week to date"])} "Week to date"]] [:div.control @@ -501,6 +611,7 @@ (t/month (local-today)) 1) (t/period :days 1))]) + true "Last Month"])} "Last Month"]] [:div.control @@ -511,6 +622,7 @@ (t/month (local-today)) 1) (local-today)]) + true "Month to date"])} "Month to date"]] [:div.control @@ -519,6 +631,7 @@ :on-click (dispatch-event [::range-selected (and-last-year [(t/local-date (t/year (local-today)) 1 1) (local-today)]) + true "Year to date"])} "Year to date"]] [:div.control @@ -528,77 +641,92 @@ (and-last-year [(t/plus (t/minus (local-today) (t/period :years 1)) (t/period :days 1)) (local-today)]) + true "Full year"])} "Full year"]]]] - (for [[_ i] (map vector periods (range))] - ^{:key i} - [:div.field.is-grouped - [:div.control - [:p.help "From"] - [bind-field - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "mm/dd/yyyy" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:periods i 0] - :event [::date-picked] - :popper-props (clj->js {:placement "right"}) - :subscription params}]]] + [:div + [:div.field + [:label.checkbox + [bind-field + [:input {:type "checkbox" + :field [:show-advanced?] + :event [::period-inputs-change] + :subscription period-inputs}]] + " Show Advanced"]]] + (when (:show-advanced? period-inputs) + (for [[_ i] (map vector periods (range))] + ^{:key i} + [:div.field.is-grouped + [:div.control + [:p.help "From"] + [bind-field + [date-picker {:class-name "input" + :class "input" + :format-week-number (fn [] "") + :previous-month-button-label "" + :placeholder "mm/dd/yyyy" + :next-month-button-label "" + :next-month-label "" + :type "date" + :field [:periods i 0] + :event [::date-picked] + :popper-props (clj->js {:placement "right"}) + :subscription params}]]] - [:div.control - [:p.help "To"] - [bind-field - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "mm/dd/yyyy" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:periods i 1] - :event [::date-picked] - :popper-props (clj->js {:placement "right"}) - :subscription params}]]]])] + [:div.control + [:p.help "To"] + [bind-field + [date-picker {:class-name "input" + :class "input" + :format-week-number (fn [] "") + :previous-month-button-label "" + :placeholder "mm/dd/yyyy" + :next-month-button-label "" + :next-month-label "" + :type "date" + :field [:periods i 1] + :event [::date-picked] + :popper-props (clj->js {:placement "right"}) + :subscription params}]]]]))] [status/big-loader status] - [:div - [:<> - (for [location @(re-frame/subscribe [::locations])] - ^{:key (str location "-summary")} - [location-summary location params] - )] - [:h2.title.is-4 {:style {:margin-bottom "1rem"}} "Detail"] - [:table.table.compact.balance-sheet - [:tbody - [:tr - [:td.has-text-right "Period Ending"] - (map-periods - (fn [i] - [:<> - [:td.has-text-right (date->str (get-in periods [i 1]))] - [:td]]) - (fn [i] - [:td.has-text-right "𝝙"]) - periods)] + (when (not= :loading (:state status)) + [:div [:<> (for [location @(re-frame/subscribe [::locations])] - ^{:key location} - [location-rows location] - )]]]]]))) - {:component-will-mount #(do (re-frame/dispatch-sync [::range-selected (and-last-year [(t/minus (local-today) (t/period :years 1)) (local-today)]) nil])) })) + ^{:key (str location "-summary")} + [location-summary location params] + )] + [:h2.title.is-4 {:style {:margin-bottom "1rem"}} "Detail"] + [:table.table.compact.balance-sheet + [:tbody + [:tr + [:td.has-text-right "Period Ending"] + (map-periods + (fn [i] + [:<> + [:td.has-text-right + (or (get-in periods [i 2]) + (date->str (get-in periods [i 1])))] + [:td]]) + (fn [i] + [:td.has-text-right "𝝙"]) + periods + (:include-deltas params))] + [:<> + (for [location @(re-frame/subscribe [::locations])] + ^{:key location} + [location-rows location] + )]]]])]))) + {:component-will-mount #(re-frame/dispatch [::mounted]) })) (re-frame/reg-event-fx - ::unmounted + ::unmounted-pnl (fn [{:keys [db]} _] {:dispatch [::data-page/dispose ::ledger] ::track/dispose {:id ::ledger-params}})) (re-frame/reg-event-fx - ::mounted + ::mounted-pnl (fn [{:keys [db]} _] {::track/register {:id ::ledger-params :subscription [::data-page/params ::ledger] @@ -614,8 +742,8 @@ (defn profit-and-loss-page [] (reagent/create-class {:display-name "profit-and-loss-page" - :component-did-mount #(re-frame/dispatch [::mounted]) - :component-will-unmount #(re-frame/dispatch [::unmounted]) + :component-did-mount #(re-frame/dispatch [::mounted-pnl]) + :component-will-unmount #(re-frame/dispatch [::unmounted-pnl]) :reagent-render (fn [] (let [ledger-list-active? @(re-frame/subscribe [::ledger-list-active?]) diff --git a/src/cljs/auto_ap/views/pages/ledger/side_bar.cljs b/src/cljs/auto_ap/views/pages/ledger/side_bar.cljs index 6f27d953..1ca2bc4f 100644 --- a/src/cljs/auto_ap/views/pages/ledger/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/side_bar.cljs @@ -51,7 +51,7 @@ [:p.menu-label "Vendor"] [:div - [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) + [typeahead-entity {:matches @(re-frame/subscribe [::subs/searchable-vendors]) :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %]) :include-keys [:name :id] :match->text :name diff --git a/src/cljs/auto_ap/views/pages/ledger/table.cljs b/src/cljs/auto_ap/views/pages/ledger/table.cljs index bda3c845..9710543d 100644 --- a/src/cljs/auto_ap/views/pages/ledger/table.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/table.cljs @@ -9,7 +9,7 @@ [auto-ap.views.components.grid :as grid] [auto-ap.views.pages.data-page :as data-page])) -(defn ledger-row [{{:keys [client vendor status date amount id line-items] :as i} :row +(defn ledger-row [{{:keys [client vendor alternate-description status date amount id line-items] :as i} :row :keys [selected-client accounts-by-id bank-accounts-by-id]}] [:<> [grid/row {:class (:class i) :id id} @@ -17,7 +17,8 @@ [grid/cell {} (:name client)]) [grid/cell {} (if vendor (:name vendor) - [:i.has-text-grey (str "Unknown Merchant")])] + [:i.has-text-grey (or (when alternate-description (str "Bank Description: " alternate-description)) + "Unknown Merchant")])] [grid/cell {} (date->str date) ] [grid/cell {} ] [grid/cell {:class "has-text-right"} (nf amount )] diff --git a/src/cljs/auto_ap/views/pages/payments/side_bar.cljs b/src/cljs/auto_ap/views/pages/payments/side_bar.cljs index 3ba2a5da..14423b09 100644 --- a/src/cljs/auto_ap/views/pages/payments/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/payments/side_bar.cljs @@ -19,7 +19,7 @@ [:div [:p.menu-label "Vendor"] [:div - [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) + [typeahead-entity {:matches @(re-frame/subscribe [::subs/searchable-vendors]) :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %]) :include-keys [:name :id] :match->text :name 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 114ae320..37ee9815 100644 --- a/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs @@ -71,7 +71,7 @@ [:p.menu-label "Vendor"] [:div - [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) + [typeahead-entity {:matches @(re-frame/subscribe [::subs/searchable-vendors]) :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %]) :include-keys [:name :id] :match->text :name diff --git a/test/clj/auto_ap/graphql.clj b/test/clj/auto_ap/graphql.clj index 51bb7fa0..a05ef4f5 100644 --- a/test/clj/auto_ap/graphql.clj +++ b/test/clj/auto_ap/graphql.clj @@ -92,6 +92,15 @@ (is (int? (:start result))) (is (seqable? (:journal-entries result))))))) + +(deftest vendors + (testing "vendors" + (testing "it should find vendors" + (let [result (:ledger-page (:data (sut/query (admin-token) "{ ledger_page(client_id: null) { count, start, journal_entries { id } }}")))] + (is (int? (:count result))) + (is (int? (:start result))) + (is (seqable? (:journal-entries result))))))) + (deftest transaction-rule-page (testing "it should find rules" (let [result (-> (sut/query (admin-token) "{ transaction_rule_page(client_id: null) { count, start, transaction_rules { id } }}") diff --git a/test/clj/auto_ap/graphql/vendors.clj b/test/clj/auto_ap/graphql/vendors.clj new file mode 100644 index 00000000..40e75b25 --- /dev/null +++ b/test/clj/auto_ap/graphql/vendors.clj @@ -0,0 +1,81 @@ +(ns clj.auto-ap.graphql.vendors + (:require [auto-ap.graphql.vendors :as sut2] + [auto-ap.datomic :refer [uri conn]] + [auto-ap.datomic.migrate :as m] + + [clojure.test :as t :refer [deftest is testing use-fixtures]] + [datomic.api :as d] + [clj-time.core :as time])) + +(defn wrap-setup + [f] + (with-redefs [auto-ap.datomic/uri "datomic:mem://datomic-transactor:4334/invoice"] + (d/create-database uri) + (with-redefs [auto-ap.datomic/conn (d/connect uri)] + (m/-main false) + (f) + (d/release conn) + (d/delete-database uri)))) + +(defn admin-token [] + {:user "TEST ADMIN" + :exp (time/plus (time/now) (time/days 1)) + :user/role "admin" + :user/name "TEST ADMIN"}) + +(defn user-token [client-id] + {:user "TEST USER" + :exp (time/plus (time/now) (time/days 1)) + :user/role "user" + :user/name "TEST USER" + :user/clients [{:db/id client-id}]}) + + + + + +(defn new-invoice [args] + (merge {:invoice/total 100.0 + :invoice/invoice-number (.toString (java.util.UUID/randomUUID))} + args)) + +(use-fixtures :each wrap-setup) + + +(deftest vendors + (testing "vendors" + (let [{:strs [vendor client]} (:tempids @(d/transact (d/connect uri) [{:vendor/name "Test" :db/id "vendor"} + {:db/id "client" + :client/code "DEF"}]))] + (testing "it should find vendors" + (let [result (sut2/get-graphql {} {} {})] + (is (= 1 (count result))))) + + (testing "It should count invoice usages for each client" + @(d/transact (d/connect uri) + [{:invoice/client client + :invoice/invoice-number "123" + :invoice/vendor vendor}]) + + + (let [result (sut2/get-graphql {:id (admin-token)} {} {})] + (is (= [{:client_id client + :count 1}] (-> result first :usage))))) + + (testing "It should count transaction usages for each client" + @(d/transact (d/connect uri) + [{:transaction/client client + :transaction/vendor vendor}]) + (let [result (sut2/get-graphql {:id (admin-token)} {} {})] + (is (= [{:client_id client + :count 2}] (-> result first :usage))))) + + (testing "It should limit usages to visible clients" + (let [result (sut2/get-graphql {:id (user-token client)} {} {})] + (is (= [{:client_id client + :count 2}] (-> result first :usage)))) + + (let [result (sut2/get-graphql {:id (user-token 0)} {} {})] + (is (= nil (-> result first :usage)))))))) + +