(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)))))