implements import page in new UI

This commit is contained in:
2024-05-26 22:28:31 -07:00
parent af8e55803d
commit e43f313ac0
6 changed files with 386 additions and 224 deletions

File diff suppressed because one or more lines are too long

View File

@@ -86,7 +86,7 @@
[:button (update params
:class #(cond-> %
true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center relative justify-center"
true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center relative justify-center disabled:opacity-50"
(bg-colors (:color params) (:disabled params)))
(not (:disabled params))

View File

@@ -21,4 +21,5 @@
[:payment/status :xform iol-ion.query/ident] [:db/ident]}]}]
#_[:payment/_invoices :as :invoice/payments]
[:invoice/status :xform iol-ion.query/ident] [:db/ident]
[:invoice/import-status :xform iol-ion.query/ident] [:db/ident]
:invoice/vendor [:vendor/name :db/id]}])

View File

@@ -1,33 +1,46 @@
(ns auto-ap.ssr.invoice.import
(:require [auto-ap.client-routes :as client-routes]
(:require [amazonica.aws.s3 :as s3]
[auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3 conn
merge-query observable-query pull-many]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.permissions :refer [can?]]
:refer [add-sorter-fields apply-pagination apply-sort-3
audit-transact conn merge-query observable-query
pull-attr pull-many random-tempid]]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.invoices :as d-invoices]
[auto-ap.datomic.vendors :as d-vendors]
[auto-ap.graphql.utils :refer [assert-can-see-client
assert-not-locked
exception->notification
extract-client-ids]]
[auto-ap.logging :as alog]
[auto-ap.parse :as parse]
[auto-ap.permissions :refer [can? wrap-must]]
[auto-ap.routes.invoice :as route]
[auto-ap.routes.payments :as payment-route]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.invoice.common :refer [default-read]]
[auto-ap.ssr.pos.common :refer [date-range-field*]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [clj-date-schema entity-id html-response main-transformer
ref->enum-schema strip wrap-implied-route-param]]
:refer [apply-middleware-to-all-handlers clj-date-schema
entity-id html-response main-transformer ref->enum-schema
strip wrap-entity wrap-implied-route-param
wrap-schema-enforce]]
[auto-ap.time :as atime]
[auto-ap.utils :refer [dollars=]]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clj-time.coerce :as coerce :refer [to-date]]
[clojure.java.io :as io]
[clojure.string :as str]
[com.brunobonacci.mulog :as mu]
[config.core :refer [env]]
[datomic.api :as dc]
[hiccup.util :as hu]
[hiccup2.core :as hiccup]
[malli.core :as mc]))
[malli.core :as mc]
[auto-ap.client-routes :as client-routes])
(:import [java.util UUID]))
(defn exact-match-id* [request]
@@ -46,7 +59,7 @@
(defn filters [request]
[:form#invoice-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
::route/table)
::route/import-table)
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
@@ -112,13 +125,15 @@
valid-clients]}
(cond-> {:query {:find []
:in '[$ [?clients ?start ?end]]
:where '[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
:where '[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
[?e :invoice/import-status :import-status/pending]]}
:args [db
[valid-clients
(some-> (:start-date query-params) coerce/to-date)
(some-> (:end-date query-params) coerce/to-date)]]}
(:client-id query-params)
(merge-query {:query {:in ['?client-id]
:where ['[?e :invoice/client ?client-id]]}
@@ -283,11 +298,6 @@
[:end-date {:optional true}
[:maybe clj-date-schema]]]]))
(comment
(mc/decode query-schema
{:start " "}
main-transformer))
(defn selected->ids [request params]
(let [all-selected (:all-selected params)
selected (:selected params)
@@ -300,117 +310,93 @@
:else
selected)]
selected)
ids (->> (dc/q '[:find ?i
:in $ [?i ...]
:where [?i :invoice/import-status :import-status/pending]]
(dc/db conn)
ids)
(map first))]
ids))
(defn pay-button* [params]
(let [ids (:ids params)
selected-client-count (if (seq ids)
(ffirst
(dc/q '[:find (count ?c)
:in $ [?i ...]
:where [?i :invoice/client ?c]]
(dc/db conn)
ids))
(def upload-schema
[:map
[:force-client {:optional true}
[:maybe entity-id]]
[:force-vendor {:optional true}
[:maybe entity-id]]
[:force-location {:optional true}
[:maybe [:string {:decode/string strip :min 2 :max 2}]]]])
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))
total (reduce + 0.0 vendor-totals)]
[:div {:hx-target "this"
:hx-get (bidi/path-for ssr-routes/only-routes
::route/pay-wizard)
:hx-trigger "click from:#pay-button"
:x-data (hx/json {:popper nil
:hovering false})
"x-init" "popper = Popper.createPopper($refs.button, $refs.tooltip, {placement: 'bottom', strategy: 'fixed', modifiers: [{name: 'preventOverflow'}, {name: 'offset', options: {offset: [0, 10]}}]});"}
(com/button {:color :primary
:id "pay-button"
:disabled (or (= (count (:ids params)) 0)
(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)
:hx-swap "outerHTML"
:hx-trigger "selectedChanged from:body, htmx:afterSwap from:#entity-table"
"@mouseover" "hovering=true; $nextTick(() => popper.update())"
"@mouseout" "hovering=false;"
:x-ref "button"
:minimal-loading? true
:class "relative"}
(if (> (count (:ids params)) 0)
(format "Pay %d invoices ($%,.2f)"
(count (:ids params))
(or total 0.0))
"Pay")
(when (or (= 0 (count ids))
(> selected-client-count 1))
(com/badge {} "!")))
[:div (hx/alpine-appear {:x-ref "tooltip"
: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)
[:div "Can only pay for one client at a time"]
:else
[:div "Click to choose a bank account"])]]))
(defn pay-button [request]
(html-response
(pay-button* {:ids (selected->ids request
(:query-params request))})))
;; TODO test as a real user
(def grid-page
(helper/build {:id "entity-table"
:nav com/main-aside-nav
:check-boxes? true
:page-specific-nav filters
:above-grid (fn [request]
(com/content-card {}
[:div.px-4.py-3.space-y-4
[:h1.text-2xl.mb-3.font-bold "Import new invoices"]
[:form.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.max-w-md.w-md.text-center.cursor-pointer
{:action (bidi/path-for ssr-routes/only-routes
::route/import-page)
:method "POST"
:id "upload"}
"Drop files to upload here"]
[:script
(hiccup/raw
"
ezcater_dropzone = new Dropzone (\"#upload\", {
success: function (file, response) {
document.getElementById(\"page-notification\").innerHTML = response;
document.getElementById(\"page-notification\").style[\"display\"] = \"block\";
},
acceptedFiles: '.xls,.xlsx,.pdf,.csv',
disablePreviews: true
});")]]))
:above-grid (fn [{:keys [form-params form-errors] :as request}]
(com/content-card {}
[:div.px-4.py-3.space-y-4
[:div.flex.justify-between.items-center [:h1.text-2xl.mb-3.font-bold "Import new invoices"]
[:div.flex.gap-2.items-baseline "Trouble with the new upload experience?"
[:a {:href (bidi/path-for client-routes/routes :import-invoices )}
(com/pill {:color :secondary}
"Go back to previous version")]]]
[:div#page-notification.notification.block {:style {:display "none"}}]
[:form
{:action (bidi/path-for ssr-routes/only-routes
::route/import-file)
:method "POST"
:id "upload"}
(fc/start-form
form-params form-errors
[:div.flex.gap-4
(fc/with-field :force-client
(com/validated-field {:label "Force client"
:errors (fc/field-errors)}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :company-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
(fc/with-field :force-location
(com/validated-field {:label "Force location"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:size 2})))
(fc/with-field :force-vendor
(com/validated-field {:label "Force vendor"
:errors (fc/field-errors)}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))])
[:div.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-blue-700.text-lg "Drop files to upload here"]]
[:script
(hiccup/raw
"ezcater_dropzone = new Dropzone (\"#upload\", {
success: function (file, response) {
document.getElementById(\"page-notification\").innerHTML = response;
document.getElementById(\"page-notification\").style[\"display\"] = \"block\";
document.body.dispatchEvent(new Event('invalidated'));
},
acceptedFiles: '.xls,.xlsx,.pdf,.csv',
disablePreviews: true
});")]]))
:fetch-page fetch-page
:oob-render
@@ -422,43 +408,37 @@
(mc/decode query-schema p main-transformer))
:action-buttons (fn [request]
(let [[_ _ outstanding total] (:page-results request)]
[(com/pill {:color :primary} "Outstanding: "
(format "$%,.2f" outstanding))
(com/pill {:color :secondary} "Total: "
(format "$%,.2f" total))
(when (can? (:identity request) {:subject :invoice :activity :bulk-delete})
(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete))
[
(when (can? (:identity request) {:subject :invoice :activity :import})
(com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes ::route/bulk-approve))
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
"hx-include" "#invoice-filters"
:color :primary
":disabled" "$data.all_selected || ($data.selected && $data.selected.length > 0) ? false: true "
}
"Approve selected"))
(when (can? (:identity request) {:subject :invoice :activity :import})
(com/button {:hx-delete (str (bidi/path-for ssr-routes/only-routes ::route/bulk-disapprove))
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
"x-init" ""
"hx-include" "#invoice-filters"
":disabled" "$data.all_selected || ($data.selected && $data.selected.length > 0) ? false: true "
:color :red}
"Void selected"))
(when (can? (:identity request) {:subject :invoice :activity :pay})
(pay-button* {:ids (selected->ids request
(:query-params request))}))
(when (can? (:identity request) {:subject :invoice :activity :create})
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)}
"New invoice"))]))
"Disapprove selected"))]))
:row-buttons (fn [request entity]
[(when (and (= :invoice-status/unpaid (:invoice/status entity))
(can? (:identity request) {:subject :invoice :activity :delete}))
[(when (and (= :import-status/pending (:invoice/import-status entity))
(can? (:identity request) {:subject :invoice :activity :import}))
(com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes
::route/delete
::route/disapprove
:db/id (:db/id entity))
:hx-confirm "Are you sure you want to void this invoice?"}
svg/trash))
(when (and (can? (:identity request) {:subject :invoice :activity :edit})
(#{:invoice-status/unpaid :invoice-status/paid} (:invoice/status entity)))
:hx-confirm "Are you sure you want to remove this invoice?"}
svg/thumbs-down))
(when (and (= :import-status/pending (:invoice/import-status entity))
(can? (:identity request) {:subject :invoice :activity :import}))
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
::route/edit-wizard
:db/id (:db/id entity))}
svg/pencil))
(when (and (can? (:identity request) {:subject :invoice :activity :edit})
(#{:invoice-status/voided} (:invoice/status entity)))
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
::route/unvoid
:db/id (:db/id entity))}
svg/undo))])
::route/approve
:db/id (:db/id entity))}
svg/thumbs-up))])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Invoices"]]
@@ -467,7 +447,7 @@
(some-> r :route-params :status name str/capitalize (str " "))
"Invoices"))
:entity-name "invoices"
:route ::route/table
:route ::route/import-table
:headers [{:key "client"
:name "Client"
:sort-key "client"
@@ -488,36 +468,6 @@
:show-starting "lg"
:render (fn [{:invoice/keys [date]}]
(some-> date (atime/unparse-local atime/normal-date)))}
{:key "status"
:name "Status"
:render (fn [{:invoice/keys [status]}]
(condp = status
:invoice-status/paid
(com/pill {:color :primary} "Paid")
:invoice-status/unpaid
(com/pill {:color :secondary} "Unpaid")
:invoice-status/voided
(com/pill {:color :red} "Voided")
nil
""))}
{:key "accounts"
:name "Account"
:show-starting "lg"
: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
(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}
"+ " (dec (count expense-accounts)) " more")])])}
{:key "outstanding"
:name "Outstanding"
:sort-key "outstanding-balance"
@@ -526,44 +476,241 @@
[:div
(some->> outstanding-balance (format "$%,.2f"))
(when-not (dollars= outstanding-balance total)
[:div.text-xs.text-gray-400 (format "of $%,.2f" total)])])}
{:key "links"
:name "Links"
:show-starting "lg"
:class "w-8"
:render (fn [i]
(link-dropdown
(concat (->> i
:invoice/payments
(filter (fn [p]
(not= :payment-status/voided
(:payment/status p))))
(mapcat (fn [p]
(cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
::payment-route/all-page)
{:exact-match-id (:db/id p)})
:content (str (format "$%,.2f" (:payment/amount p))
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for client-routes/routes :transactions)
{:exact-match-id (:db/id (first (:payment/transaction p)))})
:color :secondary
:content "Transaction"})))))
(when (:invoice/journal-entry i)
[{:link (hu/url (bidi/path-for client-routes/routes :ledger)
{:exact-match-id (:db/id (first (:invoice/journal-entry i)))})
:color :yellow
:content "Ledger entry"}])
(when (:invoice/source-url i)
[{:link (:invoice/source-url i)
:color :secondary
:content "File"}]))))}]}))
[:div.text-xs.text-gray-400 (format "of $%,.2f" total)])])}]}))
(def row* (partial helper/row* grid-page))
(defn disapprove [{invoice :entity :as request identity :identity}]
(when-not (= :import-status/pending (:invoice/import-status invoice))
(throw (ex-info (str "Cannot disapprove an invoice if it is not pending." (:invoice/import-status invoice))
{:type :notification})))
(exception->notification
#(assert-can-see-client identity (:db/id (:invoice/client invoice))))
(audit-transact [[:db/retractEntity (:db/id invoice)]] identity)
(html-response (row* (:identity request) invoice
{:class "live-removed"})
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))}))
(defn approve [{invoice :entity :as request identity :identity}]
(when-not (= :import-status/pending (:invoice/import-status invoice))
(throw (ex-info (str "Cannot approve an invoice if it is not pending." (:invoice/import-status invoice))
{:type :notification})))
(exception->notification
#(do (assert-can-see-client identity (:db/id (:invoice/client invoice)))
(assert-not-locked (-> invoice :invoice/client :db/id) (-> invoice :invoice/date))))
(audit-transact [ [:upsert-invoice {:db/id (:db/id invoice) :invoice/import-status :import-status/imported}]] identity)
(html-response (row* (:identity request) invoice
{:class "live-added"})
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))}))
(defn bulk-disapprove [request]
(let [ids (selected->ids request (:form-params request))
updates (map
(fn [i] [:db/retractEntity i])
ids) ]
(audit-transact updates (:identity request) )
(html-response [:div]
:headers {"hx-trigger" (hx/json { :notification (format "Successfully disapproved %d invoices."
(count ids))
:invalidated "invalidated"})})))
(defn bulk-approve [request]
(let [ids (selected->ids request (:form-params request))]
(exception->notification
#(doseq [i ids
:let [invoice (dc/pull (dc/db conn) '[{:invoice/client [:db/id]}
:invoice/date] i)]]
(assert-can-see-client (:identity request) (-> invoice :invoice/client :db/id))
(assert-not-locked (-> invoice :invoice/client :db/id) (-> invoice :invoice/date))))
(let [transactions (map (fn [i] [:upsert-invoice {:db/id i :invoice/import-status :import-status/imported}]) ids)]
(audit-transact transactions (:identity request)))
(html-response [:div]
:headers {"hx-trigger" (hx/json { :notification (format "Successfully approved %d invoices."
(count ids))
:invalidated "invalidated"})})))
(defn upload-invoices [{{files :file
files-2 "file"
client :client
client-2 "client"
location :location
location-2 "location"
vendor :vendor
vendor-2 "vendor"} :params
user :identity}]
(let [files (or files files-2)
client (or client client-2)
location (or location location-2)
vendor (some-> (or vendor vendor-2)
(Long/parseLong))
]
))
(defn match-vendor [vendor-code forced-vendor]
(when (and (not forced-vendor) (str/blank? vendor-code))
(throw (ex-info (str "No vendor found. Please supply an forced vendor.")
{:vendor-code vendor-code})))
(let [vendor-id (or forced-vendor
(->> (dc/q
{:find ['?vendor]
:in ['$ '?vendor-name]
:where ['[?vendor :vendor/name ?vendor-name]]}
(dc/db conn) vendor-code)
first
first))]
(when-not vendor-id
(throw (ex-info (str "Vendor matching name \"" vendor-code "\" not found.")
{:vendor-code vendor-code})))
(if-let [matching-vendor (->> (dc/q
{:find [(list 'pull '?vendor-id d-vendors/default-read)]
:in ['$ '?vendor-id]}
(dc/db conn) vendor-id)
first
first)]
matching-vendor
(throw (ex-info (str "No vendor with the name " vendor-code " was found.")
{:vendor-code vendor-code})))))
(defn import->invoice [{:keys [invoice-number source-url customer-identifier account-number total date vendor-code text full-text client-override vendor-override location-override import-status]}]
(let [matching-client (cond
client-override client-override
account-number (:db/id (d-clients/exact-match account-number))
customer-identifier (:db/id (d-clients/best-match customer-identifier)))
matching-vendor (match-vendor vendor-code vendor-override)
matching-location (or (when-not (str/blank? location-override)
location-override)
(parse/best-location-match (dc/pull (dc/db conn)
[{:client/location-matches [:location-match/location :location-match/matches]}
:client/default-location
:client/locations]
matching-client)
text
full-text))]
#:invoice {:db/id (random-tempid)
:invoice/client matching-client
:invoice/client-identifier (or account-number customer-identifier)
:invoice/vendor (:db/id matching-vendor)
:invoice/source-url source-url
:invoice/invoice-number invoice-number
:invoice/total (Double/parseDouble total)
:invoice/date (to-date date)
:invoice/location matching-location
:invoice/import-status (or import-status :import-status/pending)
:invoice/outstanding-balance (Double/parseDouble total)
:invoice/status :invoice-status/unpaid}))
(defn validate-invoice [invoice user]
(when-not (:invoice/client invoice)
(throw (ex-info (str "Searched clients for '" (:invoice/client-identifier invoice) "'. No client found in file. Select a client first.")
{:invoice-number (:invoice/invoice-number invoice)
:customer-identifier (:invoice/client-identifier invoice)})))
(assert-can-see-client user (:invoice/client invoice))
(doseq [k [:invoice/invoice-number :invoice/client :invoice/vendor :invoice/total :invoice/outstanding-balance :invoice/date]]
(when (not (get invoice k))
(throw (ex-info (str (name k) "not found on invoice " invoice)
invoice))))
invoice)
(defn admin-only-if-multiple-clients [is]
(let [client-count (->> is
(map :invoice/client)
set
count)]
(map #(assoc % :invoice/source-url-admin-only (boolean (> client-count 1))) is)))
(defn import-uploaded-invoice [user imports]
(alog/info ::importing-uploaded :count (count imports))
(let [potential-invoices (->> imports
(map import->invoice)
(map #(validate-invoice % user))
admin-only-if-multiple-clients
(mapv d-invoices/code-invoice)
(mapv (fn [i] [:propose-invoice i])))]
(alog/info ::creating-invoice :invoices potential-invoices)
(let [tx (audit-transact potential-invoices user)]
(when-not (seq (dc/q '[:find ?i
:in $ [?i ...]
:where [?i :invoice/invoice-number]]
(:db-after tx)
(map :e (:tx-data tx))))
(throw (ex-info "No new invoices found."
{})))
tx)))
(defn import-file [request]
(let [{:keys [file force-client force-vendor force-location]} (:multipart-params request)
{:keys [filename tempfile]} file]
(mu/with-context {:parsing-file filename}
(try
(let [extension (last (str/split (.getName (io/file filename)) #"\."))
s3-location (str "invoice-files/" (str (UUID/randomUUID)) "." extension)
_ (s3/put-object (:data-bucket env)
s3-location
(io/input-stream tempfile)
{:content-type (if (= "csv" extension)
"text/csv"
"application/pdf")
:content-length (.length tempfile)})
imports (->> (parse/parse-file (.getPath tempfile) filename)
(map #(assoc %
:client-override force-client
:location-override force-location
:vendor-override force-vendor
:source-url (str "https://" (:data-bucket env)
"/"
s3-location))))]
(import-uploaded-invoice (:identity request) imports))
(html-response [:div.bg-primary-50.p-4.text-primary-700.rounded-lg "Invoices have been successfully uploaded."])
(catch Exception e
(alog/warn ::couldnt-import-upload
:error e)
(html-response [:div.bg-red-50.p-4.text-red-700.rounded-lg (.getMessage e)])
#_{:status 400
:body (pr-str {:message (.getMessage e)
:error (.toString e)
:data (ex-data e)})
:headers {"Content-Type" "application/edn"}})))))
#_(defn wrap-test [handler]
(fn [request]
(clojure.pprint/pprint (:multipart-params request))
(handler request )))
(def key->handler
{::route/import-page
(->
(helper/page-route grid-page :parse-query-params? false)
(wrap-implied-route-param :status nil))})
(apply-middleware-to-all-handlers
{::route/import-page
(->
(helper/page-route grid-page :parse-query-params? false)
(wrap-implied-route-param :status nil))
::route/import-table
(-> (helper/table-route grid-page :parse-query-params? false)
(wrap-implied-route-param :status nil))
::route/disapprove (-> disapprove
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
::route/approve (-> approve
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
::route/bulk-disapprove (-> bulk-disapprove
(wrap-schema-enforce :form-schema query-schema))
::route/bulk-approve (-> bulk-approve
(wrap-schema-enforce :form-schema query-schema))
::route/import-file (-> import-file
(wrap-schema-enforce :multipart-schema upload-schema))}
(fn [a]
(-> a
(wrap-must {:subject :invoice :activity :import})))))

View File

@@ -392,7 +392,7 @@
:error {:explain (mc/explain schema entity)}}))))
(defn schema-enforce-request [{:keys [form-params query-params hx-query-params params] :as request} & {:keys [form-schema hx-schema query-schema route-schema params-schema]}]
(defn schema-enforce-request [{:keys [form-params query-params hx-query-params multipart-params params] :as request} & {:keys [form-schema multipart-schema hx-schema query-schema route-schema params-schema]}]
(let [request (try
(cond-> request
(and (:params request) params-schema)
@@ -408,6 +408,13 @@
route-schema
(:route-params request)
main-transformer))
(and (:multipart-params request) multipart-schema)
(assoc :multipart-params
(mc/coerce
multipart-schema
(:multipart-params request)
main-transformer))
(and form-schema form-params)
(assoc :form-params
@@ -453,10 +460,11 @@
:error (:data (ex-data e))}))))]
request))
(defn wrap-schema-enforce [handler & {:keys [form-schema query-schema route-schema params-schema hx-schema]}]
(defn wrap-schema-enforce [handler & {:keys [form-schema query-schema route-schema params-schema hx-schema multipart-schema]}]
(fn [request]
(handler (schema-enforce-request request
:hx-schema hx-schema
:multipart-schema multipart-schema
:form-schema form-schema
:query-schema query-schema
:route-schema route-schema

View File

@@ -3,7 +3,13 @@
"/unpaid" ::unpaid-page
"/paid" ::paid-page
"/voided" ::voided-page
"/import" ::import-page}
"/import" {"" ::import-page
"/upload" ::import-file
"/table" ::import-table
"/disapprove" {:delete ::bulk-disapprove}
"/approve" {:put ::bulk-approve}
["/" [#"\d+" :db/id] "/disapprove"] ::disapprove
["/" [#"\d+" :db/id] "/approve"] ::approve}}
"/new" {:get ::new-wizard
:post ::new-invoice-submit
:put ::new-invoice-submit