supports credits
This commit is contained in:
@@ -17,36 +17,42 @@
|
||||
[db [type id]]
|
||||
(let [{:expected-deposit/keys [total client date]} (d/pull db '[:expected-deposit/total :expected-deposit/client :expected-deposit/date] id)]
|
||||
#:journal-entry
|
||||
{:source "expected-deposit"
|
||||
:original-entity id
|
||||
{:source "expected-deposit"
|
||||
:original-entity id
|
||||
|
||||
:client client
|
||||
:date date
|
||||
:amount total
|
||||
:vendor :vendor/ccp-square
|
||||
:line-items [#:journal-entry-line
|
||||
{:credit total
|
||||
:location "A"
|
||||
:account :account/receipts-split}
|
||||
#:journal-entry-line
|
||||
{:debit total
|
||||
:location "A"
|
||||
:account :account/ccp}]}))
|
||||
:client client
|
||||
:date date
|
||||
:amount total
|
||||
:vendor :vendor/ccp-square
|
||||
:line-items [#:journal-entry-line
|
||||
{:credit total
|
||||
:location "A"
|
||||
:account :account/receipts-split}
|
||||
#:journal-entry-line
|
||||
{:debit total
|
||||
:location "A"
|
||||
:account :account/ccp}]}))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn regenerate-literals []
|
||||
(require 'com.github.ivarref.gen-fn)
|
||||
(spit
|
||||
"resources/functions.edn"
|
||||
(let [datomic-fn @(resolve 'com.github.ivarref.gen-fn/datomic-fn)]
|
||||
[(datomic-fn :pay #'iol-ion.tx.pay/pay)
|
||||
(datomic-fn :plus #'iol-ion.tx.plus/plus)
|
||||
(datomic-fn :propose-invoice #'iol-ion.tx.propose-invoice/propose-invoice)
|
||||
(datomic-fn :reset-rels #'iol-ion.tx.reset-rels/reset-rels)
|
||||
(datomic-fn :reset-scalars #'iol-ion.tx.reset-scalars/reset-scalars)
|
||||
(datomic-fn :upsert-entity #'iol-ion.tx.upsert-entity/upsert-entity)
|
||||
(datomic-fn :upsert-invoice #'iol-ion.tx.upsert-invoice/upsert-invoice)
|
||||
(datomic-fn :upsert-ledger #'iol-ion.tx.upsert-ledger/upsert-ledger)
|
||||
(datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction)])))
|
||||
(spit
|
||||
"resources/functions.edn"
|
||||
(let [datomic-fn @(resolve 'com.github.ivarref.gen-fn/datomic-fn)]
|
||||
[(datomic-fn :pay #'iol-ion.tx.pay/pay)
|
||||
(datomic-fn :plus #'iol-ion.tx.plus/plus)
|
||||
(datomic-fn :propose-invoice #'iol-ion.tx.propose-invoice/propose-invoice)
|
||||
(datomic-fn :reset-rels #'iol-ion.tx.reset-rels/reset-rels)
|
||||
(datomic-fn :reset-scalars #'iol-ion.tx.reset-scalars/reset-scalars)
|
||||
(datomic-fn :upsert-entity #'iol-ion.tx.upsert-entity/upsert-entity)
|
||||
(datomic-fn :upsert-invoice #'iol-ion.tx.upsert-invoice/upsert-invoice)
|
||||
(datomic-fn :upsert-ledger #'iol-ion.tx.upsert-ledger/upsert-ledger)
|
||||
(datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction)])))
|
||||
|
||||
(comment
|
||||
(regenerate-literals)
|
||||
|
||||
(auto-ap.datomic/install-functions)
|
||||
)
|
||||
@@ -6,6 +6,6 @@
|
||||
new-outstanding-balance (- current-outstanding-balance amount)]
|
||||
[[:upsert-invoice {:db/id e
|
||||
:invoice/outstanding-balance new-outstanding-balance
|
||||
:invoice/status (if (> new-outstanding-balance 0)
|
||||
:invoice-status/unpaid
|
||||
:invoice-status/paid)}]]))
|
||||
:invoice/status (if (< -0.0001 new-outstanding-balance 0.0001)
|
||||
:invoice-status/paid
|
||||
:invoice-status/unpaid)}]]))
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -67,9 +67,9 @@
|
||||
(for [{:keys [name sort-icon sort-key]} sort]
|
||||
[:div.py-1.px-3.text-sm.rounded.bg-gray-100.dark:bg-gray-600.flex.items-center.gap-2.relative name [:div.h-4.w-4.mr-3 sort-icon]
|
||||
[:div {:class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white hover:scale-110 transition-all duration-300 bg-gray-400 border-2 border-white rounded-full -top-2 -right-2 dark:border-gray-900"}
|
||||
[:a {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
(:route grid-spec)) "?remove-sort=" sort-key)
|
||||
:hx-boost "true"
|
||||
[:a {:hx-get (str (bidi/path-for ssr-routes/only-routes
|
||||
(:route grid-spec)) "?remove-sort=" sort-key)
|
||||
:href "#"
|
||||
:hx-target (str "#" (:id grid-spec))}
|
||||
[:div.h-4.w-4 svg/x]]]]))
|
||||
"default sort"))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query observable-query
|
||||
pull-many]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.datomic.bank-accounts :as d-bank-accounts]
|
||||
[auto-ap.datomic.invoices :as d-invoices]
|
||||
[auto-ap.graphql.checks :as gq-checks :refer [base-payment
|
||||
@@ -117,7 +118,6 @@
|
||||
(exact-match-id* request)]])
|
||||
|
||||
|
||||
;; TODO clientize
|
||||
(def default-read '[:db/id
|
||||
:invoice/invoice-number
|
||||
:invoice/total
|
||||
@@ -324,7 +324,22 @@
|
||||
(dc/db conn)
|
||||
ids))
|
||||
|
||||
0)]
|
||||
0)
|
||||
vendor-totals (if (seq ids)
|
||||
(->>
|
||||
(dc/q '[:find ?i ?v ?ob
|
||||
:in $ [?i ...]
|
||||
:where [?i :invoice/vendor ?v]
|
||||
[?i :invoice/outstanding-balance ?ob]]
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(reduce (fn [acc [_ v ob]]
|
||||
(update acc v (fnil + 0) ob))
|
||||
{})
|
||||
(vals)))
|
||||
all-credits-or-debits (or (every? #(<= % 0.0) vendor-totals)
|
||||
(every? #(>= % 0.0) vendor-totals))]
|
||||
|
||||
|
||||
[:div {:hx-target "this"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
@@ -336,7 +351,8 @@
|
||||
(com/button {:color :primary
|
||||
:id "pay-button"
|
||||
:disabled (or (= (count (:ids params)) 0)
|
||||
(not= 1 selected-client-count))
|
||||
(not= 1 selected-client-count)
|
||||
(not all-credits-or-debits))
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#invoice-filters"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/pay-button)
|
||||
@@ -359,6 +375,8 @@
|
||||
:x-show "hovering"
|
||||
:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"})
|
||||
(cond
|
||||
(not all-credits-or-debits)
|
||||
[:div "All vendor totals must be either positive or negative."]
|
||||
(= 0 (count ids))
|
||||
[:div "Please select some invoices to pay"]
|
||||
(> selected-client-count 1)
|
||||
@@ -446,12 +464,15 @@
|
||||
{:key "accounts"
|
||||
:name "Account"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:invoice/keys [expense-accounts]}]
|
||||
:render (fn [{:invoice/keys [expense-accounts client]}]
|
||||
[:div.flex.flex-col.gap-y-2
|
||||
(when (first expense-accounts)
|
||||
[:div.flex-initial
|
||||
(com/pill {:color :primary}
|
||||
(:account/name (:invoice-expense-account/account (first expense-accounts))))])
|
||||
(:account/name
|
||||
(d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id (:invoice-expense-account/account (first expense-accounts))))
|
||||
|
||||
(:db/id client))))])
|
||||
(when (> (count expense-accounts) 1)
|
||||
[:div.flex-initial
|
||||
(com/pill {:color :secondary}
|
||||
@@ -495,8 +516,6 @@
|
||||
(def row* (partial helper/row* grid-page))
|
||||
|
||||
|
||||
;; TODO clientize accounts
|
||||
|
||||
(defn delete [{invoice :entity :as request identity :identity}]
|
||||
(exception->notification
|
||||
#(when-not (= :invoice-status/unpaid (:invoice/status invoice))
|
||||
@@ -618,11 +637,7 @@
|
||||
(count ids))})})))
|
||||
|
||||
;; TODO
|
||||
;; voiding invoice - should you allow if there are ANY payments? i think no
|
||||
;; Allow for paying balances from set of invoices for one vendor
|
||||
;; Allow for credits, just highlight it
|
||||
;; Entering 0.00 payment should just remove it from the set
|
||||
;; inline all the check printing stuff
|
||||
|
||||
(defn does-amount-exceed-outstanding? [amount outstanding-balance]
|
||||
(let [outstanding-balance (round-money outstanding-balance)
|
||||
@@ -662,9 +677,9 @@
|
||||
[:check-number {:optional true} :int]
|
||||
[:handwritten-date {:optional true} [:maybe clj-date-schema]]
|
||||
[:mode [:enum :simple :advanced]]
|
||||
[:method [:enum :debit :print-check :cash :handwrite-check]]]))
|
||||
[:method [:enum :debit :print-check :cash :handwrite-check :credit]]]))
|
||||
|
||||
(defn bank-account-card-base [{:keys [bg-color text-color icon bank-account can-handwrite?]}]
|
||||
(defn bank-account-card-base [{:keys [bg-color text-color icon bank-account can-handwrite? credit-only?]}]
|
||||
[:div {:class "w-[30em]"}
|
||||
(com/card {:class "w-full"}
|
||||
[:div.flex.items-stretch {:x-data (hx/json {:chosen false
|
||||
@@ -683,9 +698,18 @@
|
||||
[:div.font-medium.text-gray-700 (:bank-account/name bank-account)]
|
||||
[:div.font-light.text-gray-600 (:bank-account/bank-name bank-account)]]
|
||||
[:div.grow-0.m-2.self-center
|
||||
(com/button {:x-ref "button"
|
||||
"@click.prevent.capture" "chosen=true; $nextTick(() => popper.update())"}
|
||||
"Pay")
|
||||
(if credit-only?
|
||||
(com/button {:color :primary
|
||||
:minimal-loading? true
|
||||
:hx-vals (hx/json {"step-params[bank-account]" (:db/id bank-account)
|
||||
"step-params[method]" "credit"})
|
||||
:hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/pay-wizard-navigate)
|
||||
{:from (mm/encode-step-key :choose-method)
|
||||
:to (mm/encode-step-key :payment-details)})}
|
||||
"Credit")
|
||||
(com/button {:x-ref "button"
|
||||
"@click.prevent.capture" "chosen=true; $nextTick(() => popper.update())"}
|
||||
"Pay"))
|
||||
[:div.flex.flex-col.gap-2 (hx/alpine-appear {:x-show "chosen" :x-ref "tooltip"
|
||||
:data-key "vis"
|
||||
:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"
|
||||
@@ -731,31 +755,34 @@
|
||||
:to (mm/encode-step-key :payment-details)})}
|
||||
"Handwrite check"))]]])])
|
||||
|
||||
(defmulti bank-account-card (fn [ba _]
|
||||
(defmulti bank-account-card (fn [ba _ _]
|
||||
(:bank-account/type ba)))
|
||||
(defmethod bank-account-card :bank-account-type/cash [bank-account can-handwrite?]
|
||||
(defmethod bank-account-card :bank-account-type/cash [bank-account can-handwrite? credit-only?]
|
||||
(bank-account-card-base {:bg-color "bg-green-50"
|
||||
:text-color "text-green-600"
|
||||
:icon svg/dollar
|
||||
:bank-account bank-account
|
||||
:can-handwrite? can-handwrite?}))
|
||||
:can-handwrite? can-handwrite?
|
||||
:credit-only? credit-only?}))
|
||||
|
||||
(defmethod bank-account-card
|
||||
:bank-account-type/credit
|
||||
[bank-account can-handwrite?]
|
||||
[bank-account can-handwrite? credit-only?]
|
||||
(bank-account-card-base {:bg-color "bg-purple-50"
|
||||
:text-color "text-purple-600"
|
||||
:icon svg/credit-card
|
||||
:bank-account bank-account
|
||||
:can-handwrite? can-handwrite?}))
|
||||
:can-handwrite? can-handwrite?
|
||||
:credit-only? credit-only?}))
|
||||
|
||||
(defmethod bank-account-card
|
||||
:bank-account-type/check [bank-account can-handwrite?]
|
||||
:bank-account-type/check [bank-account can-handwrite? credit-only?]
|
||||
(bank-account-card-base {:bg-color "bg-blue-50"
|
||||
:text-color "text-blue-600"
|
||||
:icon svg/check
|
||||
:bank-account bank-account
|
||||
:can-handwrite? can-handwrite?}))
|
||||
:can-handwrite? can-handwrite?
|
||||
:credit-only? credit-only?}))
|
||||
|
||||
|
||||
(defn can-handwrite? [invoices]
|
||||
@@ -764,6 +791,15 @@
|
||||
(= 1 (count selected-vendors))
|
||||
(> (reduce + 0 (map :invoice/outstanding-balance invoices)) 0.0))))
|
||||
|
||||
(defn credit-only? [invoices]
|
||||
(->> invoices
|
||||
(group-by :invoice/vendor)
|
||||
vals
|
||||
(map (fn [is]
|
||||
(alog/peek ::invoices is)
|
||||
(reduce + 0.0 (map :invoice/outstanding-balance is))))
|
||||
(every? #(< % 0.0))))
|
||||
|
||||
|
||||
(defrecord ChoosePaymentMethodModal [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
@@ -781,7 +817,8 @@
|
||||
(render-step
|
||||
[this request]
|
||||
(let [invoices (:invoices (:snapshot (:multi-form-state request)))
|
||||
can-handwrite? (can-handwrite? (map (comp (:invoice-by-id linear-wizard) :invoice-id) invoices))]
|
||||
can-handwrite? (can-handwrite? (map (comp (:invoice-by-id linear-wizard) :invoice-id) invoices))
|
||||
credit-only? (credit-only? (map (comp (:invoice-by-id linear-wizard) :invoice-id) invoices))]
|
||||
(mm/default-render-step
|
||||
linear-wizard this
|
||||
:head [:div.p-2.inline-flex.gap-2.items-center "Pay " (count invoices) " invoices"
|
||||
@@ -792,7 +829,7 @@
|
||||
{}
|
||||
[:div.flex.flex-col.space-y-2
|
||||
(for [ba (:bank-accounts linear-wizard)]
|
||||
(bank-account-card ba can-handwrite?))])
|
||||
(bank-account-card ba can-handwrite? credit-only?))])
|
||||
:footer
|
||||
nil
|
||||
:validation-route ::route/pay-wizard-navigate))))
|
||||
@@ -839,9 +876,12 @@
|
||||
(com/radio-list {:x-model "mode"
|
||||
:name "step-params[mode]"
|
||||
:options [{:value "simple"
|
||||
:content (format "Pay in full ($%,.2f)" (reduce + 0.0
|
||||
(map (comp :invoice/outstanding-balance (:invoice-by-id linear-wizard) :invoice-id)
|
||||
(:invoices (:snapshot (:multi-form-state request))))))}
|
||||
:content (let [total (reduce + 0.0
|
||||
(map (comp :invoice/outstanding-balance (:invoice-by-id linear-wizard) :invoice-id)
|
||||
(:invoices (:snapshot (:multi-form-state request)))))]
|
||||
(if (< total 0)
|
||||
(format "Credit in full ($%,.2f)" total)
|
||||
(format "Pay in full ($%,.2f)" total)))}
|
||||
{:value "advanced"
|
||||
:content "Customize payments"}]})
|
||||
[:div.space-y-4 (hx/alpine-appear {:x-show "mode==\"advanced\""})
|
||||
@@ -870,9 +910,6 @@
|
||||
(com/data-grid-cell
|
||||
{:class "text-right"}
|
||||
[:span.inline-flex.gap-2
|
||||
(when (< (-> (fc/field-value) :invoice :invoice/outstanding-balance) 0.0)
|
||||
(com/pill {:color :yellow}
|
||||
"credit")) ;; TODO this credit should be based on the total for the vendor
|
||||
(format "$%,.2f" (-> (fc/field-value) :invoice :invoice/outstanding-balance))])
|
||||
(com/data-grid-cell
|
||||
{:class "w-20"}
|
||||
@@ -885,7 +922,6 @@
|
||||
(mm/default-step-footer linear-wizard this :validation-route ::route/pay-wizard-navigate)
|
||||
:validation-route ::route/pay-wizard-navigate)))
|
||||
|
||||
|
||||
(defn add-handwritten-check [request wizard snapshot]
|
||||
(let [invoices (d-invoices/get-multi (map :invoice-id (:invoices snapshot))) ;; TODO shouldn't need datomic
|
||||
bank-account-id (:bank-account snapshot)
|
||||
@@ -916,6 +952,16 @@
|
||||
(doseq [[_ i] (:tempids result)]
|
||||
(solr/touch-with-ledger i)))))
|
||||
|
||||
;; TODO Payment validations
|
||||
;; 1. ensure that filtering for selected ids happens again
|
||||
;; at the end of the modal to prevent race conditions
|
||||
;; add validation to prevent overpaying
|
||||
;; Paying a completed invoice is allowed presently
|
||||
;; Thought is to put it into the pay function itself
|
||||
;; balance should only go to negative if total was negative
|
||||
;; balance should stay positive if total was positive
|
||||
|
||||
;; TODO support crediting from balance
|
||||
(defrecord PayWizard [form-params current-step invoice-by-id]
|
||||
mm/LinearModalWizard
|
||||
(hydrate-from-request
|
||||
@@ -1009,6 +1055,8 @@
|
||||
:payment-type/debit
|
||||
(= :cash (:method snapshot))
|
||||
:payment-type/cash
|
||||
(= :credit (:method snapshot))
|
||||
:payment-type/credit
|
||||
:else :payment-type/debit) ;; TODO might not be right
|
||||
identity)))]
|
||||
(modal-response
|
||||
@@ -1036,10 +1084,10 @@
|
||||
|
||||
(defn wrap-status-from-source [handler]
|
||||
(fn [{:keys [matched-current-page-route] :as request}]
|
||||
(let [ request (cond-> request
|
||||
(= ::route/paid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/paid)
|
||||
(= ::route/unpaid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/unpaid)
|
||||
(= ::route/voided-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/voided)
|
||||
(let [request (cond-> request
|
||||
(= ::route/paid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/paid)
|
||||
(= ::route/unpaid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/unpaid)
|
||||
(= ::route/voided-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/voided)
|
||||
(= ::route/all-page matched-current-page-route) (assoc-in [:route-params :status] nil))]
|
||||
(handler request))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user