Skip items with no :netto

This commit is contained in:
Daniel O'Connell 2019-10-09 13:34:49 +02:00
parent 19c371cfef
commit aea45a0609
6 changed files with 72 additions and 33 deletions

View File

@ -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: The price can be provided in one of the following ways:
* :netto - is a set price and will be displayed as provided * :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 * :base + :per-day - in the case of a variable number of hours worked. :base provides the amount that would be paid
if `<number of hours worked> == <number of hours in the current month if full time> / per-day`. if `<number of hours worked> == <number of hours in the current month if full time> / per-day`.
In the case of someone working full time, :per-day would be 8, and if the number of hours worked 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 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 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 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 * :function - an S-expression describing how to calculate the net value. Only numbers, basic mathematical
operations (+, -, /, *) and timesheet specific variables are supported (:worked, :required). 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 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. and make sure the `:worklogs` section has an item that can be used to access the worklog.

View File

@ -14,20 +14,24 @@
{:vat 21 :hourly 43.12 :title "Usługa szewska" :worklog :from-jira} {: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 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"]} {: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 :function (* 23 (+ 1 2 3 (- 23 13))) :title "Pucowania obuwia - stały koszt"}
{:vat 23 :base 4300.00 :per-day 4 :title "Praca za ladą"}] ;; {: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
:smtp {:host "smtp.gmail.com" :user "mr.blobby@buty.sa" :pass "asd;l;kjsdfkljld" :ssl true}}] {:vat 23 :base 4300.00 :per-day 4 :title "Praca za ladą" :worklog :item2}]
:worklogs [{:type :jira ;; :smtp {:host "smtp.gmail.com" :user "mr.blobby@buty.sa" :pass "asd;l;kjsdfkljld" :ssl true}
:ids [:from-jira] }]
:tempo-token "5zq7zF9LADefEGAs12eDDas3FDttiM" :worklogs [
:jira-token "qypaAsdFwASasEddDDddASdC" ;; {:type :jira
:jira-user "mr.blobby@boots.rs"} ;; :ids [:from-jira]
{:type :imap ;; :tempo-token "5zq7zF9LADefEGAs12eDDas3FDttiM"
:ids [:item1 :item2] ;; :jira-token "qypaAsdFwASasEddDDddASdC"
:folder "inbox" ;; :jira-user "mr.blobby@boots.rs"}
:host "imap.gmail.com" ;; {:type :imap
:user "mr.blobby@boots.rs" ;; :ids [:item1 :item2]
:pass "lksjdfklsjdflkjw" ;; :folder "inbox"
:from "hr@boots.rs" ;; :host "imap.gmail.com"
:subject "'Hours 'YYYY-MM" ;; :user "mr.blobby@boots.rs"
:headers [:id :worked]}]} ;; :pass "lksjdfklsjdflkjw"
;; :from "hr@boots.rs"
;; :subject "'Hours 'YYYY-MM"
;; :headers [:id :worked]}
]}

View File

@ -19,16 +19,16 @@
:else func)) :else func))
(defn calc-part-time [{worked :worked total :required} {base :base per-day :per-day}] (defn calc-part-time [{worked :worked total :required} {base :base per-day :per-day}]
(let [hourly (/ (* base 8) (* total per-day))] (when (and worked total)
(float (* hourly worked)))) (let [hourly (/ (* base 8) (* total per-day))]
(float (* hourly worked)))))
(defn calc-hourly [{worked :worked} {hourly :hourly}] (defn calc-hourly [{worked :worked} {hourly :hourly}]
(* worked hourly)) (when worked (* worked hourly)))
(defn calc-custom [worked {function :function}] (defn calc-custom [worked {function :function}]
(parse-custom worked function)) (parse-custom worked function))
(defn set-price (defn set-price
"Set the net price for the given item, calculating it from a worklog if applicable." "Set the net price for the given item, calculating it from a worklog if applicable."
[worked item] [worked item]

View File

@ -27,14 +27,17 @@
(defn for-month [{seller :seller buyer :buyer smtp :smtp callbacks :callbacks :as invoice} when & [number font]] (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))] (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))) (run-callbacks file callbacks)))
(defn item-price [worklogs item] (defn item-price [worklogs item]
(-> item :worklog (worklogs) (set-price item))) (-> item :worklog (worklogs) (set-price item)))
(defn calc-prices [invoice worklogs] (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] (defn prepare-invoice [{seller :seller font :font-path} month worklogs invoice]
(-> invoice (-> invoice
@ -54,7 +57,7 @@
:default 1 :default 1
:parse-fn #(Integer/parseInt %)] :parse-fn #(Integer/parseInt %)]
["-w" "--when DATE" "The month for which to generate the invoice" ["-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 %)] :parse-fn #(java.time.LocalDate/parse %)]
;; A non-idempotent option (:default is applied first) ;; A non-idempotent option (:default is applied first)
["-c" "--company NIP" "companies for which to generate invoices. All, if not provided" ["-c" "--company NIP" "companies for which to generate invoices. All, if not provided"
@ -82,9 +85,9 @@
errors (exit -1 (str/join "\n" errors)) errors (exit -1 (str/join "\n" errors))
(not= 1 (count arguments)) (exit -1 "No config file provided")) (not= 1 (count arguments)) (exit -1 "No config file provided"))
(println "Generating invoices") (println "Generating invoices" )
(let [month (java.time.LocalDate/now) (let [month (:when options)
config (-> "config.edn" (invoices month)) config (-> (first arguments) (invoices month))
worklogs (timesheets month (:worklogs config))] worklogs (timesheets month (:worklogs config))]
(process-invoices config month worklogs))) (process-invoices config month worklogs)))
(shutdown-agents)) (shutdown-agents))

View File

@ -69,14 +69,27 @@
(is (= (round (calc-part-time {:worked 22 :required 24} {:base 10 :per-day 4})) 18.33)) (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 ; 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 (deftest test-calc-hourly
(testing "Check whether calculating hourly rates works" (testing "Check whether calculating hourly rates works"
(is (= (calc-hourly {:worked 12} {:hourly 10}) 120)) (is (= (calc-hourly {:worked 12} {:hourly 10}) 120))
(is (= (calc-hourly {:worked 12.5} {:hourly 10}) 125.0)) (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 (deftest test-calc-custom

View File

@ -42,10 +42,25 @@
(is (nil? (format-notes []))) (is (nil? (format-notes [])))
(is (nil? (format-notes nil))))) (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 (deftest test-get-title
(testing "Check whether getting titles works" (testing "Check whether getting titles works"
(is (= (get-title nil "mr blobby" "2019/02/11") "mr_blobby_luty_2019_02_11")) (is (= (get-title {:title "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 {:seller {:team "asd" :name "mr blobby"}} "2019/02/11") "asd_mr_blobby_luty_2019_02_11"))))
(deftest test-get-pdf (deftest test-get-pdf
(testing "Check whether generating pdf bodies works" (testing "Check whether generating pdf bodies works"