invoice.in 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #!@GUILE@ \
  2. -e main
  3. !#
  4. ;; Copyright 2015,2018 Eric Bavier <bavier@member.fsf.org>
  5. ;;
  6. ;; This program is free software. It is released under the GNU GPLv3,
  7. ;; or any later version, at your option.
  8. (use-modules (ice-9 format)
  9. (ice-9 match)
  10. (srfi srfi-1) ;append-map
  11. (srfi srfi-37) ;for args-fold
  12. (srfi srfi-26)) ;for cut
  13. (define (billable hours rate description)
  14. (vector hours rate description))
  15. (define (billable-hours billable)
  16. (vector-ref billable 0))
  17. (define (billable-rate billable)
  18. (vector-ref billable 1))
  19. (define (billable-description billable)
  20. (vector-ref billable 2))
  21. (define (subtotal billable)
  22. (* (billable-hours billable)
  23. (billable-rate billable)))
  24. (define (total billables)
  25. (apply + (map subtotal billables)))
  26. (define* (invoice number
  27. bill-to
  28. bill-for
  29. billables
  30. #:optional
  31. (port (current-output-port))
  32. #:key
  33. (name (getenv "USERNAME"))
  34. (address "ADDRESS")
  35. (phone #f)
  36. (email #f)
  37. (closing "Thank you for your business!")
  38. (payment #f))
  39. "Writes to the current output, or PORT if given, an invoice in LaTeX
  40. markup. The list of billable items in BILLABLES will be formatted in
  41. a table with subtotals and total."
  42. (format port "~
  43. \\documentclass[12pt]{article}
  44. \\usepackage{microtype}
  45. \\usepackage{xcolor}
  46. \\usepackage{ctable}
  47. \\usepackage{tabularx}
  48. \\leftskip 0.1in
  49. \\parindent -0.1in
  50. \\setlength{\\textfloatsep}{0cm}
  51. \\begin{document}
  52. \\thispagestyle{empty} %% Do not output page numbers
  53. \\noindent
  54. \\parbox[t]{.5\\linewidth}{\\Large\\scshape\\textbf{~a}}
  55. \\hfill
  56. \\parbox[t]{.5\\linewidth}{\\raggedleft \\Huge\\scshape\\textcolor{gray}{Invoice}}
  57. \\vspace{2\\baselineskip}
  58. \\noindent
  59. \\parbox[t]{.5\\linewidth}{\\raggedright ~a \\\\\\vspace{1ex}~@[Phone: ~a~]~@[\\\\ Email: ~a~]}
  60. \\hfill
  61. \\parbox[t]{.5\\linewidth}{\\raggedleft \\textsc{Date}: \\today \\\\ \\textsc{Invoice}: ~d}
  62. \\vspace{2\\baselineskip}
  63. \\noindent
  64. \\parbox[t]{.5\\linewidth}{\\textsc{Bill To}:\\\\ ~a}
  65. \\hfil
  66. \\parbox[t]{.5\\linewidth}{\\textsc{For}:\\\\ ~a}
  67. \\vspace{3\\baselineskip}
  68. \\noindent
  69. \\begin{tabularx}{1.0\\linewidth}[h]{Xrrr}
  70. \\toprule
  71. \\textit{Description} & \\textit{Hours} & \\textit{\\$/hr} & \\textit{Amount} \\\\ \\midrule~a
  72. & & \\textsc{Total}: & \\$ ~$ \\\\ \\bottomrule
  73. \\end{tabularx}
  74. \\vspace{2\\baselineskip}
  75. \\noindent ~:[Make checks payable to \\textbf{~a}~;~a~]
  76. \\vspace{2\\baselineskip}
  77. \\begin{center}
  78. \\textsc{~a}
  79. \\end{center}
  80. \\end{document}~%"
  81. name
  82. address
  83. phone
  84. email
  85. number
  86. bill-to
  87. bill-for
  88. (format #f "~:{
  89. ~a & ~f & ~$ & ~$ \\\\~}"
  90. (map (lambda (b)
  91. (list (billable-description b)
  92. (billable-hours b)
  93. (billable-rate b)
  94. (subtotal b)))
  95. billables))
  96. (total billables)
  97. payment
  98. (or payment name)
  99. closing))
  100. (define %config-file
  101. (string-append (getenv "HOME") "/.invoicerc"))
  102. (define %last-invoice-file
  103. (string-append (getenv "HOME") "/.last-invoice-number"))
  104. (define (load-config)
  105. (primitive-load %config-file))
  106. (define (last-invoice-number)
  107. (primitive-load %last-invoice-file))
  108. (define (write-invoice-number num)
  109. (call-with-output-file %last-invoice-file
  110. (cut format <> "~d" num)))
  111. (define (read-args args config)
  112. (args-fold args
  113. (let ((display-and-exit-proc
  114. (lambda (msg)
  115. (lambda (opt name args load)
  116. (display msg) (newline) (quit))))
  117. (simple-opt
  118. (lambda (short long)
  119. (option (list short long) #t #f
  120. (lambda (opt name arg load)
  121. (alist-replace long arg load))))))
  122. (list (option '(#\v "version") #f #f
  123. (display-and-exit-proc "@PACKAGE_NAME@ @PACKAGE_VERSION@"))
  124. (option '(#\h "help") #f #f
  125. (display-and-exit-proc
  126. (format #f
  127. "Usage: invoice [options] -D <desc> -R <rate> -H <hours> ...
  128. Options:
  129. -h, --help Print this message
  130. -v, --version Print this program's version
  131. -t STR, --bill-to=STR
  132. -f STR, --bill-for=STR
  133. -n STR, --name=STR
  134. -a STR, --address=STR
  135. -p STR, --phone=STR
  136. -e STR, --email=STR
  137. -c STR, --closing=STR
  138. -y STR, --payment=STR
  139. Set template contents.
  140. -N STR, --invoice-number=NUM
  141. Override the invoice number in ~a
  142. -U, --no-update
  143. Do NOT record the latest invoice number in ~a
  144. -D STR, --description=STR
  145. Begin a billable item and provide its description.
  146. Expects to be followed by an --hours argument, and
  147. may optionally be followed by --rate.
  148. -H NUM, --hours=NUM
  149. Set the number of hours for the current billable item.
  150. -R STR, --rate=STR
  151. If given before the first --description argument is
  152. encountered, sets the default hourly rate. Otherwise
  153. sets the hourly rate for the current billable item.
  154. Default configuration will be loaded from ~a
  155. Send bug reports to @PACKAGE_BUGREPORT@~%"
  156. %last-invoice-file
  157. %last-invoice-file
  158. %config-file)))
  159. (simple-opt #\t "bill-to")
  160. (simple-opt #\f "bill-for")
  161. (simple-opt #\n "name")
  162. (simple-opt #\a "address")
  163. (simple-opt #\p "phone")
  164. (simple-opt #\e "email")
  165. (simple-opt #\c "closing")
  166. (simple-opt #\y "payment")
  167. (simple-opt #\N "invoice-number")
  168. (option '(#\U "no-update") #f #f
  169. (lambda (opt name arg load)
  170. (alist-replace "no-update" #t load)))
  171. (option '(#\D "description") #t #f
  172. (lambda (opt name arg load)
  173. (alist-reduce
  174. "billables"
  175. `(("description" . ,arg)
  176. ("rate" . ,(or (config-lookup "rate" load)
  177. 0.0))
  178. ("hours" . 0.0))
  179. load cons '())))
  180. (option '(#\R "rate") #t #f
  181. (lambda (opt name arg load)
  182. (let ((rate (string->number arg)))
  183. (match (config-lookup "billables" load)
  184. ((head . tail)
  185. (alist-replace "billables"
  186. `(,(alist-replace "rate" rate head)
  187. ,@tail)
  188. load))
  189. (#f
  190. (alist-replace "rate" rate load))))))
  191. (option '(#\H "hours") #t #f
  192. (lambda (opt name arg load)
  193. (let ((hours (string->number arg)))
  194. (match (config-lookup "billables" load)
  195. ((head . tail)
  196. (alist-replace "billables"
  197. `(,(alist-replace "hours" hours head)
  198. ,@tail)
  199. load))
  200. (else
  201. (error (format #f "invoice: cannot give ~a before ~:@
  202. --description~%"
  203. name)))))))))
  204. (lambda (opt name arg loads)
  205. (error "Unrecognized option `~A'" name))
  206. (lambda (op loads) (cons op loads))
  207. config))
  208. (define (config-lookup key configdb)
  209. "Lookup the configuration variable KEY in the configuration database
  210. CONFIGDB. Values are assumed to be strings. If KEY does not have a
  211. value then #f is returned."
  212. (match (assoc key configdb)
  213. ((_ . value) value)
  214. (#f #f)))
  215. (define (maybe-keyword kw configdb)
  216. "Lookup the the KW's name in configdb, and return (KW <value>) or an
  217. empty list if there is no value on CONFIGDB."
  218. (or (and=> (config-lookup (symbol->string (keyword->symbol kw)) configdb)
  219. (cut list kw <>))
  220. '()))
  221. (define* (alist-replace key datum alist #:optional (= equal?))
  222. "Insert an association KEY and DATUM into ALIST. If an association
  223. for KEY already exists in ALIST, it is overriden. Equality is
  224. determined with the = predicate, or equal? if not given."
  225. (alist-cons key datum
  226. (alist-delete key alist =)))
  227. (define (alist->billables billable-alist)
  228. "Transform a list of alist-based billable items into the more
  229. compact format."
  230. (map (lambda (b)
  231. (billable (assoc-ref b "hours")
  232. (assoc-ref b "rate")
  233. (assoc-ref b "description")))
  234. billable-alist))
  235. (define* (alist-reduce key datum alist proc init #:optional (= equal?))
  236. "Insert an association into ALIST which is the result of (PROC DATUM
  237. <previous>), where <previous> is either the existing value of the
  238. association or INIT if there is no association with KEY."
  239. (alist-replace key
  240. (proc datum (or (and=> (assoc key alist =) cdr)
  241. init))
  242. alist =))
  243. (define (main args)
  244. (let* ((config (read-args (cdr args) (load-config)))
  245. (invoice-num (or (and=> (config-lookup "invoice-number" config)
  246. string->number)
  247. (1+ (last-invoice-number)))))
  248. (apply invoice `(,invoice-num
  249. ,(config-lookup "bill-to" config)
  250. ,(config-lookup "bill-for" config)
  251. ,(alist->billables (or (and=> (config-lookup "billables" config)
  252. reverse)
  253. '()))
  254. ,@(append-map (cut maybe-keyword <> config)
  255. '(#:name #:address #:phone #:email
  256. #:closing #:payment))))
  257. (unless (config-lookup "no-update" config)
  258. (write-invoice-number invoice-num))))