From 9f30c74619f008c469335939884aafdba8a28bcb Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Tue, 23 Apr 2019 07:09:21 -0700 Subject: [PATCH] percentage-based splitting. --- src/clj/auto_ap/datomic/migrate.clj | 4 +- src/cljc/auto_ap/entities/shared.cljc | 5 +- .../components/expense_accounts_field.cljs | 79 ++++++++++++++----- .../auto_ap/views/pages/invoices/form.cljs | 54 +++++++++---- .../views/pages/transactions/form.cljs | 55 ++++++++----- src/cljs/auto_ap/views/utils.cljs | 17 ++++ 6 files changed, 158 insertions(+), 56 deletions(-) diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index 9c20b1ae..2be04772 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -128,8 +128,8 @@ :requires [:auto-ap/convert-invoices]} :auto-ap/add-yodlee-merchant2 {:txes add-general-ledger/add-yodlee-merchant :requires [:auto-ap/convert-transactions]} - :auto-ap/bulk-load-invoice-ledger3 {:txes-fn `add-general-ledger/bulk-load-invoice-ledger :requires [:auto-ap/convert-transactions]} - :auto-ap/bulk-load-transaction-ledger3 {:txes-fn `add-general-ledger/bulk-load-transaction-ledger :requires [:auto-ap/convert-transactions]} + #_#_:auto-ap/bulk-load-invoice-ledger3 {:txes-fn `add-general-ledger/bulk-load-invoice-ledger :requires [:auto-ap/convert-transactions]} + #_#_:auto-ap/bulk-load-transaction-ledger3 {:txes-fn `add-general-ledger/bulk-load-transaction-ledger :requires [:auto-ap/convert-transactions]} }] (println "Conforming database...") diff --git a/src/cljc/auto_ap/entities/shared.cljc b/src/cljc/auto_ap/entities/shared.cljc index 8f5ea165..f3377268 100644 --- a/src/cljc/auto_ap/entities/shared.cljc +++ b/src/cljc/auto_ap/entities/shared.cljc @@ -12,5 +12,6 @@ (s/def ::required-identifier (s/and string? #(not (str/blank? %)))) -(s/def ::money (s/and string? - #(re-matches money-regex %))) +(s/def ::money (s/or :string (s/and string? + #(re-matches money-regex %)) + :float float?)) 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 7850ac6d..a0ff8aea 100644 --- a/src/cljs/auto_ap/views/components/expense_accounts_field.cljs +++ b/src/cljs/auto_ap/views/components/expense_accounts_field.cljs @@ -2,7 +2,7 @@ (:require [auto-ap.forms :as forms] [auto-ap.subs :as subs] [auto-ap.views.components.typeahead :refer [typeahead]] - [auto-ap.views.utils :refer [bind-field dispatch-event]] + [auto-ap.views.utils :refer [bind-field dispatch-event ->$]] [goog.string :as gstring] [re-frame.core :as re-frame] [clojure.string :as str])) @@ -14,7 +14,9 @@ ::add-expense-account (fn [_ [_ event expense-accounts]] {:dispatch (conj event (conj expense-accounts - {:amount 0 :id (str "new-" (random-uuid))}))})) + {:amount 0 :id (str "new-" (random-uuid)) + :amount-mode "%" + :amount-percentage 0}))})) (re-frame/reg-event-fx ::remove-expense-account @@ -26,13 +28,30 @@ [] expense-accounts))})) +(defn recalculate-amounts [expense-accounts total] + (mapv + (fn [ea] + (assoc ea :amount + (js/parseFloat + (goog.string/format "%.2f" + (* (/ (js/parseFloat (:amount-percentage ea)) 100.0) (js/parseFloat total)))))) + expense-accounts)) + (re-frame/reg-event-fx ::expense-account-changed - (fn [_ [_ event expense-accounts field value]] + (fn [_ [_ event expense-accounts max-value field value]] (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])) - ) + (= (list :amount-percentage) (drop 1 field)) (assoc-in [(first field) :amount] + (js/parseFloat + (goog.string/format "%.2f" + (do + (println value max-value) + (* (/ (cond-> value + (not (float? value)) (js/parseFloat )) 100.0) + (cond-> max-value + (not (float? max-value)) (js/parseFloat)))))))) updated-accounts (if-let [location (get-in updated-accounts [(first field) :account :location])] (assoc-in updated-accounts [(first field) :location] location) updated-accounts)] @@ -43,16 +62,18 @@ (defn expense-accounts-field [{expense-accounts :value max-value :max locations :locations event :event descriptor :descriptor}] (let [chooseable-expense-accounts @(re-frame/subscribe [::subs/chooseable-expense-accounts]) accounts-by-id @(re-frame/subscribe [::subs/accounts-for-client-by-id])] + (println expense-accounts) [:div [:div.columns [:div.column - [:h1.subtitle.is-4.is-inline (str/capitalize descriptor) "s"]] + [:h1.subtitle.is-4.is-inline (str/capitalize descriptor) "s"] + [:p.help "Remaining " (->$ (- max-value (reduce + 0 (map (comp js/parseFloat :amount) expense-accounts))))]] [:div.column.is-narrow [:p.buttons [:a.button {:on-click (dispatch-event [::add-expense-account event expense-accounts])} "Add"]]]] - (for [[index {:keys [account id location amount] :as expense-account}] (map vector (range) expense-accounts) + (for [[index {:keys [account id location amount amount-mode] :as expense-account}] (map vector (range) expense-accounts) :let [account (accounts-by-id (:id account))]] ^{:key id} [:div.box @@ -74,7 +95,9 @@ [typeahead {:matches (map (fn [x] [(:id x) (str (:numeric-code x) " - " (:name x))]) chooseable-expense-accounts) :type "typeahead" :field [index :account :id] - :event [::expense-account-changed event expense-accounts] + #_#_:text-field [index :account :name] + + :event [::expense-account-changed event expense-accounts max-value] :subscription expense-accounts}]]]] [:div.column.is-narrow [:p.help "Location"] @@ -92,7 +115,7 @@ :field [index :location] :allow-nil? true :spec (set locations) - :event [::expense-account-changed event expense-accounts] + :event [::expense-account-changed event expense-accounts max-value] :subscription expense-accounts} (map (fn [l] ^{:key l} [:option {:value l} l]) locations)]]])]]]] @@ -101,14 +124,34 @@ [:p.help "Amount"] [:div.control [:div.field.has-addons.is-extended - [:p.control [:a.button.is-static "$"]] + [:p.control [:span.select + [bind-field + [:select {:type "select" + :field [index :amount-mode] + :allow-nil? false + :event [::expense-account-changed event expense-accounts max-value] + :subscription expense-accounts} + [:option "$"] + [:option "%"]]]]] [:p.control - [bind-field - [:input.input {:type "number" - :field [index :amount] - :style {:text-align "right" :width "7em"} - :event [::expense-account-changed event expense-accounts] - :subscription expense-accounts - :value (get-in expense-account [:amount]) - :max max-value - :step "0.01"}]]]]]]])])) + (if (= "$" amount-mode) + [bind-field + [:input.input {:type "number" + :field [index :amount] + :style {:text-align "right" :width "7em"} + :event [::expense-account-changed event expense-accounts max-value] + :subscription expense-accounts + :precision 2 + :value (get-in expense-account [:amount]) + :max max-value + :step "0.01"}]] + [bind-field + [:input.input {:type "number" + :field [index :amount-percentage] + :style {:text-align "right" :width "7em"} + :event [::expense-account-changed event expense-accounts max-value] + :precision 2 + :subscription expense-accounts + :value (get-in expense-account [:amount-percentage]) + :max "100" + :step "0.01"}]])]]]]])])) diff --git a/src/cljs/auto_ap/views/pages/invoices/form.cljs b/src/cljs/auto_ap/views/pages/invoices/form.cljs index 742cb0cd..67e6a22c 100644 --- a/src/cljs/auto_ap/views/pages/invoices/form.cljs +++ b/src/cljs/auto_ap/views/pages/invoices/form.cljs @@ -6,7 +6,7 @@ [auto-ap.subs :as subs] [auto-ap.views.components.dropdown :refer [drop-down]] [auto-ap.views.components.typeahead :refer [typeahead]] - [auto-ap.views.components.expense-accounts-field :refer [expense-accounts-field]] + [auto-ap.views.components.expense-accounts-field :refer [expense-accounts-field recalculate-amounts]] [auto-ap.views.pages.invoices.common :refer [invoice-read]] [auto-ap.views.utils :refer @@ -104,7 +104,10 @@ (re-frame/reg-event-db ::adding (fn [db [_ new]] - (-> db (forms/start-form ::form (assoc new :expense-accounts [{:amount 0 :id (str "new-" (random-uuid))}]))))) + (-> db (forms/start-form ::form (assoc new :expense-accounts [{:amount 0 + :id (str "new-" (random-uuid)) + :amount-percentage 100 + :amount-mode "%"}]))))) (re-frame/reg-event-db ::editing @@ -113,16 +116,28 @@ edit-invoice (assoc edit-invoice :original edit-invoice)] (-> db (forms/start-form ::form {:id (:id edit-invoice) - :status (:status edit-invoice) - :date (:date edit-invoice) - :invoice-number (:invoice-number edit-invoice) - :total (:total edit-invoice) - :original edit-invoice - :vendor-id (:id (:vendor edit-invoice)) - :vendor-name (:name (:vendor edit-invoice)) - :client-id (:id (:client edit-invoice)) - :expense-accounts (:expense-accounts edit-invoice) - :client-name (:name (:client edit-invoice))}))))) + :status (:status edit-invoice) + :date (:date edit-invoice) + :invoice-number (:invoice-number edit-invoice) + :total (:total edit-invoice) + :original edit-invoice + :vendor-id (:id (:vendor edit-invoice)) + :vendor-name (:name (:vendor edit-invoice)) + :client-id (:id (:client edit-invoice)) + :expense-accounts (if (seq (:expense-accounts which)) + (vec (map + (fn [a] + (-> a + (update :amount #(js/parseFloat %)) + (assoc :amount-percentage (* 100 (/ (js/parseFloat (:amount a)) + (Math/abs (js/parseFloat (:total which)))))) + (assoc :amount-mode "%"))) + (:expense-accounts edit-invoice))) + [{:id (str "new-" (random-uuid)) + :amount-mode "$" + :amount (Math/abs (:total edit-invoice)) + :amount-percentage 100}]) + :client-name (:name (:client edit-invoice))}))))) (re-frame/reg-event-fx @@ -135,6 +150,15 @@ [:client-id] value [:location] first-location]}))) +(re-frame/reg-event-fx + ::change-amount + [(forms/in-form ::form)] + (fn [{{:keys [data]} :db} [_ field value]] + (print field value (:expense-accounts data)) + {:dispatch [::forms/change ::form + field value + [:expense-accounts] (recalculate-amounts (:expense-accounts data) value)]})) + (re-frame/reg-event-fx ::change-vendor [(forms/in-form ::form)] @@ -148,6 +172,8 @@ field value [:expense-accounts] [{:id (str "new-" (random-uuid)) :amount (:total data) + :amount-percentage 100 + :amount-mode "%" :account @(re-frame/subscribe [::subs/vendor-default-account value])}]]} {:dispatch [::forms/change ::form field value]})))) @@ -284,7 +310,7 @@ [:input.input {:type "number" :field [:total] :disabled (if can-change-amount? "" "disabled") - :event change-event + :event [::change-amount] :min min-total :subscription data :spec ::invoice/total @@ -297,7 +323,7 @@ :descriptor "expense account" :event change-event :locations locations - :max-value (:total data) + :max (:total data) :field [:expense-accounts]}]]] diff --git a/src/cljs/auto_ap/views/pages/transactions/form.cljs b/src/cljs/auto_ap/views/pages/transactions/form.cljs index 7221734c..928e92d8 100644 --- a/src/cljs/auto_ap/views/pages/transactions/form.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/form.cljs @@ -39,6 +39,7 @@ (forms/stop-form ::edit-transaction)) :dispatch (conj edit-completed edit-transaction)})) + (re-frame/reg-event-db ::editing (fn [db [_ which]] @@ -46,14 +47,25 @@ (-> db (forms/start-form ::edit-transaction {:id (:id which) :yodlee-merchant (:yodlee-merchant which) + :amount (:amount 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))}])})))) + :accounts (if (seq (:accounts which)) + (vec (map + (fn [a] + (-> a + (update :amount js/parseFloat) + (assoc :amount-percentage (* 100 (/ (js/parseFloat (:amount a)) + (Math/abs (js/parseFloat (:amount which)))))) + (assoc :amount-mode "$"))) + (:accounts which))) + [{:id (str "new-" (random-uuid)) + :amount-mode "$" + :amount (Math/abs (:amount which)) + :amount-percentage 100}])})))) (re-frame/reg-event-fx @@ -111,6 +123,15 @@ :disabled "disabled" :subscription data}]]]] + [:div.field + [:p.help "Amount"] + [:div.control + [bind-field + [:input.input {:type "text" + :field [:amount] + :disabled "disabled" + :subscription data}]]]] + [:div.field [:p.help "Description"] [:div.control @@ -133,15 +154,16 @@ :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}]] + [:div.field + [bind-field + [expense-accounts-field + {:type "expense-accounts" + :field [:accounts] + :max (Math/abs (js/parseFloat (:amount data))) + :descriptor "credit account" + :locations locations + :event change-event + :subscription data}]]] (comment [:div.field @@ -151,18 +173,11 @@ :field [:always-map] :subscription data}] " Always match Merchant '" (:merchant-name data) "' to '" (:vendor-name data) "'" ]]]) - - - - - (when error ^{:key error} [:div.notification.is-warning.animated.fadeInUp error]) - [:button.button.is-medium.is-primary.is-fullwidth {:disabled (if @(re-frame/subscribe [::can-submit]) "" "disabled") :class (str @(re-frame/subscribe [::forms/loading-class ::edit-transaction]) - (when error " animated shake"))} "Save"] - ])]) + (when error " animated shake"))} "Save"]])]) diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 96eb37f1..4cb0ce6b 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -156,6 +156,23 @@ (into [dom keys] (with-keys rest)))) +(defmethod do-bind "number" [dom {:keys [field precision event subscription class spec] :as keys :or {precision 2}} & rest] + (let [field (if (keyword? field) [field] field) + event (if (keyword? event) [event] event) + keys (assoc keys + :on-change (fn [e] + (.preventDefault e) + (re-frame/dispatch (-> event + (conj field) + (conj (js/parseFloat (.. e -target -value)))))) + :value (get-in subscription field) + + :class (str class + (when (and spec (not (s/valid? spec (get-in subscription field)))) + " is-danger"))) + keys (dissoc keys :field :subscription :event :spec)] + (into [dom keys] (with-keys rest)))) + (defmethod do-bind :default [dom {:keys [field event subscription class spec] :as keys} & rest] (let [field (if (keyword? field) [field] field) event (if (keyword? event) [event] event)