Makes editing work correctly for non-admins

This commit is contained in:
2024-04-03 20:08:46 -07:00
parent d2ed08f6f9
commit dfccbf35cd
7 changed files with 341 additions and 139 deletions

View File

@@ -1,13 +1,13 @@
(ns auto-ap.ssr.components.aside
(:require [auto-ap.client-routes :as client-routes]
[auto-ap.logging :as alog]
[auto-ap.permissions :refer [can?]]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.excel-invoices :as ei-routes]
[auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.outgoing-invoice :as oi-routes]
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.routes.invoice :as invoice-route]
[auto-ap.routes.outgoing-invoice :as oi-routes]
[auto-ap.routes.payments :as payment-routes]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.hiccup-helper :as hh]
@@ -131,53 +131,56 @@
:active? (= ::invoice-route/voided-page (:matched-route request))
:hx-boost "true"}
"Voided")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::oi-routes/new)
:active? (= ::oi-routes/new (:matched-route request))
:hx-boost "true"}
"Create outgoing"))
(menu-button- {:href (bidi/path-for client-routes/routes
:import-invoices)} "Import")
#_(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::oi-routes/new)
:active? (= ::oi-routes/new (:matched-route request))
:hx-boost "true"}
"Create outgoing"))
(menu-button- {:icon svg/receipt-register-1
(when
(can? (:identity request) {:subject :sales :activity :read})
(list
(menu-button- {:icon svg/receipt-register-1
"@click.prevent" "if (selected == 'sales') {selected = null } else { selected = 'sales'} "}
"Sales")
(sub-menu- {:selector "sales"
:active? (= "sales" selected)}
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-sales)
"?date-range=week")
:active? (= :pos-sales (:matched-route request))
:hx-boost "true"}
"Sales")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-expected-deposits)
"?date-range=week")
:active? (= :pos-expected-deposits (:matched-route request))
:hx-boost "true"}
"Expected Deposits")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-tenders)
"?date-range=week")
:active? (= :pos-tenders (:matched-route request))
:hx-boost "true"}
"Tenders")
"@click.prevent" "if (selected == 'sales') {selected = null } else { selected = 'sales'} "}
"Sales")
(sub-menu- {:selector "sales"
:active? (= "sales" selected)}
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-sales)
"?date-range=week")
:active? (= :pos-sales (:matched-route request))
:hx-boost "true"}
"Sales")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-expected-deposits)
"?date-range=week")
:active? (= :pos-expected-deposits (:matched-route request))
:hx-boost "true"}
"Expected Deposits")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-tenders)
"?date-range=week")
:active? (= :pos-tenders (:matched-route request))
:hx-boost "true"}
"Tenders")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-refunds)
"?date-range=week")
:active? (= :pos-refunds (:matched-route request))
:hx-boost "true"}
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-refunds)
"?date-range=week")
:active? (= :pos-refunds (:matched-route request))
:hx-boost "true"}
"Refunds")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-cash-drawer-shifts)
"?date-range=week")
:active? (= :cash-drawer-shifts (:matched-route request))
:hx-boost "true"}
"Cash drawer shifts"))))
"Refunds")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-cash-drawer-shifts)
"?date-range=week")
:active? (= :cash-drawer-shifts (:matched-route request))
:hx-boost "true"}
"Cash drawer shifts"))
;; TODO make specific routes for categories
;; TODO auto-apen sub menus
(menu-button- {"@click.prevent" "if (selected == 'payments') {selected = null } else { selected = 'payments'} "
:icon svg/payments}
"Payments")
@@ -224,26 +227,35 @@
:requires-feedback-transactions)} "Client Review")
(menu-button- {:href (bidi/path-for client-routes/routes
:approved-transactions)} "Approved")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:transaction-insights)} "Insights"))]
(when (can? (:identity request)
{:subject :transaction :activity :insights})
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:transaction-insights)} "Insights")))]
(menu-button- {"@click.prevent" "if (selected == 'ledger') {selected = null } else { selected = 'ledger'} "
:icon svg/receipt}
"Ledger")
(sub-menu- {:selector "ledger"
:active? (= "ledger" selected)}
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Register")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss)} "Profit & Loss")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss-detail)} "Profit & Loss Detail")
(menu-button- {:href (bidi/path-for client-routes/routes
:cash-flows)} "Cash Flows")
(menu-button- {:href (bidi/path-for client-routes/routes
:balance-sheet)} "Balance Sheet")
(menu-button- {:href (bidi/path-for client-routes/routes
:external-import-ledger)} "External Ledger Import"))]))
(when (can? (:identity request)
{:subject :ledger-page})
(list
(menu-button- {"@click.prevent" "if (selected == 'ledger') {selected = null } else { selected = 'ledger'} "
:icon svg/receipt}
"Ledger")
(sub-menu- {:selector "ledger"
:active? (= "ledger" selected)}
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Register")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss)} "Profit & Loss")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss-detail)} "Profit & Loss Detail")
(menu-button- {:href (bidi/path-for client-routes/routes
:cash-flows)} "Cash Flows")
(menu-button- {:href (bidi/path-for client-routes/routes
:balance-sheet)} "Balance Sheet")
(when (can? (:identity request)
{:subject :ledger
:activity :import})
(menu-button- {:href (bidi/path-for client-routes/routes
:external-import-ledger)} "External Ledger Import")))))]))
(defn company-aside-nav- [_]
@@ -330,7 +342,7 @@
:active? (= :admin-rules matched-route)
:href (bidi/path-for ssr-routes/only-routes
:admin-history)
:hx-boost "true" }
:hx-boost "true"}
"History")]
[:li
@@ -342,9 +354,10 @@
"Background Jobs")]
(menu-button- {:icon svg/arrow-in
"@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "}
"Import")
(when (can? (:identity request) {:subject :invoice :activity :import})
(menu-button- {:icon svg/arrow-in
"@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "}
"Import"))
(sub-menu- {:selector "import"}
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
@@ -356,10 +369,10 @@
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::ib-routes/page)
:active? (= ::ib-routes/page matched-route)
:hx-boost true}
:hx-boost true}
"Import Batches")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:admin-ezcater-xls)
:active? (= :admin-ezcater-xls matched-route)
:hx-boost "true"}
:hx-boost "true"}
"EZCater XLS Import"))])

View File

@@ -20,8 +20,7 @@
:hx-swap "outerHTML"
:hx-target-400 "#form-errors .error-content"
:hx-trigger "submit"
:hx-target "this"
"x-trap" "true"})
:hx-target "this" })
(defprotocol ModalWizardStep
(step-key [this])

View File

@@ -5,6 +5,7 @@
[auto-ap.datomic.invoices :as d-invoices]
[auto-ap.graphql.utils :refer [assert-can-see-client
assert-not-locked exception->4xx]]
[auto-ap.logging :as alog]
[auto-ap.routes.invoice :as route]
[auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated]]
@@ -69,8 +70,6 @@
(defn check-vendor-default-account [vendor-id]
(some? (:vendor/default-account (get-vendor vendor-id))))
;; TODO negative expense accounts for negative invoices?
(def new-form-schema
[:map
[:db/id {:optional true} [:maybe entity-id]]
@@ -97,7 +96,7 @@
[:fn {:error/message "Not an allowed account."}
check-allowance]]]
[:invoice-expense-account/location :string]
[:invoice-expense-account/amount [:double {:min 0}]]]
[:invoice-expense-account/amount :double]]
[:fn {:error/fn (fn [r x] (:type r))
:error/path [:invoice-expense-account/location]} check-invoice-expense-account-location]]]]])
@@ -557,36 +556,67 @@
(when-not (dollars= total expense-account-total)
(form-validation-error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")")))))
(defn maybe-spread-locations [invoice]
(let [valid-locations (pull-attr (dc/db conn) :client/locations (:invoice/client invoice))]
(with-precision 2
(let [expense-accounts (vec (mapcat
(fn [ea]
(let [cents-to-distribute (int (Math/round (Math/abs (* (:invoice-expense-account/amount ea) 100))))]
(if (= "Shared" (:invoice-expense-account/location ea))
(->> valid-locations
(map
(fn [cents location]
(assoc ea
:db/id (random-tempid)
:invoice-expense-account/amount (* 0.01 cents)
:invoice-expense-account/location location))
(rm/spread-cents cents-to-distribute (count valid-locations))))
[ea])))
(:invoice/expense-accounts invoice)))
expense-accounts (mapv
(fn [a]
(update a :invoice-expense-account/amount
#(with-precision 2
(double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP)))))
expense-accounts)
leftover (with-precision 2 (.round (bigdec (- (Math/abs (:invoice/total invoice))
(Math/abs (reduce + 0.0 (map #(:invoice-expense-account/amount %) expense-accounts)))))
*math-context*))
accounts (if (seq expense-accounts)
(update-in expense-accounts [(dec (count expense-accounts)) :invoice-expense-account/amount] #(+ % (double leftover)))
[])]
(assoc invoice :invoice/expense-accounts (into [] accounts))))))
(defn- calculate-spread
"Helper function to calculate the amount to be assigned to each location"
[shared-amount total-locations]
(let [base-amount (int (/ shared-amount total-locations))
remainder (- shared-amount (* base-amount total-locations))]
{:base-amount base-amount
:remainder remainder}))
(defn- spread-expense-account
"Spreads the expense account amount across the given locations"
[locations expense-account]
(if (= "Shared" (:invoice-expense-account/location expense-account))
(let [{:keys [base-amount remainder]} (calculate-spread (:invoice-expense-account/amount expense-account) (count locations))]
(map-indexed (fn [idx _]
(assoc expense-account
:invoice-expense-account/amount (+ base-amount (if (< idx remainder) 1 0))
:invoice-expense-account/location (nth locations idx)))
locations))
[expense-account]))
(defn $->cents [x]
(int
(let [result (* 100M (bigdec x))]
(.setScale result 0 java.math.BigDecimal/ROUND_HALF_UP))))
(defn cents->$ [x]
(double
(let [result (* 0.01M (bigdec x))]
(.setScale result 2 java.math.BigDecimal/ROUND_HALF_UP))))
(defn- apply-total-delta-to-account [invoice-total eas]
(when (seq eas)
(let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas)))
leftover-beyond-a-single-cent? (or (< leftover -1)
(> leftover 1))
leftover (if leftover-beyond-a-single-cent?
0
leftover)
[first-eas & rest] eas]
(cons
(update first-eas :invoice-expense-account/amount #(+ % leftover))
rest))))
(defn maybe-spread-locations
"Converts any expense account for a \"Shared\" location into a separate expense account for all valid locations for that client"
([invoice]
(maybe-spread-locations invoice (pull-attr (dc/db conn) :client/locations (:invoice/client invoice))))
([invoice locations]
(update-in invoice
[:invoice/expense-accounts]
(fn [expense-accounts]
(->> expense-accounts
(map (fn [ea] (update ea :invoice-expense-account/amount $->cents)))
(mapcat (partial spread-expense-account locations))
(apply-total-delta-to-account ($->cents (:invoice/total invoice)))
(map (fn [ea] (update ea :invoice-expense-account/amount cents->$))))))))
(defrecord NewWizard2 [_ current-step]
mm/LinearModalWizard
@@ -622,9 +652,27 @@
new-form-schema)
(submit [this {:keys [multi-form-state request-method identity] :as request}]
(let [invoice (:snapshot multi-form-state)
_ (alog/peek invoice)
extant? (:db/id invoice)
client-id (->db-id (:invoice/client invoice))
vendor-id (->db-id (:invoice/vendor invoice))
paid-amount (if-let [outstanding-balance
(and extant?
(-
(pull-attr (dc/db conn)
:invoice/total
(:db/id invoice))
(pull-attr (dc/db conn)
:invoice/outstanding-balance
(:db/id invoice))))]
outstanding-balance
0.0)
outstanding-balance (- (or
(:invoice/total (:step-params multi-form-state))
(:invoice/total (:snapshot multi-form-state)))
paid-amount)
transaction [:upsert-invoice (-> multi-form-state
:snapshot
(assoc :db/id (or (:db/id invoice) "invoice"))
@@ -638,11 +686,11 @@
:invoice-expense-account/amount (or (:invoice/total (:step-params multi-form-state))
(:invoice/total (:snapshot multi-form-state)))}]))
(assoc
:invoice/outstanding-balance (or
(:invoice/total (:step-params multi-form-state))
(:invoice/total (:snapshot multi-form-state)))
:invoice/outstanding-balance outstanding-balance
:invoice/import-status :import-status/imported
:invoice/status :invoice-status/unpaid)
:invoice/status (if (dollars= 0.0 outstanding-balance)
:invoice-status/paid
:invoice-status/unpaid))
(maybe-spread-locations)
(update :invoice/date coerce/to-date)
(update :invoice/due coerce/to-date)

View File

@@ -401,17 +401,19 @@
(when (can? (:identity request) {:subject :invoice :activity :create})
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)}
"New invoice"))])
:row-buttons (fn [_ entity]
[(when (= :invoice-status/unpaid (:invoice/status entity))
:row-buttons (fn [request entity]
[(when (and (= :invoice-status/unpaid (:invoice/status entity))
(can? (:identity request) {:subject :invoice :activity :delete}))
(com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes
::route/delete
:db/id (:db/id entity))
:hx-confirm "Are you sure you want to void this invoice?"}
svg/trash))
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
::route/edit-wizard
:db/id (:db/id entity)) }
svg/pencil)])
(when (can? (:identity request) {:subject :invoice :activity :edit})
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
::route/edit-wizard
:db/id (:db/id entity))}
svg/pencil))])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Invoices"]]
:title (fn [r]

View File

@@ -19,6 +19,7 @@
[com.brunobonacci.mulog.buffer :as rb]
[config.core :refer [env]]
[datomic.api :as dc]
[puget.printer :as puget]
[datomic.api :as d]
[figwheel.main.api]
[hawk.core]
@@ -27,30 +28,32 @@
(:import (org.apache.commons.io.input BOMInputStream)
[org.eclipse.jetty.server.handler.gzip GzipHandler]))
(defn println-event [item]
(printf "%s: %s - %s:%s by %s\n"
(str (c/to-date-time (:mulog/timestamp item)))
(:mulog/namespace item) (:mulog/event-name item)
(if (:mulog/duration item)
(str " " (int (/ (:mulog/duration item) 1000000)) "ms")
"")
(:user-name item))
(println (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user)))
#_(puget/cprint (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user))
{:seq-limit 10})
#_(printf "%s: %s - %s:%s by %s\n"
(str (c/to-date-time (:mulog/timestamp item)))
(:mulog/namespace item) (:mulog/event-name item)
(if (:mulog/duration item)
(str " " (int (/ (:mulog/duration item) 1000000)) "ms")
"")
(:user-name item))
#_(println (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user)))
(when (= :auto-ap.logging/peek (:mulog/event-name item))
(println "\u001B[31mTEST")
)
(puget/cprint (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user))
{:seq-limit 10})
(println))
@@ -354,7 +357,7 @@
`src` or `resources`."
[]
(println "starting auto reset")
(hawk.core/watch! [{:paths ["src/"]
(hawk.core/watch! [{:paths ["src/" "test/"]
:handler auto-reset-handler}]))