From b67a6138a04ef0372477d79df6980d5c4baeac9c Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Mon, 7 Oct 2019 18:28:55 +0200 Subject: [PATCH] Add notes --- README.md | 7 +-- resources/config.edn | 1 + src/invoices/pdf.clj | 92 ++++++++++++++++++++++---------------- test/invoices/pdf_test.clj | 27 +++++++++++ 4 files changed, 86 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 35d272b..016391e 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ The buyer can have the following keys ### Items The list of items should contain maps with a required :title, optional :vat (if not provided it is assumed that -item is VAT free), :to (the date from which this item is valid), :from (the date till which this item is valid) -and a key providing the cost of the item. The price can be provided in one of the following ways: +item is VAT free), :to (the date from which this item is valid), :from (the date till which this item is valid), +:notes (a list of extra notes to be added at the bottom of the invoice) and a key providing the cost of the item. +The price can be provided in one of the following ways: * :netto - is a set price and will be displayed as provided * :hourly - is an hourly price - JIRA will be queried in order to work out how many hours should be billed @@ -80,7 +81,7 @@ and a key providing the cost of the item. The price can be provided in one of th Examples: ; 8% VAT, and a price of 600, recurring every period before 2019-05-30 - {:vat 8 :netto 600 :title "Shoes" :to "2019-05-30"} + {:vat 8 :netto 600 :title "Shoes" :to "2019-05-30" :notes ["A note at the bottom"]} ; 12% VAT, and an hourly rate of 12, first appearing on 2019-07-01 {:vat 12 :hourly 12 :title "Something worth 12/h" :from "2019-07-01"} diff --git a/resources/config.edn b/resources/config.edn index c1e0f6d..6d6eb35 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -12,6 +12,7 @@ :items [{:vat 8 :netto 123.21 :title "Buty kowbojskie"} {:vat 21 :hourly 43.12 :title "Usługa szewska"} {:netto 321.45 :title "Usługa szewska bez VAT"} + {:netto 321.45 :title "Usługa szewska zwolniona z VAT" :notes ["Podstawa zwolnienia z VAT: art. 113 ust. 1 i 9 Ustawa o VAT"]} {:vat 23 :function (* :worked (+ 1 2 3 (- 23 13))) :title "Pucowania obuwia"} {:vat 23 :base 4300.00 :per-day 4 :title "Praca za ladą"}] :font-path "/usr/share/fonts/truetype/freefont/FreeSans.ttf" diff --git a/src/invoices/pdf.clj b/src/invoices/pdf.clj index a8257db..ce1e219 100644 --- a/src/invoices/pdf.clj +++ b/src/invoices/pdf.clj @@ -21,6 +21,15 @@ (-> item vat round str) (brutto item)]))) +(defn format-notes + "Adds an `Uwagi` section with the given notes, one per line." + [notes] + (when (seq notes) + [[:spacer 2] [:line] + (concat + [:table {:border false :padding 0 :spacing 0} [[:phrase {:style :bold} "Uwagi:"]]] + (map vector notes))])) + (defn get-title [team who which] (let [[nr month year] (-> which (str/split #"/")) @@ -34,51 +43,58 @@ "Generate the actual pdf body" [title seller buyer items when number font] [{:title title - :right-margin 50 - :author (:name seller) - :bottom-margin 10 - :left-margin 10 - :top-margin 20 - :font font - :size "a4" - :footer "page"} + :right-margin 50 + :author (:name seller) + :bottom-margin 10 + :left-margin 10 + :top-margin 20 + :font font + :size "a4" + :footer "page"} - [:heading "Faktura"] - [:spacer] - [:paragraph (str "Nr " number)] - [:spacer 2] + [:heading "Faktura"] + [:spacer] + [:paragraph (str "Nr " number)] + [:spacer 2] - [:table {:border false :padding 0 :spacing 0 :num-cols 6} - [(format-param "sprzedawca") (format-value (:name seller)) (format-param "nabywca") (format-value (:name buyer))] - [(format-param "adres") (format-value (:address seller)) (format-param "adres") (format-value (:address buyer))] - [(format-param "nip") (format-value (:nip seller)) (format-param "nip") (format-value (:nip buyer))] - (clojure.core/when (:phone seller) [(format-param "numer telefonu") (format-value (:phone seller))])] + [:table {:border false :padding 0 :spacing 0 :num-cols 6} + [(format-param "sprzedawca") (format-value (:name seller)) (format-param "nabywca") (format-value (:name buyer))] + [(format-param "adres") (format-value (:address seller)) (format-param "adres") (format-value (:address buyer))] + [(format-param "nip") (format-value (:nip seller)) (format-param "nip") (format-value (:nip buyer))] + (clojure.core/when (:phone seller) [(format-param "numer telefonu") (format-value (:phone seller))])] - [:spacer] - [:line] - [:table {:border false :padding 0 :spacing 0 :num-cols 6} - [(format-param "data wystawienia") (-> when .toString format-value) (format-param "sposób płatności") (format-value "Przelew")] - [(format-param "data sprzedaży") (-> when .toString format-value) (format-param "bank") (format-value (:bank seller))] - [(format-param "termin płatności") (-> when (.plusDays 14) .toString format-value) (format-param "numer konta") (format-value (:account seller))]] + [:spacer] + [:line] + [:table {:border false :padding 0 :spacing 0 :num-cols 6} + [(format-param "data wystawienia") (-> when .toString format-value) (format-param "sposób płatności") (format-value "Przelew")] + [(format-param "data sprzedaży") (-> when .toString format-value) (format-param "bank") (format-value (:bank seller))] + [(format-param "termin płatności") (-> when (.plusDays 14) .toString format-value) (format-param "numer konta") (format-value (:account seller))]] - (concat - [:table - {:header [{:background-color [216 247 249]} "Lp." [:cell {:colspan 4} "Nazwa"] "Ilość" "Cena netto" "Stawka VAT" "Kwota VAT" "Wartość brutto"] - :num-cols 10}] - (->> items - (map format-product) - (map-indexed #(concat [(inc %1)] %2))) - [[[:cell {:background-color [84 219 229] :colspan 5 :align :center} "Razem"] - (format-total items (constantly 1)) - (format-total items :netto) - "" - (format-total items vat) - (format-total items brutto)]])]) + (concat + [:table + {:header [{:background-color [216 247 249]} "Lp." [:cell {:colspan 4} "Nazwa"] "Ilość" "Cena netto" "Stawka VAT" "Kwota VAT" "Wartość brutto"] + :num-cols 10}] + (->> items + (map format-product) + (map-indexed #(concat [(inc %1)] %2))) + [[[:cell {:background-color [84 219 229] :colspan 5 :align :center} "Razem"] + (format-total items (constantly 1)) + (format-total items :netto) + "" + (format-total items vat) + (format-total items brutto)]])]) + +(defn add-notes + "Some items require extra notes to be added (for various legal reasons)" + [body items] + (->> items (map :notes) (remove nil?) flatten distinct format-notes (conj body))) (defn render [seller buyer items when number & [font-path]] (let [title (get-title (:team seller) (:name seller) number)] (println " -" title) - (pdf (pdf-body title seller buyer items when number (clojure.core/when font-path{:encoding :unicode :ttf-name font-path})) - (str title ".pdf")) + (-> title + (pdf-body seller buyer items when number (clojure.core/when font-path {:encoding :unicode :ttf-name font-path})) + (add-notes items) + (pdf (str title ".pdf"))) title)) diff --git a/test/invoices/pdf_test.clj b/test/invoices/pdf_test.clj index 45fb52f..6d6e40d 100644 --- a/test/invoices/pdf_test.clj +++ b/test/invoices/pdf_test.clj @@ -33,6 +33,15 @@ (is (= (format-product {:netto 1000 :title "bla bla"}) [[:cell {:colspan 4} "bla bla"] "1" "1000.0" "zw." "0.0" "1000.0"])))) +(deftest test-format-notes + (testing "Check whether notes get correctly formatted" + (is (= (format-notes ["line 1" "line 2" "line 3"]) + [[:spacer 2] [:line] (list :table {:border false, :padding 0, :spacing 0} [[:phrase {:style :bold} "Uwagi:"]] ["line 1"] ["line 2"] ["line 3"])]))) + + (testing "Check whether the notes section is skipped if none provided." + (is (nil? (format-notes []))) + (is (nil? (format-notes nil))))) + (deftest test-get-title (testing "Check whether getting titles works" (is (= (get-title nil "mr blobby" "2019/02/11") "mr_blobby_luty_2019_02_11")) @@ -106,3 +115,21 @@ "" [:cell {:background-color [216 247 249]} "9.86"] [:cell {:background-color [216 247 249]} "454.52"]])]))))) + +(deftest test-add-notes + (testing "Check whether notes get correctly added" + (is (= (add-notes [:table] [{:notes ["line 1" "line 2"]} {} {} {:notes ["line 3"]} {:notes []}]) + [:table + [[:spacer 2] [:line] + (list :table {:border false, :padding 0, :spacing 0} [[:phrase {:style :bold} "Uwagi:"]] + ["line 1"] ["line 2"] ["line 3"])]]))) + + (testing "Check whether duplicates get removed" + (is (= (add-notes [:table] [{:notes ["line 1" "line 1"]} {} {} {:notes ["line 1"]} {:notes []}]) + [:table + [[:spacer 2] [:line] + (list :table {:border false, :padding 0, :spacing 0} [[:phrase {:style :bold} "Uwagi:"]] ["line 1"])]]))) + + (testing "Check whether the notes section is skipped if none provided." + (is (= (add-notes [:table] []) [:table nil])) + (is (= (add-notes [:table] nil) [:table nil]))))