284 lines
13 KiB
Clojure
284 lines
13 KiB
Clojure
(ns auto-ap.ssr.ledger.balance-sheet
|
|
(:require
|
|
[amazonica.aws.s3 :as s3]
|
|
[auto-ap.datomic
|
|
:refer [conn pull-many]]
|
|
[auto-ap.graphql.utils :refer [assert-can-see-client]]
|
|
[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.routes.ledger :as route]
|
|
[auto-ap.routes.utils
|
|
:refer [wrap-client-redirect-unauthenticated]]
|
|
[auto-ap.ssr-routes :as ssr-routes]
|
|
[auto-ap.ssr.components :as com]
|
|
[auto-ap.ssr.form-cursor :as fc]
|
|
[auto-ap.ssr.hx :as hx]
|
|
[auto-ap.ssr.ledger.report-table :as rtable]
|
|
[auto-ap.ssr.svg :as svg]
|
|
[auto-ap.ssr.ui :refer [base-page]]
|
|
[auto-ap.ssr.utils
|
|
:refer [apply-middleware-to-all-handlers clj-date-schema
|
|
html-response modal-response wrap-form-4xx-2
|
|
wrap-schema-enforce]]
|
|
[auto-ap.time :as atime]
|
|
[bidi.bidi :as bidi]
|
|
[clj-pdf.core :as pdf]
|
|
[clj-time.coerce :as coerce]
|
|
[clj-time.core :as time]
|
|
[clojure.java.io :as io]
|
|
[clojure.string :as str]
|
|
[config.core :refer [env] :as env]
|
|
[datomic.api :as dc]
|
|
[iol-ion.utils :refer [by]]
|
|
[malli.core :as mc])
|
|
(:import
|
|
[java.util UUID]
|
|
[org.apache.commons.io.output ByteArrayOutputStream]))
|
|
|
|
|
|
|
|
|
|
|
|
(def query-schema (mc/schema
|
|
[:maybe [:map
|
|
[:client {:unspecified/value :all}
|
|
[:or
|
|
[:enum :all]
|
|
[:vector {:coerce? true :min 1}
|
|
[:entity-map {:pull [:db/id :client/name]}]]]]
|
|
[:include-deltas {:default false}
|
|
[:boolean {:decode/string {:enter #(if (= % "on") true
|
|
|
|
(boolean %))}}]]
|
|
[:date {:unspecified/fn (fn [] [(atime/local-now)])}
|
|
[:vector {:coerce? true
|
|
:decode/string (fn [s] (if (string? s) (str/split s #", ")
|
|
s))}
|
|
clj-date-schema]] ]]))
|
|
;; TODO
|
|
;; 1. Rerender form when running
|
|
;; 2. Don't throw crazy errors when missing a field
|
|
;; 3. General cleanup of the patterns in run-balance-sheet
|
|
;; 4. Review ledger dialog
|
|
|
|
(defn get-report [{ {:keys [date client] :as qp} :query-params :as request}]
|
|
(when (and date client)
|
|
(let [client (if (= :all client) (take 5 (:clients request)) client)
|
|
date (reverse (sort date ))
|
|
client-ids (map :db/id client)
|
|
_ (doseq [client-id client-ids]
|
|
(assert-can-see-client (:identity request) client-id))
|
|
|
|
_ (upsert-running-balance (into #{} client-ids))
|
|
|
|
lookup-account (->> client-ids
|
|
(map (fn build-lookup [client-id]
|
|
[client-id (build-account-lookup client-id)]))
|
|
(into {}))
|
|
data (into []
|
|
(for [client-id client-ids
|
|
d date
|
|
[client-id account-id location debits credits balance count] (iol-ion.query/detailed-account-snapshot (dc/db conn) client-id (coerce/to-date d))
|
|
:let [account ((or (lookup-account client-id) {}) account-id)]]
|
|
{:client-id client-id
|
|
:account-id account-id
|
|
:location location
|
|
:debits debits
|
|
:credits credits
|
|
:count count
|
|
:amount balance
|
|
:account-type (:account_type account)
|
|
:numeric-code (:numeric_code account)
|
|
:name (:name account)
|
|
:period (coerce/to-date d)}))
|
|
args (assoc (:query-params request)
|
|
:periods (map coerce/to-date (filter identity date)))
|
|
clients (pull-many (dc/db conn) [:client/code :client/name :db/id] client-ids)
|
|
|
|
pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients))
|
|
report (l-reports/summarize-balance-sheet pnl-data) ]
|
|
(alog/info ::balance-sheet :params args)
|
|
{:data report
|
|
:report report})))
|
|
|
|
(defn maybe-trim-clients [request client ]
|
|
(if (= :all client)
|
|
(cond-> {:client (take 20 (:clients request))}
|
|
(> (count (:clients request)) 20)
|
|
(assoc :warning "You requested a report with more than 20 clients. This report will only contain the first 20."))
|
|
{:client client}))
|
|
|
|
(defn balance-sheet* [{ {:keys [date client] } :query-params :as request}]
|
|
[:div#report
|
|
(when (and date client)
|
|
(let [{:keys [client warning]} (maybe-trim-clients request client)
|
|
{:keys [data report]} (get-report (assoc-in request [:query-params :client] client))
|
|
client-count (count (set (map :client-id (:data data)))) ]
|
|
(list
|
|
[:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client))) ]
|
|
(rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count))
|
|
(> (count date) 1) (into (repeat 13 (* 2 client-count (dec (count date))))))
|
|
:investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate)
|
|
:table report
|
|
:warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))} ))))])
|
|
|
|
(defn form* [request & children]
|
|
(let [params (or (:query-params request) {})]
|
|
(fc/start-form
|
|
params
|
|
(:form-errors request)
|
|
[:div#balance-sheet-form.flex.flex-col.gap-4.mt-4
|
|
[:div.flex.gap-8
|
|
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/run-balance-sheet)
|
|
:hx-target "#balance-sheet-form"
|
|
:hx-swap "outerHTML"
|
|
:hx-disabled-elt "find fieldset"}
|
|
[:fieldset
|
|
[:div.flex.gap-8 {:x-data (hx/json {})}
|
|
(fc/with-field :client
|
|
(com/validated-inline-field
|
|
{:label "Customers" :errors (fc/field-errors)}
|
|
(com/multi-typeahead {:name (fc/field-name)
|
|
:placeholder "Search for companies..."
|
|
:class "w-64"
|
|
:id "client"
|
|
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
|
:value (fc/field-value)
|
|
:value-fn :db/id
|
|
:content-fn :client/name})))
|
|
(fc/with-field :date
|
|
(com/validated-inline-field {:label "Date"
|
|
:errors (fc/field-errors)}
|
|
(com/dates-dropdown {:value (fc/field-value)
|
|
:name (fc/field-name)})))
|
|
(fc/with-field :include-deltas
|
|
(com/toggle {:name (fc/field-name)
|
|
:checked (fc/field-value)}
|
|
"Include Deltas"))
|
|
(com/button {:color :primary :class "w-32"}
|
|
"Run")
|
|
(com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-balance-sheet) } "Export PDF")]]] ]
|
|
children])))
|
|
|
|
(defn form [request]
|
|
(html-response (form* request
|
|
(balance-sheet* request))
|
|
:headers {"hx-retarget" "#balance-sheet-form"
|
|
"hx-push-url" (str "?" (:query-string request))}))
|
|
|
|
(defn balance-sheet [request]
|
|
(base-page
|
|
request
|
|
(com/page {:nav com/main-aside-nav
|
|
|
|
:client-selection (:client-selection request)
|
|
:clients (:clients request)
|
|
:client (:client request)
|
|
:identity (:identity request)
|
|
:request request}
|
|
(apply com/breadcrumbs {} [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
|
"Ledger"]])
|
|
(form* request))
|
|
"Balance Sheet"))
|
|
|
|
(defn make-balance-sheet-pdf [request report]
|
|
|
|
(let [output-stream (ByteArrayOutputStream.)
|
|
client-count (count (or (seq (:client (:query-params request)))
|
|
(seq (:client (:form-params request)))))
|
|
date (:date (:query-params request)) ]
|
|
(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 (or (seq (:client (:query-params request)))
|
|
(seq (:client (:form-params request)))))))]]
|
|
|
|
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
|
|
(conj
|
|
(table->pdf report
|
|
(cond-> (into [30 ] (repeat client-count 13))
|
|
(> (count date) 1) (into (repeat (* 2 client-count (dec (count date))) 13 ))))))
|
|
output-stream)
|
|
(.toByteArray output-stream)))
|
|
|
|
(defn join-names [client-ids]
|
|
(str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_" ))
|
|
|
|
(defn balance-sheet-args->name [request]
|
|
(let [date (atime/unparse-local
|
|
(: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)))
|
|
|
|
(defn print-balance-sheet [request]
|
|
(let [uuid (str (UUID/randomUUID))
|
|
{:keys [client warning]} (maybe-trim-clients request (:client (:query-params request)))
|
|
request (assoc-in request [:query-params :client] client)
|
|
pdf-data (make-balance-sheet-pdf request (:report (get-report request)))
|
|
name (balance-sheet-args->name request)
|
|
key (str "reports/balance-sheet/" uuid "/" name ".pdf")
|
|
url (str "https://" (:data-bucket env) "/" key)]
|
|
(s3/put-object :bucket-name (:data-bucket env/env)
|
|
:key key
|
|
:input-stream (io/make-input-stream pdf-data {})
|
|
:metadata {:content-length (count pdf-data)
|
|
:content-type "application/pdf"})
|
|
@(dc/transact conn
|
|
[{:report/name name
|
|
:report/client (map :db/id client)
|
|
:report/key key
|
|
:report/url url
|
|
:report/creator (:user (:identity request))
|
|
:report/created (java.util.Date.)}])
|
|
{:report/name name
|
|
:report/url url}))
|
|
|
|
;; TODO PRINT WARNING
|
|
(defn export [request]
|
|
(modal-response
|
|
(com/modal {}
|
|
(com/modal-card
|
|
{}
|
|
"Ready!"
|
|
(com/modal-body {}
|
|
(let [bs (print-balance-sheet request)]
|
|
[:div.flex.flex-col.mt-4.space-y-4.items-center
|
|
[:a {:href (:report/url bs)}
|
|
[:div.w-24.h-24.bg-green-50.rounded-full.p-4.text-green-300 {:class " hover:scale-110 transition duration-100"}
|
|
|
|
svg/download]]
|
|
[:span.text-gray-800
|
|
"Click "
|
|
(com/link {:href (:report/url bs)} "here")
|
|
" to download"]
|
|
]))
|
|
nil))
|
|
:headers (-> {}
|
|
(assoc "hx-retarget" ".modal-stack")
|
|
(assoc "hx-reswap" "beforeend"))))
|
|
|
|
(def key->handler
|
|
(apply-middleware-to-all-handlers
|
|
(->
|
|
{::route/balance-sheet (-> balance-sheet
|
|
(wrap-schema-enforce :query-schema query-schema)
|
|
(wrap-form-4xx-2 balance-sheet))
|
|
::route/run-balance-sheet (-> form
|
|
(wrap-schema-enforce :query-schema query-schema)
|
|
(wrap-form-4xx-2 form))
|
|
::route/export-balance-sheet (-> export
|
|
(wrap-schema-enforce :query-schema query-schema)
|
|
(wrap-form-4xx-2 form))}
|
|
)
|
|
(fn [h]
|
|
(-> h
|
|
#_(wrap-merge-prior-hx)
|
|
(wrap-must {:activity :read :subject :balance-sheet})
|
|
(wrap-client-redirect-unauthenticated)))))
|