diff --git a/src/clj/auto_ap/square/core3.clj b/src/clj/auto_ap/square/core3.clj index 1c5e82ed..a5ff1c20 100644 --- a/src/clj/auto_ap/square/core3.clj +++ b/src/clj/auto_ap/square/core3.clj @@ -911,23 +911,24 @@ (let [[c [l]] (get-square-client-and-location "NGFA")])) (clojure.data.csv/write-csv *out* - (for [c (get-square-clients) - l (:client/square-locations c) - :when (:square-location/client-location l) - bad-row (try (->> @(search c l (coerce/to-date-time #inst "2024-04-01T00:00:00-07:00") (coerce/to-date-time #inst "2024-04-15T23:59:00-07:00")) - (filter #(not (should-import-order? %))) - (map #(first (deref (order->sales-order c l %)))) - (filter (fn already-exists [o] - (when (:sales-order/external-id o) - (seq (dc/q '[:find ?i - :in $ ?ei - :where [?i :sales-order/external-id ?ei]] - (dc/db conn) - (:sales-order/external-id o))))))) - (catch Exception e - []))] - [(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row) (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)]) - :separator \tab) + (for [c (get-square-clients) + l (:client/square-locations c) + :when (:square-location/client-location l) + bad-row (try (->> @(search c l (coerce/to-date-time #inst "2024-04-01T00:00:00-07:00") (coerce/to-date-time #inst "2024-04-15T23:59:00-07:00")) + (filter #(not (should-import-order? %))) + (map #(first (deref (order->sales-order c l %)))) + (filter (fn already-exists [o] + (when (:sales-order/external-id o) + (seq (dc/q '[:find ?i + :in $ ?ei + :where [?i :sales-order/external-id ?ei]] + (dc/db conn) + (:sales-order/external-id o))))))) + (catch Exception e + []))] + [(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row) (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)]) + :separator \tab) + diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 1d57ff60..93a43523 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -83,7 +83,7 @@ (defn main-aside-nav- [request] (let [selected (cond - (#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new} (:matched-route request)) + (#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page} (:matched-route request)) "invoices" (#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request)) @@ -138,8 +138,10 @@ (when (can? (:identity request) {:subject :invoice :activity :import}) - (menu-button- {:href (bidi/path-for client-routes/routes - :import-invoices)} "Import")) + (menu-button- {:href (bidi/path-for ssr-routes/only-routes + ::invoice-route/import-page) + :active? (= ::invoice-route/import-page (:matched-route request)) + :hx-boost "true"} "Import")) (when (can? (:identity request) diff --git a/src/clj/auto_ap/ssr/grid_page_helper.clj b/src/clj/auto_ap/ssr/grid_page_helper.clj index fa36881f..63213091 100644 --- a/src/clj/auto_ap/ssr/grid_page_helper.clj +++ b/src/clj/auto_ap/ssr/grid_page_helper.clj @@ -263,6 +263,8 @@ :identity (:identity request) :request request} (apply com/breadcrumbs {} (:breadcrumbs grid-spec)) + (when (:above-grid grid-spec) + ( (:above-grid grid-spec) request)) [:div {:x-data (hx/json {:selected [] :all_selected false}) "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})" :x-init "$watch('selected', s=> $dispatch('selectedChanged', {selected: s, all_selected: all_selected}) ); @@ -313,6 +315,12 @@ [:=> [:cat [:map-of :keyword :any]] [:map-of :keyword :any]]] + [:above-grid + {:optional true + :default (fn [request])} + [:=> + [:cat request-spec] + vector?]] [:oob-render {:optional true :default (fn [request])} diff --git a/src/clj/auto_ap/ssr/invoice/import.clj b/src/clj/auto_ap/ssr/invoice/import.clj new file mode 100644 index 00000000..79518fd1 --- /dev/null +++ b/src/clj/auto_ap/ssr/invoice/import.clj @@ -0,0 +1,569 @@ +(ns auto-ap.ssr.invoice.import + (:require [auto-ap.client-routes :as client-routes] + [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?]] + [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.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]] + [auto-ap.time :as atime] + [auto-ap.utils :refer [dollars=]] + [bidi.bidi :as bidi] + [clj-time.coerce :as coerce] + [clojure.string :as str] + [datomic.api :as dc] + [hiccup.util :as hu] + [hiccup2.core :as hiccup] + [malli.core :as mc])) + + +(defn exact-match-id* [request] + (if (nat-int? (:exact-match-id (:query-params request))) + [:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"} + (com/hidden {:name "exact-match-id" + "x-model" "exact_match"}) + (com/pill {:color :primary} + [:span.inline-flex.space-x-2.items-center + [:div "exact match"] + [:div.w-3.h-3 + (com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"} + svg/x)]])] + [:div {:id "exact-match-id-tag"}])) + +(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) + "hx-target" "#entity-table" + "hx-indicator" "#entity-table"} + + (com/hidden {:name "status" + :value (some-> (:status (:query-params request)) name)}) + [:fieldset.space-y-6 + (com/field {:label "Vendor"} + (com/typeahead {:name "vendor" + :id "vendor" + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :value (:vendor (:query-params request)) + :value-fn :db/id + :content-fn :vendor/name})) + (date-range-field* request) + (com/field {:label "Check #"} + (com/text-input {:name "check-number" + :id "check-number" + :class "hot-filter" + :value (:check-number (:query-params request)) + :placeholder "e.g., 10001" + :size :small})) + (com/field {:label "Invoice #"} + (com/text-input {:name "invoice-number" + :id "invoice-number" + :class "hot-filter" + :value (:invoice-number (:query-params request)) + :placeholder "e.g., ABC-456" + :size :small})) + + (com/field {:label "Amount"} + [:div.flex.space-x-4.items-baseline + (com/money-input {:name "amount-gte" + :id "amount-gte" + :hx-preserve "true" + :class "hot-filter w-20" + :value (:amount-gte (:query-params request)) + :placeholder "0.01" + :size :small}) + [:div.align-baseline + "to"] + (com/money-input {:name "amount-lte" + :hx-preserve "true" + :id "amount-lte" + :class "hot-filter w-20" + :value (:amount-lte (:query-params request)) + :placeholder "9999.34" + :size :small})]) + (exact-match-id* request)]]) + + +(defn fetch-ids [db {:keys [query-params route-params] :as request}] + (let [valid-clients (extract-client-ids (:clients request) + (:client-id request) + (when (:client-code request) + [:client/code (:client-code request)])) + query + (if (:exact-match-id query-params) + {:query {:find '[?e] + :in '[$ ?e [?c ...]] + :where '[[?e :invoice/client ?c]]} + :args [db + (:exact-match-id query-params) + valid-clients]} + (cond-> {:query {:find [] + :in '[$ [?clients ?start ?end]] + :where '[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]} + :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]]} + :args [(:client-id query-params)]}) + + (:client-code query-params) + (merge-query {:query {:in ['?client-code] + :where ['[?e :invoice/client ?client-id] + '[?client-id :client/code ?client-code]]} + :args [(:client-code query-params)]}) + + + (:start (:due-range query-params)) (merge-query {:query {:in '[?start-due] + :where ['[?e :invoice/due ?due] + '[(>= ?due ?start-due)]]} + :args [(coerce/to-date (:start (:due-range query-params)))]}) + + (:end (:due-range query-params)) (merge-query {:query {:in '[?end-due] + :where ['[?e :invoice/due ?due] + '[(<= ?due ?end-due)]]} + :args [(coerce/to-date (:end (:due-range query-params)))]}) + + + (:import-status query-params) + (merge-query {:query {:in ['?import-status] + :where ['[?e :invoice/import-status ?import-status]]} + :args [(:import-status query-params)]}) + (:status route-params) + (merge-query {:query {:in ['?status] + :where ['[?e :invoice/status ?status]]} + :args [(:status route-params)]}) + (:vendor query-params) + (merge-query {:query {:in ['?vendor-id] + :where ['[?e :invoice/vendor ?vendor-id]]} + :args [(:db/id (:vendor query-params))]}) + + (:account-id query-params) + (merge-query {:query {:in ['?account-id] + :where ['[?e :invoice/expense-accounts ?iea ?] + '[?iea :invoice-expense-account/account ?account-id]]} + :args [(:account-id query-params)]}) + + (:amount-gte query-params) + (merge-query {:query {:in ['?amount-gte] + :where ['[?e :invoice/total ?total-filter] + '[(>= ?total-filter ?amount-gte)]]} + :args [(:amount-gte query-params)]}) + + (:amount-lte query-params) + (merge-query {:query {:in ['?amount-lte] + :where ['[?e :invoice/total ?total-filter] + '[(<= ?total-filter ?amount-lte)]]} + :args [(:amount-lte query-params)]}) + + (not-empty (:invoice-number query-params)) + (merge-query {:query {:in ['?invoice-number-like] + :where ['[?e :invoice/invoice-number ?invoice-number] + '[(.contains ^String ?invoice-number ?invoice-number-like)]]} + :args [(:invoice-number query-params)]}) + + (:scheduled-payments query-params) + (merge-query {:query {:in [] + :where ['[?e :invoice/scheduled-payment]]} + :args []}) + + (:unresolved query-params) + (merge-query {:query {:in [] + :where ['(or-join [?e] + (not [?e :invoice/expense-accounts]) + (and [?e :invoice/expense-accounts ?ea] + (not [?ea :invoice-expense-account/account])))]} + :args []}) + + (seq (:location query-params)) + (merge-query {:query {:in ['?location] + :where ['[?e :invoice/expense-accounts ?eas] + '[?eas :invoice-expense-account/location ?location]]} + :args [(:location query-params)]}) + + (:sort query-params) (add-sorter-fields {"client" ['[?e :invoice/client ?c] + '[?c :client/name ?sort-client]] + "vendor" ['[?e :invoice/vendor ?v] + '[?v :vendor/name ?sort-vendor]] + "description-original" ['[?e :transaction/description-original ?sort-description-original]] + "location" ['[?e :invoice/expense-accounts ?iea] + '[?iea :invoice-expense-account/location ?sort-location]] + "date" ['[?e :invoice/date ?sort-date]] + "due" ['[(get-else $ ?e :invoice/due #inst "2050-01-01") ?sort-due]] + "invoice-number" ['[?e :invoice/invoice-number ?sort-invoice-number]] + "total" ['[?e :invoice/total ?sort-total]] + "outstanding-balance" ['[?e :invoice/outstanding-balance ?sort-outstanding-balance]]} + query-params) + true + (merge-query {:query {:find ['?sort-default '?e]}})))] + + (->> (observable-query query) + (apply-sort-3 (assoc query-params :default-asc? false)) + (apply-pagination query-params)))) + + +(defn hydrate-results [ids db _] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id)) + refunds (->> ids + (map results) + (map first))] + refunds)) + +(defn sum-outstanding [ids] + + (->> + (dc/q {:find ['?id '?o] + :in ['$ '[?id ...]] + :where ['[?id :invoice/outstanding-balance ?o]]} + (dc/db conn) + ids) + (map last) + (reduce + + + 0.0))) + +(defn sum-total-amount [ids] + + (->> + (dc/q {:find ['?id '?o] + :in ['$ '[?id ...]] + :where ['[?id :invoice/total ?o]]} + (dc/db conn) + ids) + (map last) + (reduce + + + 0.0))) + +(defn fetch-page [request] + (let [db (dc/db conn) + {ids-to-retrieve :ids matching-count :count + all-ids :all-ids} (fetch-ids db request)] + + [(->> (hydrate-results ids-to-retrieve db request)) + matching-count + (sum-outstanding all-ids) + (sum-total-amount all-ids)])) + +(def query-schema (mc/schema + [:maybe [:map {:date-range [:date-range :start-date :end-date]} + [:sort {:optional true} [:maybe [:any]]] + [:per-page {:optional true :default 25} [:maybe :int]] + [:start {:optional true :default 0} [:maybe :int]] + [:amount-gte {:optional true} [:maybe :double]] + [:amount-lte {:optional true} [:maybe :double]] + [:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]] + [:check-number {:optional true} [:maybe [:string {:decode/string strip}]]] + [:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]] + [:status {:optional true} [:maybe (ref->enum-schema "invoice-status")]] + [:exact-match-id {:optional true} [:maybe entity-id]] + [:all-selected {:optional true :default nil} [:maybe :boolean]] + [:selected {:optional true :default nil} [:maybe [:vector {:coerce? true} + entity-id]]] + [:start-date {:optional true} + [:maybe clj-date-schema]] + [: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) + ids (cond + all-selected + (:ids (fetch-ids (dc/db conn) (-> request + (assoc :query-params params) + (assoc-in [:query-params :start] 0) + (assoc-in [:query-params :per-page] 250)))) + + + :else + selected)] + 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)) + + 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 + });")]])) + + :fetch-page fetch-page + :oob-render + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true) + (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)]) + :query-schema query-schema + :parse-query-params (fn [p] + (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)) + "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})" + "hx-include" "#invoice-filters" + :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"))])) + :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)) + (when (and (can? (:identity request) {:subject :invoice :activity :edit}) + (#{:invoice-status/unpaid :invoice-status/paid} (:invoice/status entity))) + (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))]) + + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} + "Invoices"]] + :title (fn [r] + (str + (some-> r :route-params :status name str/capitalize (str " ")) + "Invoices")) + :entity-name "invoices" + :route ::route/table + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :invoice/client :client/name)} + {:key "vendor" + :name "Vendor" + :sort-key "vendor" + :render #(-> % :invoice/vendor :vendor/name)} + {:key "invoice-number" + :name "Invoice number" + :sort-key "invoice-number" + :render :invoice/invoice-number} + {:key "date" + :sort-key "date" + :name "Date" + :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" + :class "text-right" + :render (fn [{:invoice/keys [outstanding-balance total]}] + [: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"}]))))}]})) + +(def row* (partial helper/row* grid-page)) + +(def key->handler + {::route/import-page + (-> + (helper/page-route grid-page :parse-query-params? false) + (wrap-implied-route-param :status nil))}) + + diff --git a/src/clj/auto_ap/ssr/invoices.clj b/src/clj/auto_ap/ssr/invoices.clj index 582c3a20..9acb830d 100644 --- a/src/clj/auto_ap/ssr/invoices.clj +++ b/src/clj/auto_ap/ssr/invoices.clj @@ -33,6 +33,7 @@ [auto-ap.ssr.hx :as hx] [auto-ap.ssr.invoice.common :refer [default-read]] [auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard] + [auto-ap.ssr.invoice.import :as invoice-import] [auto-ap.ssr.pos.common :refer [date-range-field*]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils @@ -1249,7 +1250,8 @@ (mm/wrap-decode-multi-form-state)) ::route/table (helper/table-route grid-page :parse-query-params? false)} - (merge new-invoice-wizard/key->handler)) + (merge new-invoice-wizard/key->handler) + (merge invoice-import/key->handler)) (fn [h] (-> h (wrap-copy-qp-pqp) diff --git a/src/cljc/auto_ap/routes/invoice.cljc b/src/cljc/auto_ap/routes/invoice.cljc index b1b0d6d2..106f11d8 100644 --- a/src/cljc/auto_ap/routes/invoice.cljc +++ b/src/cljc/auto_ap/routes/invoice.cljc @@ -2,7 +2,8 @@ (def routes {"" {:get ::all-page "/unpaid" ::unpaid-page "/paid" ::paid-page - "/voided" ::voided-page} + "/voided" ::voided-page + "/import" ::import-page} "/new" {:get ::new-wizard :post ::new-invoice-submit :put ::new-invoice-submit 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 eac2a8fe..88100885 100644 --- a/src/cljs/auto_ap/views/components/expense_accounts_field.cljs +++ b/src/cljs/auto_ap/views/components/expense_accounts_field.cljs @@ -74,6 +74,7 @@ + (defn expense-accounts-field-v2 [{value :value on-change :on-change allowance :allowance expense-accounts :value client :client max-value :max locations :locations disabled :disabled percentage-only? :percentage-only? :or {percentage-only? false} vendor-id :vendor-id}] [form-builder/virtual-builder {:value value :schema schema