(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] [: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))) (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)))]) (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 (into [{: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)))]] (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 [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/created (java.util.Date.)}]) url ))