This commit is contained in:
2026-01-05 21:37:30 -08:00
18 changed files with 787 additions and 315 deletions

View File

@@ -520,7 +520,8 @@
["manually-pay-cintas" "Manually Pay Cintas"]
["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]
["import-custom-amount" "Import Custom Amount Line Items from Square"]
["code-sysco-items" "Code individual sysco line items"]]})))
["code-sysco-items" "Code individual sysco line items"]
["report-pedantic" "Show two decimals in reports"]]})))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))

View File

@@ -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")})))

View File

@@ -1,29 +1,29 @@
(ns auto-ap.ssr.invoice.import
(:require [amazonica.aws.s3 :as s3]
[auto-ap.datomic
(:require
[amazonica.aws.s3 :as s3]
[auto-ap.datomic
: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.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[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
[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
can-see-client? 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.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[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 [apply-middleware-to-all-handlers clj-date-schema
entity-id html-response main-transformer ref->enum-schema
strip wrap-entity wrap-implied-route-param
@@ -656,17 +656,28 @@
: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))))
(let [missing-keys (for [k [:invoice/invoice-number :invoice/client :invoice/vendor :invoice/total :invoice/outstanding-balance :invoice/date]
:when (not (get invoice k))]
k
)]
(cond
(not (:invoice/client invoice))
(do
(alog/warn ::no-client :invoice invoice)
(assoc invoice :error-message (str "Searched clients for '" (:invoice/client-identifier invoice) "'. No client found in file. Select a client first.")))
invoice)
(not (can-see-client? user (:invoice/client invoice)))
(do
(alog/warn ::cant-see-client :invoice invoice )
(assoc invoice :error-message "No access for the client in this file.")
)
(seq missing-keys)
(do
(alog/warn ::mising-keys :keys missing-keys)
(assoc invoice :error-message (str "Missing the key " missing-keys)))
:else
invoice)))
(defn admin-only-if-multiple-clients [is]
(let [client-count (->> is
@@ -683,19 +694,29 @@
(map #(import->invoice % user))
(map #(validate-invoice % user))
admin-only-if-multiple-clients
(mapv d-invoices/code-invoice)
(mapv (fn [i] [:propose-invoice i])))]
)
errored-invoices (->> potential-invoices
(filter #(:error-message %)))
successful-invoices (->> potential-invoices
(filter #(not (:error-message %))))
proposed-invoices (->> potential-invoices
(filter #(not (:error-message %)))
(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
(alog/info ::creating-invoice :invoices proposed-invoices)
(let [tx (audit-transact proposed-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."
{:template (:template (first imports))})))
tx)))
{:tx tx
:errored-invoices errored-invoices
:successful-invoices successful-invoices
:imports imports})))
(defn import-internal [tempfile filename force-client force-location force-vendor force-chatgpt identity]
(mu/with-context {:parsing-file filename}
@@ -711,7 +732,7 @@
:content-length (.length tempfile)})
imports (->> (if force-chatgpt
(parse/glimpse2 (.getPath tempfile))
(parse/parse-file (.getPath tempfile) filename))
(parse/parse-file (.getPath tempfile) filename :allow-glimpse? true))
(map #(assoc %
:client-override force-client
:location-override force-location
@@ -722,6 +743,7 @@
(try
(import-uploaded-invoice identity imports)
(catch Exception e
(alog/warn ::couldnt-import-upload
:error e
@@ -729,8 +751,7 @@
(throw (ex-info (ex-message e)
{:template (:template ( first imports))
:sample (first imports)}
e))))
imports)
e)))))
(catch Exception e
(alog/warn ::couldnt-import-upload
:error e)
@@ -744,61 +765,74 @@
[file])
results (reduce
(fn [result {:keys [filename tempfile]}]
(try
(let [i (import-internal tempfile filename force-client force-location force-vendor force-chatgpt (:identity request) )]
(update result :results conj {:filename filename
:response "success!"
:template (:template (first i))}))
(catch Exception e
(-> result
(assoc :error? true)
(update :results conj {:filename filename
:response (.getMessage e)
:sample (:sample (ex-data e))
:template (:template (ex-data e))})))))
{:error? false :results []}
file)]
(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"}
(try
(let [i (import-internal tempfile filename force-client force-location force-vendor force-chatgpt (:identity request))]
(alog/info ::failure-error-count :count (count (:errored-invoices i)) )
[:ul
(for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)]
[:li (name k) ": " (str v)])]
#_(:template r)])])]]
:headers
{"hx-trigger" "invalidated"})
))
(-> result
(update :error? #(or %
(boolean (seq (:errored-invoices i)))))
(update :files conj {:filename filename
:error? (boolean (seq (:errored-invoices i)))
:successful-invoices (count (:successful-invoices i))
:errors [:div
[:p.text-green-500 [:b (count (:successful-invoices i)) " succeeded in total."]
]
[:p [:b (count (:errored-invoices i)) " failed in total."]
]
[:ul
(for [e (take 5 (:errored-invoices i))]
[:li (:error-message e)]) ]]
:template (:template (first (:imports i)))})))
(catch Exception e
(-> result
(assoc :error? true)
(update :files conj {:filename filename
:errors "Can't process file"
:response (.getMessage e)
:sample (:sample (ex-data e))
:template (:template (ex-data e))})))))
{:error? false
:files []
}
file)]
(html-response [:div#page-notification.p-4.rounded-lg
[:table
[:thead
[:tr [:td "File"] [:td "Result"]
[:td "Template"]
[:td "Sample"]]
#_[:tr "Result"]
#_[:tr "Template"]]
(for [r (:files results)]
[:tr
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
(:filename r)]
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
(:errors r)]
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
"Template: " (:template r)]
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
[: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 wrap-test [handler]
(fn [request]

View File

@@ -1166,7 +1166,7 @@
:name (fc/field-name)
:error? (fc/field-errors)
:placeholder "10001"}))))
(when (= :handwrite-check (:method (:snapshot (:multi-form-state request))))
(when (#{:handwrite-check :print-check} (:method (:snapshot (:multi-form-state request))))
(fc/with-field :handwritten-date
(com/validated-field
{:errors (fc/field-errors)
@@ -1187,6 +1187,7 @@
(format "Pay in full ($%,.2f)" total)))}
{:value "advanced"
:content "Customize payments"}]})
[:div.space-y-4
(fc/with-field :invoices
(com/validated-field
@@ -1245,13 +1246,13 @@
bank-account
:payment-type/check
0
invoice-payment-lookup)]
invoice-payment-lookup
(:handwritten-date snapshot))]
(let [result (audit-transact
(into [(assoc base-payment
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/check-number (:check-number snapshot)
:payment/date (coerce/to-date (:handwritten-date snapshot)))]
:payment/check-number (:check-number snapshot))]
(invoice-payments invoices invoice-payment-lookup))
(:identity request))]
(try
@@ -1345,24 +1346,33 @@
(= "" (:check-number snapshot)))
(throw (Exception. "Check number is required")))
true))
result (exception->4xx
#(if (= :handwrite-check (:method snapshot))
(add-handwritten-check request this snapshot)
(print-checks-internal (map (fn [i] {:invoice-id (:invoice-id i)
:amount (:amount i)})
(:invoices snapshot))
(:client snapshot)
(:bank-account snapshot)
(cond (= :print-check (:method snapshot))
:payment-type/check
(= :debit (:method snapshot))
:payment-type/debit
(= :cash (:method snapshot))
:payment-type/cash
(= :credit (:method snapshot))
:payment-type/credit
:else :payment-type/debit)
identity)))]
#(do
(when (:handwritten-date snapshot)
(let [invoices (d-invoices/get-multi (map :invoice-id (:invoices snapshot)))]
(assert-not-locked (:db/id (:invoice/client (first invoices))) (:handwritten-date snapshot))))
(if (= :handwrite-check (:method snapshot))
(add-handwritten-check request this snapshot)
(try
(print-checks-internal (map (fn [i] {:invoice-id (:invoice-id i)
:amount (:amount i)})
(:invoices snapshot))
(:client snapshot)
(:bank-account snapshot)
(cond (= :print-check (:method snapshot))
:payment-type/check
(= :debit (:method snapshot))
:payment-type/debit
(= :cash (:method snapshot))
:payment-type/cash
(= :credit (:method snapshot))
:payment-type/credit
:else :payment-type/debit)
identity
(:handwritten-date snapshot))
(catch Exception e
(println e))))))]
(modal-response
(com/modal {}
(com/modal-card-advanced

View File

@@ -7,8 +7,8 @@
[auto-ap.ledger :refer [build-account-lookup upsert-running-balance]]
[auto-ap.ledger.reports :as l-reports]
[auto-ap.logging :as alog]
[auto-ap.pdf.ledger :refer [table->pdf]]
[auto-ap.permissions :refer [wrap-must]]
[auto-ap.pdf.ledger :refer [table->pdf *report-pedantic*]]
[auto-ap.permissions :refer [can? wrap-must]]
[auto-ap.routes.ledger :as route]
[auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated]]
@@ -45,7 +45,7 @@
[:or
[:enum :all]
[:vector {:coerce? true :min 1}
[:entity-map {:pull [:db/id :client/name]}]]]]
[:entity-map {:pull [:db/id :client/name :client/feature-flags]}]]]]
[:column-per-location {:default false}
[:boolean {:decode/string {:enter #(if (= % "on") true
@@ -86,7 +86,7 @@
data (into []
(for [client-id client-ids
p periods
[client-id account-id location debits credits balance count] (iol-ion.query/detailed-account-snapshot (dc/db conn) client-id (coerce/to-date (:end p)) (coerce/to-date (:start p)))
[client-id account-id location debits credits balance count sample] (iol-ion.query/detailed-account-snapshot (dc/db conn) client-id (coerce/to-date (:end p)) (coerce/to-date (:start p)))
:let [account ((or (lookup-account client-id) {}) account-id)]]
{:client-id client-id
:account-id account-id
@@ -102,12 +102,16 @@
:account-type (:account_type account)
:numeric-code (:numeric_code account)
:name (:name account)
:period {:start ( coerce/to-date (:start p)) :end (coerce/to-date (time/plus (:end p) (time/days 1)))}}))
:sample sample
:period {:start ( coerce/to-date (:start p)) :end (coerce/to-date (:end p))}}))
args (assoc (:form-params request)
:periods (map (fn [d] {:start ( coerce/to-date (:start d)) :end ( coerce/to-date (:end d))}) periods))
clients (pull-many (dc/db conn) [:client/code :client/name :db/id] client-ids)
:periods (map (fn [d]
{:start ( coerce/to-date (:start d)) :end ( coerce/to-date (:end d))}) periods))
clients (pull-many (dc/db conn) [:client/code :client/name :db/id :client/feature-flags] client-ids)
pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients))
#_#__ (clojure.pprint/pprint pnl-data)
report (l-reports/summarize-pnl pnl-data)]
(alog/info ::profit-and-loss :params args)
{:data report
@@ -130,14 +134,26 @@
(list
[:div.text-2xl.font-bold.text-gray-600 (str "Profit and loss - " (str/join ", " (map :client/name client)))]
(table {:widths (into [20] (take (dec (cell-count table-contents))
(mapcat identity
(repeat
(if (-> data :args :include-deltas)
[13 6 13]
[13 6])))))
(mapcat identity
(repeat
(if (-> data :args :include-deltas)
[13 6 13]
[13 6])))))
:investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate)
:table table-contents
:warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))}))))])
:warning [:div
(not-empty (str (str/join "\n " (filter not-empty [warning (:warning report)]))))
(when (can? (:identity request)
{:subject :history
:activity :view})
(for [n (:invalid-ids report)]
[:div
(com/link {:href (str (bidi/path-for ssr-routes/only-routes
:admin-history)
"/" n)}
"Sample")]))]
}))))])
@@ -205,26 +221,37 @@
"Profit and loss"))
(defn make-profit-and-loss-pdf [request report]
(let [ output-stream (ByteArrayOutputStream.)
(let [output-stream (ByteArrayOutputStream.)
date (:periods (:form-params request))
table (concat-tables (:details report))]
(pdf/pdf
(-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
:size :letter
:font {:size 6
:ttf-name "fonts/calibri-light.ttf"}}
[:heading (str "Balance Sheet - " (str/join ", " (map :client/name (seq (:client (:form-params request))))))]]
(-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
:size :letter
:font {:size 6
:ttf-name "fonts/calibri-light.ttf"}}
[:heading (str "Profit and Loss - " (str/join ", " (map :client/name (seq (:client (:form-params request))))))]]
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
(conj
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
(into
(for [table (concat (:summaries report)
(:details report))]
(table->pdf table
(into [20] (take (dec (cell-count table))
(mapcat identity
(repeat
(if (-> (:form-params request) :include-deltas)
[13 6 13]
[13 6]))))))))
output-stream)
(mapcat identity
(repeat
(if (-> (:form-params request) :include-deltas)
[13 6 13]
[13 6]))))))))
#_(conj
(table->pdf table
(into [20] (take (dec (cell-count table))
(mapcat identity
(repeat
(if (-> (:form-params request) :include-deltas)
[13 6 13]
[13 6]))))))))
output-stream)
(.toByteArray output-stream)))
(defn join-names [client-ids]
@@ -235,13 +262,14 @@
(:date (:query-params request))
atime/iso-date)
name (->> request :query-params :client (map :db/id) join-names)]
(format "Balance-sheet-%s-for-%s" date name)))
(format "Profit-and-loss-%s-for-%s" date name)))
(defn print-profit-and-loss [request]
(let [uuid (str (UUID/randomUUID))
{:keys [client warning]} (maybe-trim-clients request (:client (:form-params request)))
request (assoc-in request [:form-params :client] client)
pdf-data (make-profit-and-loss-pdf request (:report (get-report request)))
pdf-data (binding [*report-pedantic* (boolean ((set (:client/feature-flags (first client)))
"report-pedantic"))] (make-profit-and-loss-pdf request (:report (get-report request))))
name (profit-and-loss-args->name request)
key (str "reports/profit-and-loss/" uuid "/" name ".pdf")
url (str "https://" (:data-bucket env) "/" key)]