(ns auto-ap.pdf.ledger (:require [amazonica.aws.s3 :as s3] [auto-ap.datomic :refer [conn pull-attr pull-many]] [auto-ap.graphql.utils :refer [<-graphql]] [auto-ap.ledger.reports :as l-reports] [auto-ap.time :as atime] [auto-ap.utils :refer [by dollars-0?]] [clj-pdf.core :as pdf] [clojure.java.io :as io] [clojure.string :as str] [auto-ap.logging :as alog] [config.core :refer [env]] [datomic.api :as dc]) (:import (java.io ByteArrayOutputStream) (java.text DecimalFormat) (java.util UUID))) (def ^:dynamic *report-pedantic* false) (defn cell->pdf [cell] (let [cell-contents (cond (and (= :dollar (:format cell)) (or (nil? (:value cell)) (dollars-0? (:value cell)))) "-" (= :dollar (:format cell)) (.format (DecimalFormat. "$###,##0.00") (:value cell)) (= :percent (:format cell)) (.format (DecimalFormat. (if *report-pedantic* "0.00%" "0%")) (:value cell)) :else (str (:value cell)))] [:pdf-cell (cond-> {} (:border cell) (assoc :border true :set-border (:border cell)) (:colspan cell) (assoc :colspan (:colspan cell)) (:align cell) (assoc :align (:align cell)) (= :dollar (:format cell)) (assoc :align :right) (= :percent (:format cell)) (assoc :align :right) (:bold cell) (assoc-in [:ttf-name] "fonts/calibri-bold.ttf") (:color cell) (assoc :color (:color cell)) (:bg-color cell) (assoc :background-color (:bg-color cell))) cell-contents ])) (defn cell-count [table] (let [counts (map count (:rows table))] (if (seq counts) (apply max counts) 0))) (defn table->pdf [table widths] (let [cell-count (cell-count table)] (-> [:pdf-table {:header (mapv (fn [header] (map cell->pdf header)) (:header table)) :cell-border false :width-percent (cond (<= cell-count 5) 50 (= 6 cell-count) 60 (= 7 cell-count) 70 (= 8 cell-count) 80 :else 100)} widths ] (into (for [row (:rows table)] (into [] (for [cell (take cell-count (concat row (repeat nil)))] (cell->pdf cell) )))) (conj (take cell-count (repeat (cell->pdf {:value " "}))))))) (defn split-table [table n] (let [cell-count (cell-count table)] (if (<= cell-count n) [table] (let [new-table (-> table (update :rows (fn [rows] (map (fn [[header & rest]] (into [header] (take (dec n) rest))) rows))) (update :header (fn [headers] (map (fn [[title & header]] (first (reduce (fn [[so-far a] next] (let [new-a (+ a (or (:colspan next) 1))] (if (<= new-a n) [(conj so-far next) new-a] [so-far new-a]))) [[title] 1] header))) headers)))) remaining (-> table (update :rows (fn [rows] (map (fn [[header & rest]] (into [header] (drop (dec n) rest))) rows))) (update :header (fn [headers] (map (fn [[title & header]] (first (reduce (fn [[so-far a] next] (let [new-a (+ a (or (:colspan next) 1))] (if (> new-a n) [(conj so-far next) new-a] [so-far new-a]))) [[title] 1] header))) headers))))] (into [new-table] (split-table remaining n)))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn break-apart-tables [pnl-data tables] (for [table tables table (split-table table (if (:include-deltas (:args pnl-data)) 10 9))] table)) (defn make-balance-sheet [args data] (let [data (<-graphql data) args (<-graphql args) args (assoc args :periods (filter identity (cond-> [(:date args)] (:include-comparison args) (conj (:comparison-date args))))) clients (pull-many (dc/db conn) [:client/code :client/name :db/id] (:client-ids args)) data (concat (->> (:balance-sheet-accounts data) (map (fn [b] (assoc b :period (:date args))))) (->> (:comparable-balance-sheet-accounts data) (map (fn [b] (assoc b :period (:comparison-date args)))))) pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) client-count (count (set (map :client-id (:data pnl-data)))) report (l-reports/summarize-balance-sheet pnl-data) output-stream (ByteArrayOutputStream.)] (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 clients)))]] (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) (conj (table->pdf report (cond-> (into [30 ] (repeat client-count 13)) (:include-comparison args) (into (repeat (* 2 client-count) 13)) (and (> client-count 1) (not (:include-comparison args))) (conj 13))))) output-stream) (.toByteArray output-stream))) (defn make-pnl [args data] (let [data (<-graphql data) args (<-graphql args) clients (pull-many (dc/db conn) [:client/code :client/name :db/id] (:client-ids args)) data (->> data :periods (mapcat (fn [p1 p2] (map (fn [a] (assoc a :period p1) ) (:accounts p2)) ) (:periods args))) pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) report (l-reports/summarize-pnl pnl-data) output-stream (ByteArrayOutputStream.)] (pdf/pdf (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15 :size (cond (and (>= (count (-> pnl-data :args :periods)) 8 ) (-> pnl-data :args :include-deltas)) :a2 (>= (count (-> pnl-data :args :periods)) 4 ) :tabloid :else :letter) :orientation :landscape :font {:size 6 :ttf-name "fonts/calibri-light.ttf"}} [:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))]] (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 (-> pnl-data :args :include-deltas) [13 6 13] [13 6]))))))))) output-stream) (.toByteArray output-stream))) (defn make-cash-flows [args data] (let [data (<-graphql data) args (<-graphql args) clients (pull-many (dc/db conn) '[:client/code :client/name :db/id] (:client-ids args)) data (->> data :periods (mapcat (fn [p1 p2] (map (fn [a] (assoc a :period p1) ) (:accounts p2)) ) (:periods args))) pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) report (l-reports/summarize-cash-flows pnl-data) output-stream (ByteArrayOutputStream.)] (pdf/pdf (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15 :size (cond (and (>= (count (-> pnl-data :args :periods)) 8 ) (-> pnl-data :args :include-deltas)) :a2 (>= (count (-> pnl-data :args :periods)) 4 ) :tabloid :else :letter) :orientation :landscape :font {:size 6 :ttf-name "fonts/calibri-light.ttf"}} [:heading (str "Statement of Cash Flows - " (str/join ", " (map :client/name clients)))]] (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 (-> pnl-data :args :include-deltas) [13 6 13] [13 6]))))))))) output-stream) (.toByteArray output-stream))) (defn make-journal-detail-report [args data] (let [data (<-graphql data) args (<-graphql args) clients (pull-many (dc/db conn) '[:client/code :client/name :db/id] (:client-ids args)) report (l-reports/journal-detail-report args data (by :db/id :client/code clients)) output-stream (ByteArrayOutputStream.)] (alog/info ::make-detail-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 "Journal Detail Report - " (str/join ", " (map :client/name clients)))]] (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) (conj (table->pdf report [80 25 80 25 25 25]))) 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 pnl-args->name [args] (let [min-date (atime/unparse-local (->> args :periods (map :start) first) atime/iso-date) max-date (atime/unparse-local (->> args :periods (map :end) last) atime/iso-date) names (->> args :client_ids join-names)] (format "Profit-and-loss-%s-to-%s-for-%s" min-date max-date names))) (defn cash-flows-args->name [args] (let [min-date (atime/unparse-local (->> args :periods (map :start) first) atime/iso-date) max-date (atime/unparse-local (->> args :periods (map :end) last) atime/iso-date) names (->> args :client_ids join-names)] (format "Cash-flows-%s-to-%s-for-%s" min-date max-date names))) (defn journal-detail-args->name [args] (let [min-date (atime/unparse-local (->> args :date_range :start) atime/iso-date) max-date (atime/unparse-local (->> args :date_range :end) atime/iso-date) names (->> args :client_ids join-names)] (format "Profit-and-loss-%s-to-%s-for-%s" min-date max-date names))) (defn balance-sheet-args->name [args] (let [date (atime/unparse-local (:date args) atime/iso-date) name (->> args :client_ids join-names)] (format "Balance-sheet-%s-for-%s" date name))) (defn print-pnl [user args data] (let [uuid (str (UUID/randomUUID)) pdf-data (make-pnl args data) name (pnl-args->name args) key (str "reports/pnl/" uuid "/" name ".pdf") url (str "https://" (:data-bucket env) "/" key)] (s3/put-object :bucket-name (:data-bucket 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 (:client_ids args) :report/key key :report/url url :report/creator (:user user) :report/created (java.util.Date.)}]) {:report/name name :report/url url })) (defn print-cash-flows [user args data] (let [uuid (str (UUID/randomUUID)) pdf-data (make-cash-flows args data) name (cash-flows-args->name args) key (str "reports/cash-flows/" uuid "/" name ".pdf") url (str "https://" (:data-bucket env) "/" key)] (s3/put-object :bucket-name (:data-bucket 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 (:client_ids args) :report/key key :report/url url :report/creator (:user user) :report/created (java.util.Date.)}]) {:report/name name :report/url url })) (defn print-balance-sheet [user args data] (let [uuid (str (UUID/randomUUID)) pdf-data (make-balance-sheet args data) name (balance-sheet-args->name args) key (str "reports/balance-sheet/" uuid "/" name ".pdf") url (str "https://" (:data-bucket env) "/" key)] (s3/put-object :bucket-name (:data-bucket 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 (:client_ids args) :report/key key :report/url url :report/creator (:user user) :report/created (java.util.Date.)}]) {:report/name name :report/url url })) (defn print-journal-detail-report [user args data] (let [uuid (str (UUID/randomUUID)) pdf-data (make-journal-detail-report args data) name (journal-detail-args->name args) key (str "reports/journal-detail/" uuid "/" name ".pdf") url (str "https://" (:data-bucket env) "/" key)] (s3/put-object :bucket-name (:data-bucket 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 (:client_ids args) :report/key key :report/url url :report/creator (:user user) :report/created (java.util.Date.)}]) {:report/name name :report/url url }))