Home / Emacs / Org Invoice
Lisp code
Note. All global symbols are suffixed with + to avoid name clashes.
All code resides in the org-invoice.el file:
;;; -*- lexical-binding: t -*-
;; Author: Rudolf Adamkovic <rudolf@adamkovic.org>
;; This program is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by the Free
;; Software Foundation, either version 3 of the License, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
;; more details.
;; You should have received a copy of the GNU General Public License along with
;; this program. If not, see <https://www.gnu.org/licenses/>.
;;; Customization
;; TODO Customize elsewhere.
(defcustom my-org-invoice-work-log-id nil
"Org ID of the section with invoice details and clocked hours"
:type '(string)
:group 'my-org-invoice)
;; TODO Customize elsewhere.
(defcustom my-org-invoice-list-id nil
"Org ID of the section to which invoices are generated"
:type '(string)
:group 'my-org-invoice)
(defcustom my-org-invoice-title "Invoice"
"The title used for invoices, suffixed with the invoice number."
:type '(string)
:group 'my-org-invoice)
(setopt my-org-invoice-work-log-id "0914DEB9-E2AD-46BF-8DAC-4B48E3F81359"
my-org-invoice-list-id "D4B2836B-9856-425B-9527-4372A0C5AB5C")
;;; Templating functions
(defun my-org-invoice-number (&optional lastp)
"Return the number of the next, or the last, invoice."
(let ((last-invoice-number
(save-window-excursion
(org-id-goto my-org-invoice-list-id)
(and (org-goto-first-child)
(named-let recur ((greatest-number 0))
(let* ((current-number
(let ((heading (org-get-heading t t t t)))
(and (string-match
(format "%s %s"
(regexp-quote my-org-invoice-title)
(rx (group (one-or-more digit))))
heading)
(string-to-number (match-string 1 heading)))))
(greatest-number
(if current-number
(max current-number greatest-number)
greatest-number)))
(if (outline-get-next-sibling)
(recur greatest-number)
greatest-number)))))))
(if lastp
last-invoice-number
(if last-invoice-number
(1+ last-invoice-number)
1))))
(defun my-org-invoice-title ()
"Return the title for the next invoice."
(format "%s %d" my-org-invoice-title (my-org-invoice-number)))
(defun my-org-invoice-file-name ()
"Return the file name for the next invoice."
(format "invoice-%d" (my-org-invoice-number)))
(defun my-org-invoice-property (name &optional index)
"Return the INDEX-th PROPERTY value, padded for templating."
(save-window-excursion
(org-id-goto my-org-invoice-work-log-id)
(string-pad
(or (nth (or index 0)
(let ((separator "\n"))
(when-let* ((all (let ((org-property-separators
(list (cons (regexp-quote name) separator))))
(org-entry-get (point) (concat "INVOICE_" name)))))
(string-split all separator))))
"n/a")
(length
(format "%%(%s)"
(string-join
(append (list (symbol-name #'my-org-invoice-property))
(list (format "\"%s\"" name))
(list (if index (number-to-string index) nil)))
" "))))))
(defun my-org-invoice-details (prefix)
"Return the \"Details\" section as a list of strings."
(save-window-excursion
(org-id-goto my-org-invoice-work-log-id)
(goto-char (org-log-beginning))
(named-let recur ((lines '()))
(if (eq (org-element-type (org-element-at-point-no-context)) 'clock)
(let ((line (buffer-substring-no-properties (point) (pos-eol))))
(forward-line)
(recur (cons (string-replace org-clock-string prefix line) lines)))
(string-join (reverse lines) "\n")))))
(defun my-org-invoice-total (index)
"Return the INDEX-th value of the TOTAL section, padded for templating."
(save-window-excursion
(org-id-goto my-org-invoice-work-log-id)
(when-let* ((rate-string (org-entry-get (point) "INVOICE_TOTAL_HOURLY"))
(rate (string-to-number rate-string))
(currency (org-entry-get (point) "INVOICE_TOTAL_CURRENCY")))
(string-pad
(let ((hours (/ (float (org-clock-sum-current-item)) 60)))
(pcase index
(0 (format "hours * (%s / hour)" currency))
(1 (format "= %.2f * %.2f" hours rate))
(2 (format "= %.2f %s" (* hours rate) currency))
(_ "n/a")))
(length
(format "%%(%s)"
(string-join
(append (list (symbol-name #'my-org-invoice-total))
(list (if index (number-to-string index) nil)))
" ")))))))
;;; Installation
(with-eval-after-load 'org-capture
(add-to-list 'org-capture-templates
`("i" "Invoice" entry (id ,my-org-invoice-list-id)
(file "org-invoice+template.org")
:prepend t
:jump-to-captured t
:empty-lines-after 1)))