From aea45a060934ea08e67d44d01d7289c2101ca761 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Wed, 9 Oct 2019 13:34:49 +0200 Subject: [PATCH] Skip items with no :netto --- README.md | 8 ++++++-- resources/config.edn | 38 ++++++++++++++++++++----------------- src/invoices/calc.clj | 8 ++++---- src/invoices/core.clj | 15 +++++++++------ test/invoices/calc_test.clj | 17 +++++++++++++++-- test/invoices/pdf_test.clj | 19 +++++++++++++++++-- 6 files changed, 72 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index b27abf8..984f582 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,8 @@ item is VAT free), :to (the date from which this item is valid), :from (the date 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 + * :hourly - is an hourly price - worklogs will be queried in order to work out how many hours should be billed. + If no worklog could be found (or its :worked is nil), this item will be skipped. * :base + :per-day - in the case of a variable number of hours worked. :base provides the amount that would be paid if ` == / per-day`. In the case of someone working full time, :per-day would be 8, and if the number of hours worked @@ -81,9 +82,12 @@ The price can be provided in one of the following ways: had worked exactly half the number of working hours in a given month, then the price will also be :base. Otherwise the final price will be scaled accordingly. This is pretty much equivalent to working out what the hourly rate should be in a given month and multiplying it by the number - of hours worked in that month + of hours worked in that month. If no `:worked` value can be found (or if it's nil), this item + will be skipped. * :function - an S-expression describing how to calculate the net value. Only numbers, basic mathematical operations (+, -, /, *) and timesheet specific variables are supported (:worked, :required). + If a timesheet variable is used, but no such value can be found in the timesheet, an exception + will be raised. If the price is to be calculated on the basis of a worklog, add a `:worklog` key and make sure the `:worklogs` section has an item that can be used to access the worklog. diff --git a/resources/config.edn b/resources/config.edn index e369c8a..9469885 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -14,20 +14,24 @@ {:vat 21 :hourly 43.12 :title "Usługa szewska" :worklog :from-jira} {: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ą"}] - :smtp {:host "smtp.gmail.com" :user "mr.blobby@buty.sa" :pass "asd;l;kjsdfkljld" :ssl true}}] - :worklogs [{:type :jira - :ids [:from-jira] - :tempo-token "5zq7zF9LADefEGAs12eDDas3FDttiM" - :jira-token "qypaAsdFwASasEddDDddASdC" - :jira-user "mr.blobby@boots.rs"} - {:type :imap - :ids [:item1 :item2] - :folder "inbox" - :host "imap.gmail.com" - :user "mr.blobby@boots.rs" - :pass "lksjdfklsjdflkjw" - :from "hr@boots.rs" - :subject "'Hours 'YYYY-MM" - :headers [:id :worked]}]} + {:vat 23 :function (* 23 (+ 1 2 3 (- 23 13))) :title "Pucowania obuwia - stały koszt"} + ;; {:vat 23 :function (* :worked (+ 1 2 3 (- 23 13))) :title "Pucowania obuwia" :worklog :item1} ; this will cause an error if no :item1 worklog can be found + {:vat 23 :base 4300.00 :per-day 4 :title "Praca za ladą" :worklog :item2}] + ;; :smtp {:host "smtp.gmail.com" :user "mr.blobby@buty.sa" :pass "asd;l;kjsdfkljld" :ssl true} + }] + :worklogs [ + ;; {:type :jira + ;; :ids [:from-jira] + ;; :tempo-token "5zq7zF9LADefEGAs12eDDas3FDttiM" + ;; :jira-token "qypaAsdFwASasEddDDddASdC" + ;; :jira-user "mr.blobby@boots.rs"} + ;; {:type :imap + ;; :ids [:item1 :item2] + ;; :folder "inbox" + ;; :host "imap.gmail.com" + ;; :user "mr.blobby@boots.rs" + ;; :pass "lksjdfklsjdflkjw" + ;; :from "hr@boots.rs" + ;; :subject "'Hours 'YYYY-MM" + ;; :headers [:id :worked]} + ]} diff --git a/src/invoices/calc.clj b/src/invoices/calc.clj index 14480df..721c57d 100644 --- a/src/invoices/calc.clj +++ b/src/invoices/calc.clj @@ -19,16 +19,16 @@ :else func)) (defn calc-part-time [{worked :worked total :required} {base :base per-day :per-day}] - (let [hourly (/ (* base 8) (* total per-day))] - (float (* hourly worked)))) + (when (and worked total) + (let [hourly (/ (* base 8) (* total per-day))] + (float (* hourly worked))))) (defn calc-hourly [{worked :worked} {hourly :hourly}] - (* worked hourly)) + (when worked (* worked hourly))) (defn calc-custom [worked {function :function}] (parse-custom worked function)) - (defn set-price "Set the net price for the given item, calculating it from a worklog if applicable." [worked item] diff --git a/src/invoices/core.clj b/src/invoices/core.clj index c431798..e09062c 100644 --- a/src/invoices/core.clj +++ b/src/invoices/core.clj @@ -27,14 +27,17 @@ (defn for-month [{seller :seller buyer :buyer smtp :smtp callbacks :callbacks :as invoice} when & [number font]] (let [file (pdf/render invoice (last-working-day when) (invoice-number when number))] - (email/send-invoice file (:email buyer) smtp) + ;; (email/send-invoice file (:email buyer) smtp) (run-callbacks file callbacks))) (defn item-price [worklogs item] (-> item :worklog (worklogs) (set-price item))) (defn calc-prices [invoice worklogs] - (update invoice :items (partial map (partial item-price worklogs)))) + (->> invoice :items + (map (partial item-price worklogs)) + (remove (comp nil? :netto)) + (assoc invoice :items))) (defn prepare-invoice [{seller :seller font :font-path} month worklogs invoice] (-> invoice @@ -54,7 +57,7 @@ :default 1 :parse-fn #(Integer/parseInt %)] ["-w" "--when DATE" "The month for which to generate the invoice" - :default (-> (java.time.LocalDate/now) prev-month) + :default (java.time.LocalDate/now) :parse-fn #(java.time.LocalDate/parse %)] ;; A non-idempotent option (:default is applied first) ["-c" "--company NIP" "companies for which to generate invoices. All, if not provided" @@ -82,9 +85,9 @@ errors (exit -1 (str/join "\n" errors)) (not= 1 (count arguments)) (exit -1 "No config file provided")) - (println "Generating invoices") - (let [month (java.time.LocalDate/now) - config (-> "config.edn" (invoices month)) + (println "Generating invoices" ) + (let [month (:when options) + config (-> (first arguments) (invoices month)) worklogs (timesheets month (:worklogs config))] (process-invoices config month worklogs))) (shutdown-agents)) diff --git a/test/invoices/calc_test.clj b/test/invoices/calc_test.clj index 86c4d92..5f18a60 100644 --- a/test/invoices/calc_test.clj +++ b/test/invoices/calc_test.clj @@ -69,14 +69,27 @@ (is (= (round (calc-part-time {:worked 22 :required 24} {:base 10 :per-day 4})) 18.33)) ; 8h per day, 10 per month if all hours done, the person did all required hours - (is (= (round (calc-part-time {:worked 24 :required 24} {:base 10 :per-day 8})) 10.0)))) + (is (= (round (calc-part-time {:worked 24 :required 24} {:base 10 :per-day 8})) 10.0))) + (testing "Check whether nil is returned if :worked or :required missing are nil" + (is (nil? (calc-part-time {:worked nil :required 24} {:base 10 :per-day 8}))) + (is (nil? (calc-part-time {:worked 24 :required nil} {:base 10 :per-day 8}))) + (is (nil? (calc-part-time {:worked nil :required nil} {:base 10 :per-day 8})))) + + (testing "Check whether nil is returned if :worked or :required missing" + (is (nil? (calc-part-time {:required 24} {:base 10 :per-day 8}))) + (is (nil? (calc-part-time {:worked 24} {:base 10 :per-day 8}))) + (is (nil? (calc-part-time {} {:base 10 :per-day 8}))))) (deftest test-calc-hourly (testing "Check whether calculating hourly rates works" (is (= (calc-hourly {:worked 12} {:hourly 10}) 120)) (is (= (calc-hourly {:worked 12.5} {:hourly 10}) 125.0)) - (is (= (calc-hourly {:worked 12} {:hourly 10.99}) 131.88)))) + (is (= (calc-hourly {:worked 12} {:hourly 10.99}) 131.88))) + + (testing "nil is returned when no :worked provided" + (is (nil? (calc-hourly {:worked nil} {:hourly 10}))) + (is (nil? (calc-hourly {} {:hourly 10}))))) (deftest test-calc-custom diff --git a/test/invoices/pdf_test.clj b/test/invoices/pdf_test.clj index 6d6e40d..dd10e9b 100644 --- a/test/invoices/pdf_test.clj +++ b/test/invoices/pdf_test.clj @@ -42,10 +42,25 @@ (is (nil? (format-notes []))) (is (nil? (format-notes nil))))) +(deftest test-month-name + (testing "Check whether the month names are correctly returned" + (doseq [[i name] (rest (map-indexed vector month-names))] + (is (= (month-name (str "2012/" i "/02")) name))))) + +(deftest test-title-base + (testing "Check that :title is used if available" + (is (= (title-base {:seller {:team "A" :name "bla"} :title "title"}) "title")) + (is (= (title-base {:title "title"}) "title"))) + (testing "Check that the title is made from the team and name" + (is (= (title-base {:seller {:team "A" :name "bla"}}) "A_bla")) + (is (= (title-base {:seller {:name "bla"}}) "bla")) + (is (= (title-base {:seller {:team "A"}}) "A")) + (is (= (title-base {:seller {}}) "")))) + (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")) - (is (= (get-title "asd" "mr blobby" "2019/02/11") "asd_mr_blobby_luty_2019_02_11")))) + (is (= (get-title {:title "mr blobby"} "2019/02/11") "mr_blobby_luty_2019_02_11")) + (is (= (get-title {:seller {:team "asd" :name "mr blobby"}} "2019/02/11") "asd_mr_blobby_luty_2019_02_11")))) (deftest test-get-pdf (testing "Check whether generating pdf bodies works"