From 975d1c9ebd05568271f568ef79eb75a4e57f0577 Mon Sep 17 00:00:00 2001 From: Merveille van Eck Date: Sun, 18 May 2025 03:13:54 +0200 Subject: [PATCH 1/6] added dependency for pdf generation --- deps.edn | 1 + 1 file changed, 1 insertion(+) diff --git a/deps.edn b/deps.edn index afd6d8c..71336e0 100644 --- a/deps.edn +++ b/deps.edn @@ -6,6 +6,7 @@ metosin/jsonista {:mvn/version "0.3.13"} com.draines/postal {:mvn/version "2.0.5"} hiccup/hiccup {:mvn/version "2.0.0-RC5"} + clj-htmltopdf/clj-htmltopdf {:mvn/version "0.2.2"} ;; seancorfield/next.jdbc {:mvn/version "1.2.659"} ;; org.xerial/sqlite-jdbc {:mv/version "3.49.1.0"} }} From b2dde485033012f38c7123b7301cf85347f49f78 Mon Sep 17 00:00:00 2001 From: Merveille van Eck Date: Sun, 18 May 2025 03:16:07 +0200 Subject: [PATCH 2/6] update to gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1de9559..d1fab4c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,5 @@ dev.token .cache/ gmail.password -resources/invoice.template.html .DS_Store From 2db083e13b834feab5a6b348365a8c969d5c8715 Mon Sep 17 00:00:00 2001 From: Merveille van Eck Date: Sun, 18 May 2025 03:17:05 +0200 Subject: [PATCH 3/6] added compenents and utils for generating invoice to pdf --- resources/invoice.css | 159 +++++++++++++++++++++++++++++++ src/coggl/invoice/components.clj | 80 ++++++++++++++++ src/coggl/invoice/core.clj | 14 +++ src/coggl/invoice/util.clj | 8 ++ 4 files changed, 261 insertions(+) create mode 100644 resources/invoice.css create mode 100644 src/coggl/invoice/components.clj create mode 100644 src/coggl/invoice/core.clj create mode 100644 src/coggl/invoice/util.clj diff --git a/resources/invoice.css b/resources/invoice.css new file mode 100644 index 0000000..db364c1 --- /dev/null +++ b/resources/invoice.css @@ -0,0 +1,159 @@ +/* invoice.css */ +body { + font-family: Helvetica, Arial, sans-serif; + color: #000; + background: #fff; + margin: 0; + padding: 0; +} + +.invoice-container { + max-width: 800px; + margin: 0 auto; + padding: 40px; + box-sizing: border-box; +} + +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; +} + +.invoice-number { + font-size: 36px; + margin: 0; + line-height: 1; +} + +.banking-details { + text-align: right; + font-size: 12px; + line-height: 1.4; +} + +.banking-details h2 { + margin: 0 0 8px; + font-size: 20px; +} + +.banking-details p { + margin: 4px 0; +} + +.banking-details .contact { + margin-top: 8px; +} + +.vat-note { + font-size: 10px; + margin: 8px 0 24px 0; +} + +.invoice-meta { + font-size: 12px; + margin-bottom: 24px; +} + +/* .info-grid using floats */ +.info-grid { + overflow: auto; + margin-bottom: 24px; +} +.info-grid::after { + content: ""; + display: table; + clear: both; +} +.info-grid > div { + float: left; + width: 30%; + margin-right: 5%; + font-size: 12px; + line-height: 1.4; +} +.info-grid > div:nth-child(3n) { + margin-right: 0; +} +.info-grid > div:nth-child(n+4) { + margin-top: 16px; +} +.info-grid strong { + display: block; + margin-bottom: 4px; +} + +/* Ensure table clears any floats above it */ +.items { + clear: both; + width: 100%; + border-collapse: collapse; + font-size: 12px; + margin-bottom: 24px; +} +.items th, +.items td { + padding: 8px 4px; +} +.items th { + border-bottom: 2px solid #000; + font-weight: bold; +} +/* Align numeric columns’ headers to the right */ +.items th:nth-child(2), +.items th:nth-child(3), +.items th:nth-child(4) { + text-align: right; +} +.items td { + border-bottom: 1px solid #ddd; +} +.items td.right { + text-align: right; +} + +/* summary-notes with floats */ +.summary-notes { + overflow: auto; + margin-bottom: 40px; + font-size: 12px; +} +.summary-notes::after { + content: ""; + display: table; + clear: both; +} +.notes { + float: left; + width: 70%; + line-height: 1.4; +} + +/* totals as table */ +.totals { + display: table; + margin-left: auto; +} +.total-row { + display: table-row; +} +.total-label, +.total-value { + display: table-cell; + padding: 2px 8px; +} +.total-value { + text-align: right; +} +/* bold for the total row */ +.total-row.total { + font-weight: bold; +} + +footer { + text-align: center; + font-size: 10px; + margin-top: 40px; +} + diff --git a/src/coggl/invoice/components.clj b/src/coggl/invoice/components.clj new file mode 100644 index 0000000..b039b8e --- /dev/null +++ b/src/coggl/invoice/components.clj @@ -0,0 +1,80 @@ +(ns coggl.invoice.components + (:require [coggl.invoice.util :as util])) + +(defn invoice + [& {:keys [company-name + invoice-for + company-address + company-contact + company-banking + entries ;; [{:description \"...\" :quantity n :unit-price p :total-price t} …] + adjustments + due-date + invoice-number]}] + (let [subtotal (reduce + (map :total-price entries)) + total (- subtotal adjustments)] + [:html {:lang "en"} + [:head + [:meta {:charset "UTF-8"}] + [:title (str "Invoice " invoice-number)] + ;; inline CSS placeholder — invoice-css must be bound to your stylesheet string + [:style {:type "text/css"} (util/invoice-css)]] + [:body + [:div.invoice-container + + ;; Header + [:header.header + [:h1.invoice-number invoice-number] + [:div.banking-details + [:h2 company-name] + [:p company-address] + [:p company-banking] + [:p.contact company-contact]]] + + ;; VAT note + [:p.vat-note "not a registered VAT vendor"] + + ;; Info grid: three columns + [:section.info-grid + [:div + [:strong "Invoice for"] [:br] + invoice-for] + [:div + [:strong "Invoice #"] [:br] + invoice-number] + [:div + [:strong "Due date"] [:br] + due-date]] + + ;; Line items + [:table.items + [:thead + [:tr + [:th "Description"] + [:th "Qty (h)"] + [:th "Unit Price"] + [:th "Total Price"]]] + [:tbody + (for [{:keys [description quantity unit-price total-price]} entries] + [:tr + [:td description] + [:td.right (str quantity)] + [:td.right (str unit-price)] + [:td.right (str total-price)]])]] + + ;; Summary totals (table layout) + [:div.summary-notes + [:div.totals + [:div.total-row + [:span.total-label "Subtotal"] + [:span.total-value (str subtotal)]] + [:div.total-row + [:span.total-label "Adjustments"] + [:span.total-value (str adjustments)]] + [:div.total-row.total + [:span.total-label "Total"] + [:span.total-value (str total)]]]] + + ;; Footer + [:footer "not a registered VAT Vendor"]]]])) + diff --git a/src/coggl/invoice/core.clj b/src/coggl/invoice/core.clj new file mode 100644 index 0000000..0f2a8f5 --- /dev/null +++ b/src/coggl/invoice/core.clj @@ -0,0 +1,14 @@ +(ns coggl.invoice.core + (:require [clj-htmltopdf.core :refer :all] + [hiccup.page :as h] + [coggl.invoice.components :as invoice] + [coggl.invoice.util :as util])) + +(defn render [app] + (h/html5 app)) + +(defn generate-invoice [invoice-data] + (-> (invoice/invoice invoice-data) + (render) + (->pdf (util/destination-path (str (:invoice-number {:invoice-number "test"}) ".pdf"))))) + diff --git a/src/coggl/invoice/util.clj b/src/coggl/invoice/util.clj new file mode 100644 index 0000000..fec7671 --- /dev/null +++ b/src/coggl/invoice/util.clj @@ -0,0 +1,8 @@ +(ns coggl.invoice.util + (:require [coggl.config :as conf])) + +(defn invoice-css [] + (slurp (:invoice-css-path conf/env))) + +(defn destination-path [f] + (str (:cache-dir conf/env) "/" f)) From 0d7bc535cf272400cdb1e37c7ca6ae42e3cae243 Mon Sep 17 00:00:00 2001 From: Merveille van Eck Date: Mon, 19 May 2025 11:22:03 +0200 Subject: [PATCH 4/6] added etaoin webscraper --- deps.edn | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deps.edn b/deps.edn index 71336e0..707630a 100644 --- a/deps.edn +++ b/deps.edn @@ -7,6 +7,4 @@ com.draines/postal {:mvn/version "2.0.5"} hiccup/hiccup {:mvn/version "2.0.0-RC5"} clj-htmltopdf/clj-htmltopdf {:mvn/version "0.2.2"} - ;; seancorfield/next.jdbc {:mvn/version "1.2.659"} - ;; org.xerial/sqlite-jdbc {:mv/version "3.49.1.0"} - }} + etaoin/etaoin {:mvn/version "1.1.43"}}} From 92777c9b7b5c316e46c3b2a3bc0b5be66dc18ffc Mon Sep 17 00:00:00 2001 From: Merveille van Eck Date: Mon, 19 May 2025 11:22:57 +0200 Subject: [PATCH 5/6] fixed naming of generated pdf's in invoice/core --- src/coggl/invoice/core.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coggl/invoice/core.clj b/src/coggl/invoice/core.clj index 0f2a8f5..68a3fea 100644 --- a/src/coggl/invoice/core.clj +++ b/src/coggl/invoice/core.clj @@ -1,5 +1,5 @@ (ns coggl.invoice.core - (:require [clj-htmltopdf.core :refer :all] + (:require [clj-htmltopdf.core :refer [->pdf]] [hiccup.page :as h] [coggl.invoice.components :as invoice] [coggl.invoice.util :as util])) @@ -7,8 +7,8 @@ (defn render [app] (h/html5 app)) -(defn generate-invoice [invoice-data] +(defn generate-invoice [{:keys [invoice-number] :as invoice-data}] (-> (invoice/invoice invoice-data) (render) - (->pdf (util/destination-path (str (:invoice-number {:invoice-number "test"}) ".pdf"))))) + (->pdf (util/destination-path (str invoice-number ".pdf"))))) From 8b28bd4d1d7512111f8cb4ab92a0e7786dc40bfe Mon Sep 17 00:00:00 2001 From: Merveille van Eck Date: Mon, 19 May 2025 11:23:23 +0200 Subject: [PATCH 6/6] added experimental namespace for using scrapers to gen pdfs instead --- src/coggl/invoice/pdf.clj | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/coggl/invoice/pdf.clj diff --git a/src/coggl/invoice/pdf.clj b/src/coggl/invoice/pdf.clj new file mode 100644 index 0000000..154df10 --- /dev/null +++ b/src/coggl/invoice/pdf.clj @@ -0,0 +1,25 @@ +(ns coggl.invoice.pdf + (:require [etaoin.api :as e] + [coggl.invoice.components :as inv])) + +(def mock-data {:total 19000 + :subtotal 19000 + :adjustments 0 + :due-date "immediate" + :company-name "ModulR (Pty) Ltd" + :invoice-for "Appsquare (Pty) Ltd" + :invoice-number "APSQ001" + :company-address "302 waterford place, camargue boulevard, macassar, cape town 7130" + :company-banking "Account Type: Checking\nBank: Capitec Business\nAccount Nr: 1234567890\nAccount holder: ModulR (Pty) Ltd" + :submission-date "05/05/2025" + :company-contact "+26 84 254 8270" + :entries [{:description "zohm react rebuild" + :quantity 80 + :unit-price 250 + :total-price 20000}]}) + +(def driver (e/chrome-headless)) + +(e/with-chrome-headless driver + (e/go driver "https://www.youtube.com") + (e/print-page driver "target/etaoin-play/printed.pdf"))