221 lines
9.3 KiB
Clojure
221 lines
9.3 KiB
Clojure
(ns auto-ap.pdf.ledger
|
|
(:require
|
|
[amazonica.aws.s3 :as s3]
|
|
[auto-ap.ledger.reports :as l-reports]
|
|
[auto-ap.datomic :refer [conn]]
|
|
[auto-ap.graphql.utils :refer [<-graphql]]
|
|
[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]
|
|
[config.core :refer [env]]
|
|
[datomic.api :as d])
|
|
(:import
|
|
(java.io ByteArrayOutputStream)
|
|
(java.text DecimalFormat)
|
|
(java.util UUID)))
|
|
|
|
(defn cell->pdf [cell]
|
|
(let [cell-contents (cond
|
|
(and (= :dollar (:format cell))
|
|
(dollars-0? (:value cell)))
|
|
"-"
|
|
|
|
(= :dollar (:format cell))
|
|
(.format (DecimalFormat. "$###,##0.00") (:value cell))
|
|
|
|
(= :percent (:format cell))
|
|
(.format (DecimalFormat. "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)))
|
|
|
|
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 (= 5 cell-count)
|
|
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))))))
|
|
|
|
(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-pnl [args data]
|
|
|
|
(let [data (<-graphql data)
|
|
args (<-graphql args)
|
|
clients (d/pull-many (d/db conn) '[: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 clients))
|
|
report (l-reports/summarize-pnl pnl-data)
|
|
output-stream (ByteArrayOutputStream.)]
|
|
(pdf/pdf
|
|
(-> [{:left-margin 10 :right-margin 10 :top-margin 15 :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 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 (str/replace (->> args :client_ids (d/pull-many (d/db conn) [:client/name]) (map :client/name) (str/join "-")) #" " "_" )]
|
|
(format "Profit-and-loss-%s-to-%s-for-%s" min-date max-date names)))
|
|
|
|
(defn print-pnl [user args data]
|
|
(let [uuid (str (UUID/randomUUID))
|
|
pdf-data (make-pnl args data)
|
|
name (args->name args)
|
|
key (str "reports/pnl/" uuid "/" name ".pdf")
|
|
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" 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"})
|
|
@(d/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 }))
|