diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index 50406028..a248ea73 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -75,8 +75,8 @@ (assoc :per-page Integer/MAX_VALUE) (d-transactions/raw-graphql-ids ) :ids) - specific-ids (d-transactions/filter-ids (:ids args))] - (if (:ids args) + specific-ids (d-transactions/filter-ids (seq (:ids args)))] + (if (seq (:ids args)) (set specific-ids) (set ids)))) @@ -190,10 +190,12 @@ (audit-transact-batch (map (fn [t] (let [locations (client->locations (-> t :transaction/client :db/id))] - [:upsert-transaction (cond-> t - (:approval_status args) (assoc :transaction/approval-status (enum->keyword (:approval_status args) "transaction-approval-status")) - (:vendor args) (assoc :transaction/vendor (:vendor args)) - (seq (:accounts args)) (assoc :transaction/accounts (maybe-code-accounts t (:accounts args) locations)))])) + (doto + [:upsert-transaction (cond-> t + (:approval_status args) (assoc :transaction/approval-status (enum->keyword (:approval_status args) "transaction-approval-status")) + (:vendor args) (assoc :transaction/vendor (:vendor args)) + (seq (:accounts args)) (assoc :transaction/accounts (maybe-code-accounts t (:accounts args) locations)))] + clojure.pprint/pprint))) transactions) (:id context)) {:message (str "Successfully coded " (count all-ids) " transactions.")})) diff --git a/src/clj/auto_ap/parse/templates.clj b/src/clj/auto_ap/parse/templates.clj index 51fef522..7b03207d 100644 --- a/src/clj/auto_ap/parse/templates.clj +++ b/src/clj/auto_ap/parse/templates.clj @@ -1,7 +1,9 @@ (ns auto-ap.parse.templates (:require [auto-ap.parse.util :as u] [auto-ap.logging :as alog] - [clojure.string :as str])) + [clj-time.core :as time] + [clojure.string :as str] + [auto-ap.time :as atime])) (def pdf-templates @@ -13,7 +15,7 @@ :date #"\s+([0-9]+/[0-9]+/[0-9]+)" :invoice-number #"\s+[0-9]+/[0-9]+/[0-9]+\s+([0-9]+)"} :parser {:date [:clj-time "MM/dd/yyyy"]}} - + {:vendor "Gstar Seafood" :keywords [#"G Star Seafood"] :extract {:total #"Total\s{2,}([\d\-,]+\.\d{2,2}+)" @@ -42,10 +44,10 @@ :total #"INVOICE TOTAL\s+([0-9.]+)"} :parser {:date [:clj-time "MM/dd/yy"]} :multi #"\f\f"} - - + + ;; IMPACT PAPER -{:vendor "Impact Paper & Ink LTD" + {:vendor "Impact Paper & Ink LTD" :keywords [#"650-692-5598"] :extract {:total #"Total Amount\s+\$([\d\.\,\-]+)" :account-number #"CUST. #\n.*?/\d{4,}\s+(.*?)\n" @@ -94,7 +96,7 @@ :total #"Total Invoice\s+([\-]?[0-9.]+)"} :parser {:date [:clj-time "MM/dd/yy"] :total [:trim-commas-and-negate nil]}} - + {:vendor "Ben E. Keith" :keywords [#"BEN E. KEITH"] :extract {:date #"Customer No Mo Day Yr.*?\n.*?\d{5,}\s{2,}(\d+\s+\d+\s+\d+)" @@ -104,7 +106,7 @@ :parser {:date [:month-day-year nil] :total [:trim-commas-and-negate nil]}} -;; SOUTHBAY FRESH + ;; SOUTHBAY FRESH {:vendor "Southbay Fresh Produce" :keywords [#"(SOUTH BAY FRESH PRODUCE|SOUTH BAY PRODUCE)"] :extract {:date #"^([0-9]+/[0-9]+/[0-9]+)" @@ -115,25 +117,25 @@ :multi #"\n" :multi-match? #"^[0-9]+/[0-9]+/[0-9]+\s+INV "} -;; DON VITO + ;; DON VITO {:vendor "Don Vito Ozuna Food Corp" :keywords [#"408-465-2010"] :extract {:date #"([0-9]+/[0-9]+/[0-9]+)" :customer-identifier #"Bill To.*?\n(.*?)\s{2,}" :invoice-number #"(?:[0-9]+/[0-9]+/[0-9]+)\s{2,}(\d+)" :total #"Please remit payment to\s{2,}\$([\-0-9.]+)"} - :parser {:date [:clj-time "MM/dd/yyyy"]} } + :parser {:date [:clj-time "MM/dd/yyyy"]}} ;; DON VITO STATEMENT -{:vendor "Don Vito Ozuna Food Corp" + {:vendor "Don Vito Ozuna Food Corp" :keywords [#"Don Vito Ozuna Food Corp.*?\n.*?Statement"] :extract {:date #"([0-9]+/[0-9]+/[0-9]+)" :customer-identifier #"To:.*?\n\s*(.*)?\s{2,}" :invoice-number #"INV #(\d+)" :total #"Amount \$([\d\-\.]+?)\.\s{2,}"} :parser {:date [:clj-time "MM/dd/yyyy"]} - :multi #"\n" - :multi-match? #"\d+/\d+/\d+.*?INV"} + :multi #"\n" + :multi-match? #"\d+/\d+/\d+.*?INV"} ;; PFG - LEDYARD {:vendor "Performance Food Group - LEDYARD" @@ -221,8 +223,8 @@ :total [:trim-commas nil]} :multi (. java.util.regex.Pattern (compile (-> \formfeed str) java.util.regex.Pattern/CASE_INSENSITIVE)) :multi-match? #"(Total\s+[0-9\.]+|Total Order)"} - -;; AUTO-CHLOR + + ;; AUTO-CHLOR {:vendor "Auto-Chlor" :keywords [#"AUTO-CHLOR"] :extract {:date #"DATE : ([0-9]+/[0-9]+/[0-9]+)" @@ -254,7 +256,7 @@ :multi #"\n" :multi-match? #"^\s+.*?\d{6,}.*?\$"} -;; C & L + ;; C & L {:vendor "C&L Produce" :keywords [#"440 Franklin Street"] :extract {:date #"([0-9]+/[0-9]+/[0-9]+)" @@ -368,7 +370,7 @@ :total [:trim-commas nil]}} -;; Breakthru Bev + ;; Breakthru Bev {:vendor "Wine Warehouse" :keywords [#"BREAKTHRU BEVERAGE"] :extract {:date #"Invoice Date:\s+([0-9]+/[0-9]+/[0-9]+)" @@ -377,7 +379,7 @@ :account-number #"Customer #:\s+(\d+)"} :parser {:date [:clj-time "MM/dd/yyyy"] :total [:trim-commas nil]}} - + ;; THE WATER PROS {:vendor "The Water Pros" :keywords [#"The Water Pros, Inc"] @@ -418,7 +420,7 @@ :parser {:date [:clj-time "MM/dd/yyyy"] :total [:trim-commas nil]}} -;; PACIFIC SEAFOOD + ;; PACIFIC SEAFOOD {:vendor "Pacific Seafood" :keywords [#"(pacseafood|PACIFIC FRESH)"] :extract {:date #"DATE(?:.*\n.*(?=([0-9]+/[0-9]+/[0-9]+)))([0-9]+/[0-9]+/[0-9]+)" @@ -490,7 +492,7 @@ :parser {:date [:clj-time "MM/dd/yyyy"] :total [:trim-commas-and-negate nil]}} -;; A&B + ;; A&B {:vendor "A&B Produce" :keywords [#"ABProduce"] :extract {:date #"^\s+([0-9]+/[0-9]+/[0-9]+)" @@ -623,7 +625,7 @@ :parser {:date [:clj-time "MM/dd/yy"] :total [:trim-commas nil]}} -;; JFC + ;; JFC {:vendor "JFC International" :keywords [#"48490 MILMONT DRIVE"] :extract {:date #"([0-9]+/[0-9]+/[0-9]+)" @@ -723,7 +725,7 @@ :total #"Invoice Total:\s+([\d\-,]+\.\d{2,2}+)"} :parser {:date [:clj-time "MM/dd/yyyy"] :total [:trim-commas-and-negate nil]}} - + {:vendor "Mani Imports" :keywords [#"Mani Imports"] :extract {:date #"Order Date\s+([0-9]+/[0-9]+/[0-9]+)" @@ -731,7 +733,7 @@ :invoice-number #"Invoice Number:\s+(.*?)\n" :total #"Invoice Total:\s+([\d\-,]+\.\d{2,2}+)"} :parser {:date [:clj-time "MM/dd/yyyy"] - :total [:trim-commas-and-negate nil]} } + :total [:trim-commas-and-negate nil]}} {:vendor "Reel Produce" :keywords [#"REEL Produce, Inc" #"Statem"] :extract {:date #"\s*([0-9]+/[0-9]+/[0-9]+)" @@ -789,15 +791,36 @@ (and (seq r) (->> r first not-empty)))) + (take 4) (map (fn [[customer-number _ _ _ invoice-number date amount :as row]] + (println "DAT E is" date) {:customer-identifier customer-number :text (str/join " " row) :full-text (str/join " " row) - :date (u/parse-value :clj-time "MM/dd/yyyy" (str/trim date)) + :date (try (or (u/parse-value :clj-time "MM/dd/yyyy" (str/trim date)) + (try + (atime/as-local-time + (time/plus (time/date-time 1900 1 1) + (time/days (dec (dec (Integer/parseInt "45663")))))) + (catch Exception e + nil) + )) + + (catch Exception e + (try + (atime/as-local-time + (time/plus (time/date-time 1900 1 1) + (time/days (dec (dec (Integer/parseInt "45663")))))) + (catch Exception e + nil) + ) + )) :invoice-number invoice-number :total (str amount) :vendor-code vendor}))) conj [] sheet)))}]) + + diff --git a/src/clj/auto_ap/square/core3.clj b/src/clj/auto_ap/square/core3.clj index 48ff20b7..a0df0672 100644 --- a/src/clj/auto_ap/square/core3.clj +++ b/src/clj/auto_ap/square/core3.clj @@ -388,7 +388,7 @@ (defn daily-results ([client location] - (daily-results client location (time/plus (time/now) (time/days -30)) (time/now))) + (daily-results client location (time/plus (time/now) (time/days -10)) (time/now))) ([client location start end] (capture-context->lc (-> @@ -458,7 +458,7 @@ (:payout_entries result))))))) (defn payouts - ([client location] (payouts client location (time/plus (time/now) (time/days -30)) (time/now))) + ([client location] (payouts client location (time/plus (time/now) (time/days -10)) (time/now))) ([client location start end] (with-context-as {:location (:square-location/client-location location)} lc (de/chain (manifold-api-call @@ -599,7 +599,7 @@ (apply de/zip (for [square-location (:client/square-locations client) :when (:square-location/client-location square-location)] - (upsert client square-location (time/plus (time/now) (time/days -30)) (time/now))))) + (upsert client square-location (time/plus (time/now) (time/days -10)) (time/now))))) ([client location start end] (capture-context->lc (de/chain (daily-results client location start end) @@ -618,7 +618,7 @@ :when (:square-location/client-location square-location)] (upsert-payouts client square-location)))) ([client location] - (upsert-payouts client location (time/plus (time/now) (time/days -30)) (time/now))) + (upsert-payouts client location (time/plus (time/now) (time/days -10)) (time/now))) ([client location start end] (with-context-as {:source "Square payout loading" :client (:client/code client)} lc @@ -825,7 +825,7 @@ (apply de/zip (for [square-location (:client/square-locations client) :when (:square-location/client-location square-location)] - (remove-voided-orders client square-location (time/plus (time/now) (time/days -30)) (time/now))))) + (remove-voided-orders client square-location (time/plus (time/now) (time/days -10)) (time/now))))) ([client location start end] (let [start (max-date start (coerce/to-date-time #inst "2024-04-15T00:00:00-08:00"))] (capture-context->lc diff --git a/src/clj/auto_ap/ssr/company.clj.bak b/src/clj/auto_ap/ssr/company.clj.bak new file mode 100644 index 00000000..232cf7fa --- /dev/null +++ b/src/clj/auto_ap/ssr/company.clj.bak @@ -0,0 +1,319 @@ +(ns auto-ap.ssr.company + (:require [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [conn pull-attr]] + [auto-ap.datomic.clients :refer [full-read]] + [auto-ap.graphql.utils :refer [assert-can-see-client]] + [auto-ap.permissions :as permissions] + [auto-ap.solr :as solr] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.ui :refer [base-page]] + [auto-ap.ssr.utils :refer [html-response]] + [bidi.bidi :as bidi] + [cemerick.url :as url] + [clojure.java.io :as io] + [clojure.string :as str] + [config.core :refer [env]] + [datomic.api :as dc] + [ring.middleware.json :refer [wrap-json-response]]) + (:import [java.util UUID] + (org.apache.commons.codec.binary Base64))) + +(defn please-select-client-screen* [] + [:div.grid.grid-cols-3 + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + "Please select a company"]])]) + +(defn signature [request] + (let [signature-file (pull-attr (dc/db conn) :client/signature-file (:db/id (:client request)))] + (com/content-card {:class " w-[748px]" + :hx-target "this" + :hx-swap "outerHTML"} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6 space-y-4 overflow-visible " + } + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + "Signature"] + [:div#signature-notification.notification.block {:style {:display "none"}}] + [:div {:x-data (hx/json {"signature" nil + "editing" false + "existing" (boolean signature-file)}) + :hx-put (bidi/path-for ssr-routes/only-routes + :company-update-signature) + :hx-trigger "accepted" + :hx-vals "js:{signatureData: event.detail.signatureData}"} + [:div.htmx-indicator + [:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}} + (svg/spinner {:class "w-4 h-4 text-primary-300"}) + [:div.ml-3 "Loading..."]]] + + [:div.htmx-indicator-hidden + (when signature-file + [:img.rounded.rounded-lg.border.border-gray-300.bg-gray-50 {:src signature-file + :width 696 + :height 261 + :x-show "existing && !editing"}]) + [:canvas.rounded.rounded-lg.border.border-gray-300 + + + {:style {:width 696 + :height 261} + :x-init "signature= new SignaturePad($el); signature.off()" + ":class" "editing ? 'bg-white' : 'bg-gray-50' " + :width 696 + :height 261 + :x-show "existing ? editing: true"}]] + + + [:div.flex.gap-2.justify-end + (com/button {:color :primary + :x-show "!editing" + "@click" "signature.clear(); signature.on(); editing=true;"} + "New signature") + (com/button {:color :primary + :x-show "editing" + "@click" "signature.clear();"} + "Clear") + + (com/button {:color :primary + "@click" "$data.signatureData=signature.toDataURL('image/png'); signature.off(); editing=false; $dispatch('accepted', {signatureData: $data.signatureData}) " + :x-show "editing"} + "Accept")]] + + [:div + [:div.flex.justify-center " - or -"] + [:form {:hx-post (bidi/path-for ssr-routes/only-routes + :company-upload-signature) + :hx-disinherit "hx-vals" + :hx-encoding "multipart/form-data" + #_#_:hx-target "#signature-notification" + :hx-swap "outerHTML" + :id "upload" + :hx-trigger "z" + } +[:div.htmx-indicator + [:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}} + (svg/spinner {:class "w-4 h-4 text-primary-300"}) + [:div.ml-3 "Loading..."]]] +[:div.htmx-indicator-hidden + [:div.border-2.border-dashed.rounded-lg.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-lg.relative + {:x-data (hx/json {"files" nil + "hovering" false}) + :x-dispatch:z "files" + ":class" "{'bg-blue-100': !hovering, + 'border-blue-300': !hovering, + 'text-blue-700': !hovering, + 'bg-green-100': hovering, + 'border-green-300': hovering, + 'text-green-700': hovering + }"} + + + + [:input {:type "file" + :name "file" + :class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0", + :x-on:change "files = $event.target.files;", + :x-on:dragover "hovering = true", + :x-on:dragleave "hovering = false", + :x-on:drop "hovering = false"}] + [:div.flex.flex-col.space-2 + [:div + [:ul {:x-show "files != null"} + [:template {:x-for "f in files"} + [:li (com/pill {:color :primary :x-text "f.name"})]]]] + + + [:div.htmx-indicator-hidden "Drop a signature file (696x261 pixels jpeg) here."]]]] ]]]))) + +(defn upload-signature-data [{{:strs [signatureData]} :form-params client :client :as request}] + (let [prefix "data:image/png;base64,"] + (when signatureData + (when-not (str/starts-with? signatureData prefix) + (throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")}))) + (let [signature-id (str (UUID/randomUUID)) + raw-bytes (Base64/decodeBase64 (subs signatureData (count prefix)))] + (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) + :key (str signature-id ".png") + :input-stream (io/make-input-stream raw-bytes {}) + :metadata {:content-type "image/png" + :content-length (count raw-bytes)} + :canned-acl "public-read") + @(dc/transact conn [{:db/id (:db/id client) + :client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".png")}]) + (html-response + (signature request)))))) + +(defn upload-signature-file [{{:strs [signatureData]} :form-params client :client user :identity :as request}] + (assert-can-see-client user client) + (let [{:strs [file]} (:multipart-params request) ] +(try + (let [signature-id (str (UUID/randomUUID)) ] + (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) + :key (str signature-id ".jpg") + :input-stream (io/input-stream (:tempfile file)) + :metadata {:content-type "image/jpeg" + :content-length (:length (:tempfile file))} + :canned-acl "public-read") + @(dc/transact conn [{:db/id (:db/id client) + :client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")}]) + (html-response + (signature request))) + (catch Exception e + (println e) + #_(-> result + (assoc :error? true) + (update :results conj {:filename filename + :response (.getMessage e) + :sample (:sample (ex-data e)) + :template (:template (ex-data e))})))) + #_(html-response [:div#page-notification.p-4.rounded-lg + {:class (if (:error? results) + "bg-red-50 text-red-700" + "bg-primary-50 text-primary-700")} + [:table + [:thead + [:tr [:td "File"] [:td "Result"] + [:td "Template"] + (if (:error? results) + [:td "Sample match"])] + #_[:tr "Result"] + #_[:tr "Template"]] + (for [r (:results results)] + [:tr + [:td.p-2.border + {:class (if (:error? results) + "bg-red-50 text-red-700 border-red-300" + "bg-primary-50 text-primary-700 border-green-500")} + (:filename r)] + [:td.p-2.border + {:class (if (:error? results) + "bg-red-50 text-red-700 border-red-300" + "bg-primary-50 text-primary-700 border-green-500")} + (:response r)] + [:td.p-2.border + {:class (if (:error? results) + "bg-red-50 text-red-700 border-red-300" + "bg-primary-50 text-primary-700 border-green-500")} + "Template: " (:template r)] + (if (:error? results) + [:td.p-2.border + {:class "bg-red-50 text-red-700 border-red-300"} + + [:ul + (for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)] + [:li (name k) ": " (str v)])] + #_(:template r)])])]] + :headers + {"hx-trigger" "invalidated"}))) + +(defn main-content* [{:keys [client identity] :as request}] + (if-not client + (please-select-client-screen*) + (let [client (dc/pull (dc/db conn) full-read (:db/id client))] + [:div + [:div.grid.grid-cols-3.gap-4 + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + (:client/name client)] + (when-let [address (-> client :client/address)] + [:div.flex.flex-col.gap-1.text-lg.dark:text-white.text-gray-700 + [:p (-> address :address/street1)] + [:p (-> address :address/street2)] + [:p (-> address :address/city) " " + (-> address :address/state) ", " + (-> address :address/zip)]])]) + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + "Downloads"] + [:a {:href (str (assoc (url/url (str (:base-url env) "/api/vendors/company/export")) + :query {"client" (:client/code client)}))} + (com/button {:color :primary} + "Download vendor list" + (com/button-icon {} svg/download))]]) + [:div]] + (when (permissions/can? identity {:client client :subject :signature :activity :edit}) + (signature request))]))) + +(defn page [{:keys [identity matched-route] :as request}] + (base-page + request + (com/page {:nav com/company-aside-nav + :client-selection (:client-selection request) + :request request + :client (:client request) + :clients (:clients request) + :identity (:identity request) + :app-params {:hx-get (bidi/path-for ssr-routes/only-routes + :company) + :hx-trigger "clientSelected from:body" + :hx-select "#app-contents" + :hx-swap "outerHTML swap:300ms"}} + (com/breadcrumbs {} + [:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"]) + (main-content* request)) + "My Company")) + +(defn search [{:keys [clients query-params]}] + (let [valid-client-ids (set (map :db/id clients)) + name-like-ids (when (not-empty (get query-params "q")) + (set (map (comp #(Long/parseLong %) :id) + (solr/query solr/impl "clients" + {"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q")))) + "fields" "id" + "limit" 300})))) + valid-clients (for [n name-like-ids + :when (valid-client-ids n)] + {"value" n "label" (pull-attr (dc/db conn) :client/name n)})] + {:body (take 10 valid-clients)})) + +(def search (wrap-json-response search)) + + +(defn bank-account-search [{:keys [route-params query-params clients]}] + (let [valid-client-ids (set (map :db/id clients)) + selected-client-id (Long/parseLong (get route-params :db/id)) + bank-accounts (when (valid-client-ids selected-client-id) + (->> (dc/pull (dc/db conn) [{:client/bank-accounts [:db/id :bank-account/name]}] + selected-client-id) + :client/bank-accounts + (filter (fn [{:keys [bank-account/name]}] + (str/includes? (or (some-> name str/upper-case) "") + (or (some-> query-params + (get "q") + str/upper-case) + "__")))) + (map (fn [{:keys [db/id bank-account/name]}] + {"value" id "label" name}))))] + {:body (take 10 bank-accounts)})) + +(def bank-account-search (wrap-json-response bank-account-search)) + +(defn bank-account-typeahead* [{:keys [client-id name value]}] + (if client-id + (com/typeahead {:name name + :class "w-96" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :bank-account-search + :db/id client-id) + :value value + :value-fn (some-fn :db/id identity) + :content-fn (some-fn :bank-account/name #(pull-attr (dc/db conn) :bank-account/name %))}) + [:span.text-xs.text-gray-500 "Please select a client before selecting a bank account." + [:input {:type "hidden" + :name name}]])) + +(defn bank-account-typeahead [{:keys [query-params clients]}] + (html-response (bank-account-typeahead* {:client-id ((set (map :db/id clients)) + (some->> "client-id" + (get query-params) + not-empty + Long/parseLong)) + :name (get query-params "name")})))