summaryrefslogtreecommitdiff
path: root/contrib/lisp/org-invoice.el
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/lisp/org-invoice.el')
-rw-r--r--contrib/lisp/org-invoice.el401
1 files changed, 401 insertions, 0 deletions
diff --git a/contrib/lisp/org-invoice.el b/contrib/lisp/org-invoice.el
new file mode 100644
index 0000000..c951d4e
--- /dev/null
+++ b/contrib/lisp/org-invoice.el
@@ -0,0 +1,401 @@
+;;; org-invoice.el --- Help manage client invoices in OrgMode
+;;
+;; Copyright (C) 2008-2012 pmade inc. (Peter Jones pjones@pmade.com)
+;;
+;; This file is not part of GNU Emacs.
+;;
+;; Permission is hereby granted, free of charge, to any person obtaining
+;; a copy of this software and associated documentation files (the
+;; "Software"), to deal in the Software without restriction, including
+;; without limitation the rights to use, copy, modify, merge, publish,
+;; distribute, sublicense, and/or sell copies of the Software, and to
+;; permit persons to whom the Software is furnished to do so, subject to
+;; the following conditions:
+;;
+;; The above copyright notice and this permission notice shall be
+;; included in all copies or substantial portions of the Software.
+;;
+;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+;;
+;; Commentary:
+;;
+;; Building on top of the terrific OrgMode, org-invoice tries to
+;; provide functionality for managing invoices. Currently, it does
+;; this by implementing an OrgMode dynamic block where invoice
+;; information is aggregated so that it can be exported.
+;;
+;; It also provides a library of functions that can be used to collect
+;; this invoice information and use it in other ways, such as
+;; submitting it to on-line invoicing tools.
+;;
+;; I'm already working on an elisp package to submit this invoice data
+;; to the FreshBooks on-line accounting tool.
+;;
+;; Usage:
+;;
+;; In your ~/.emacs:
+;; (autoload 'org-invoice-report "org-invoice")
+;; (autoload 'org-dblock-write:invoice "org-invoice")
+;;
+;; See the documentation in the following functions:
+;;
+;; `org-invoice-report'
+;; `org-dblock-write:invoice'
+;;
+;; Latest version:
+;;
+;; git clone git://pmade.com/elisp
+(eval-when-compile
+ (require 'cl)
+ (require 'org))
+
+(defgroup org-invoice nil
+ "OrgMode Invoice Helper"
+ :tag "Org-Invoice" :group 'org)
+
+(defcustom org-invoice-long-date-format "%A, %B %d, %Y"
+ "The format string for long dates."
+ :type 'string :group 'org-invoice)
+
+(defcustom org-invoice-strip-ts t
+ "Remove org timestamps that appear in headings."
+ :type 'boolean :group 'org-invoice)
+
+(defcustom org-invoice-default-level 2
+ "The heading level at which a new invoice starts. This value
+is used if you don't specify a scope option to the invoice block,
+and when other invoice helpers are trying to find the heading
+that starts an invoice.
+
+The default is 2, assuming that you structure your invoices so
+that they fall under a single heading like below:
+
+* Invoices
+** This is invoice number 1...
+** This is invoice number 2...
+
+If you don't structure your invoices using those conventions,
+change this setting to the number that corresponds to the heading
+at which an invoice begins."
+ :type 'integer :group 'org-invoice)
+
+(defcustom org-invoice-start-hook nil
+ "Hook called when org-invoice is about to collect data from an
+invoice heading. When this hook is called, point will be on the
+heading where the invoice begins.
+
+When called, `org-invoice-current-invoice' will be set to the
+alist that represents the info for this invoice."
+ :type 'hook :group 'org-invoice)
+
+ (defcustom org-invoice-heading-hook nil
+ "Hook called when org-invoice is collecting data from a
+heading. You can use this hook to add additional information to
+the alist that represents the heading.
+
+When this hook is called, point will be on the current heading
+being processed, and `org-invoice-current-item' will contain the
+alist for the current heading.
+
+This hook is called repeatedly for each invoice item processed."
+ :type 'hook :group 'org-invoice)
+
+(defvar org-invoice-current-invoice nil
+ "Information about the current invoice.")
+
+(defvar org-invoice-current-item nil
+ "Information about the current invoice item.")
+
+(defvar org-invoice-table-params nil
+ "The table parameters currently being used.")
+
+(defvar org-invoice-total-time nil
+ "The total invoice time for the summary line.")
+
+(defvar org-invoice-total-price nil
+ "The total invoice price for the summary line.")
+
+(defconst org-invoice-version "1.0.0"
+ "The org-invoice version number.")
+
+(defun org-invoice-goto-tree (&optional tree)
+ "Move point to the heading that represents the head of the
+current invoice. The heading level will be taken from
+`org-invoice-default-level' unless tree is set to a string that
+looks like tree2, where the level is 2."
+ (let ((level org-invoice-default-level))
+ (save-match-data
+ (when (and tree (string-match "^tree\\([0-9]+\\)$" tree))
+ (setq level (string-to-number (match-string 1 tree)))))
+ (org-back-to-heading)
+ (while (and (> (org-reduced-level (org-outline-level)) level)
+ (org-up-heading-safe)))))
+
+(defun org-invoice-heading-info ()
+ "Return invoice information from the current heading."
+ (let ((title (org-no-properties (org-get-heading t)))
+ (date (org-entry-get nil "TIMESTAMP" 'selective))
+ (work (org-entry-get nil "WORK" nil))
+ (rate (or (org-entry-get nil "RATE" t) "0"))
+ (level (org-outline-level))
+ raw-date long-date)
+ (unless date (setq date (org-entry-get nil "TIMESTAMP_IA" 'selective)))
+ (unless date (setq date (org-entry-get nil "TIMESTAMP" t)))
+ (unless date (setq date (org-entry-get nil "TIMESTAMP_IA" t)))
+ (unless work (setq work (org-entry-get nil "CLOCKSUM" nil)))
+ (unless work (setq work "00:00"))
+ (when date
+ (setq raw-date (apply 'encode-time (org-parse-time-string date)))
+ (setq long-date (format-time-string org-invoice-long-date-format raw-date)))
+ (when (and org-invoice-strip-ts (string-match org-ts-regexp-both title))
+ (setq title (replace-match "" nil nil title)))
+ (when (string-match "^[ \t]+" title)
+ (setq title (replace-match "" nil nil title)))
+ (when (string-match "[ \t]+$" title)
+ (setq title (replace-match "" nil nil title)))
+ (setq work (org-hh:mm-string-to-minutes work))
+ (setq rate (string-to-number rate))
+ (setq org-invoice-current-item (list (cons 'title title)
+ (cons 'date date)
+ (cons 'raw-date raw-date)
+ (cons 'long-date long-date)
+ (cons 'work work)
+ (cons 'rate rate)
+ (cons 'level level)
+ (cons 'price (* rate (/ work 60.0)))))
+ (run-hook-with-args 'org-invoice-heading-hook)
+ org-invoice-current-item))
+
+(defun org-invoice-level-min-max (ls)
+ "Return a list where the car is the min level, and the cdr the max."
+ (let ((max 0) min level)
+ (dolist (info ls)
+ (when (cdr (assoc 'date info))
+ (setq level (cdr (assoc 'level info)))
+ (when (or (not min) (< level min)) (setq min level))
+ (when (> level max) (setq max level))))
+ (cons (or min 0) max)))
+
+(defun org-invoice-collapse-list (ls)
+ "Reorganize the given list by dates."
+ (let ((min-max (org-invoice-level-min-max ls)) new)
+ (dolist (info ls)
+ (let* ((date (cdr (assoc 'date info)))
+ (work (cdr (assoc 'work info)))
+ (price (cdr (assoc 'price info)))
+ (long-date (cdr (assoc 'long-date info)))
+ (level (cdr (assoc 'level info)))
+ (bucket (cdr (assoc date new))))
+ (if (and (/= (car min-max) (cdr min-max))
+ (= (car min-max) level)
+ (= work 0) (not bucket) date)
+ (progn
+ (setq info (assq-delete-all 'work info))
+ (push (cons 'total-work 0) info)
+ (push (cons date (list info)) new)
+ (setq bucket (cdr (assoc date new))))
+ (when (and date (not bucket))
+ (setq bucket (list (list (cons 'date date)
+ (cons 'title long-date)
+ (cons 'total-work 0)
+ (cons 'price 0))))
+ (push (cons date bucket) new)
+ (setq bucket (cdr (assoc date new))))
+ (when (and date bucket)
+ (setcdr (assoc 'total-work (car bucket))
+ (+ work (cdr (assoc 'total-work (car bucket)))))
+ (setcdr (assoc 'price (car bucket))
+ (+ price (cdr (assoc 'price (car bucket)))))
+ (nconc bucket (list info))))))
+ (nreverse new)))
+
+(defun org-invoice-info-to-table (info)
+ "Create a single org table row from the given info alist."
+ (let ((title (cdr (assoc 'title info)))
+ (total (cdr (assoc 'total-work info)))
+ (work (cdr (assoc 'work info)))
+ (price (cdr (assoc 'price info)))
+ (with-price (plist-get org-invoice-table-params :price)))
+ (unless total
+ (setq
+ org-invoice-total-time (+ org-invoice-total-time work)
+ org-invoice-total-price (+ org-invoice-total-price price)))
+ (setq total (and total (org-minutes-to-hh:mm-string total)))
+ (setq work (and work (org-minutes-to-hh:mm-string work)))
+ (insert-before-markers
+ (concat "|" title
+ (cond
+ (total (concat "|" total))
+ (work (concat "|" work)))
+ (and with-price price (concat "|" (format "%.2f" price)))
+ "|" "\n"))))
+
+(defun org-invoice-list-to-table (ls)
+ "Convert a list of heading info to an org table"
+ (let ((with-price (plist-get org-invoice-table-params :price))
+ (with-summary (plist-get org-invoice-table-params :summary))
+ (with-header (plist-get org-invoice-table-params :headers))
+ (org-invoice-total-time 0)
+ (org-invoice-total-price 0))
+ (insert-before-markers
+ (concat "| Task / Date | Time" (and with-price "| Price") "|\n"))
+ (dolist (info ls)
+ (insert-before-markers "|-\n")
+ (mapc 'org-invoice-info-to-table (if with-header (cdr info) (cdr (cdr info)))))
+ (when with-summary
+ (insert-before-markers
+ (concat "|-\n|Total:|"
+ (org-minutes-to-hh:mm-string org-invoice-total-time)
+ (and with-price (concat "|" (format "%.2f" org-invoice-total-price)))
+ "|\n")))))
+
+(defun org-invoice-collect-invoice-data ()
+ "Collect all the invoice data from the current OrgMode tree and
+return it. Before you call this function, move point to the
+heading that begins the invoice data, usually using the
+`org-invoice-goto-tree' function."
+ (let ((org-invoice-current-invoice
+ (list (cons 'point (point)) (cons 'buffer (current-buffer))))
+ (org-invoice-current-item nil))
+ (save-restriction
+ (org-narrow-to-subtree)
+ (org-clock-sum)
+ (run-hook-with-args 'org-invoice-start-hook)
+ (cons org-invoice-current-invoice
+ (org-invoice-collapse-list
+ (org-map-entries 'org-invoice-heading-info t 'tree 'archive))))))
+
+(defun org-dblock-write:invoice (params)
+ "Function called by OrgMode to write the invoice dblock. To
+create an invoice dblock you can use the `org-invoice-report'
+function.
+
+The following parameters can be given to the invoice block (for
+information about dblock parameters, please see the Org manual):
+
+:scope Allows you to override the `org-invoice-default-level'
+ variable. The only supported values right now are ones
+ that look like :tree1, :tree2, etc.
+
+:prices Set to nil to turn off the price column.
+
+:headers Set to nil to turn off the group headers.
+
+:summary Set to nil to turn off the final summary line."
+ (let ((scope (plist-get params :scope))
+ (org-invoice-table-params params)
+ (zone (move-marker (make-marker) (point)))
+ table)
+ (unless scope (setq scope 'default))
+ (unless (plist-member params :price) (plist-put params :price t))
+ (unless (plist-member params :summary) (plist-put params :summary t))
+ (unless (plist-member params :headers) (plist-put params :headers t))
+ (save-excursion
+ (cond
+ ((eq scope 'tree) (org-invoice-goto-tree "tree1"))
+ ((eq scope 'default) (org-invoice-goto-tree))
+ ((symbolp scope) (org-invoice-goto-tree (symbol-name scope))))
+ (setq table (org-invoice-collect-invoice-data))
+ (goto-char zone)
+ (org-invoice-list-to-table (cdr table))
+ (goto-char zone)
+ (org-table-align)
+ (move-marker zone nil))))
+
+(defun org-invoice-in-report-p ()
+ "Check to see if point is inside an invoice report."
+ (let ((pos (point)) start)
+ (save-excursion
+ (end-of-line 1)
+ (and (re-search-backward "^#\\+BEGIN:[ \t]+invoice" nil t)
+ (setq start (match-beginning 0))
+ (re-search-forward "^#\\+END:.*" nil t)
+ (>= (match-end 0) pos)
+ start))))
+
+(defun org-invoice-report (&optional jump)
+ "Create or update an invoice dblock report. If point is inside
+an existing invoice report, the report is updated. If point
+isn't inside an invoice report, a new report is created.
+
+When called with a prefix argument, move to the first invoice
+report after point and update it.
+
+For information about various settings for the invoice report,
+see the `org-dblock-write:invoice' function documentation.
+
+An invoice report is created by reading a heading tree and
+collecting information from various properties. It is assumed
+that all invoices start at a second level heading, but this can
+be configured using the `org-invoice-default-level' variable.
+
+Here is an example, where all invoices fall under the first-level
+heading Invoices:
+
+* Invoices
+** Client Foo (Jan 01 - Jan 15)
+*** [2008-01-01 Tue] Built New Server for Production
+*** [2008-01-02 Wed] Meeting with Team to Design New System
+** Client Bar (Jan 01 - Jan 15)
+*** [2008-01-01 Tue] Searched for Widgets on Google
+*** [2008-01-02 Wed] Billed You for Taking a Nap
+
+In this layout, invoices begin at level two, and invoice
+items (tasks) are at level three. You'll notice that each level
+three heading starts with an inactive timestamp. The timestamp
+can actually go anywhere you want, either in the heading, or in
+the text under the heading. But you must have a timestamp
+somewhere so that the invoice report can group your items by
+date.
+
+Properties are used to collect various bits of information for
+the invoice. All properties can be set on the invoice item
+headings, or anywhere in the tree. The invoice report will scan
+up the tree looking for each of the properties.
+
+Properties used:
+
+CLOCKSUM: You can use the Org clock-in and clock-out commands to
+ create a CLOCKSUM property. Also see WORK.
+
+WORK: An alternative to the CLOCKSUM property. This property
+ should contain the amount of work that went into this
+ invoice item formatted as HH:MM (e.g. 01:30).
+
+RATE: Used to calculate the total price for an invoice item.
+ Should be the price per hour that you charge (e.g. 45.00).
+ It might make more sense to place this property higher in
+ the hierarchy than on the invoice item headings.
+
+Using this information, a report is generated that details the
+items grouped by days. For each day you will be able to see the
+total number of hours worked, the total price, and the items
+worked on.
+
+You can place the invoice report anywhere in the tree you want.
+I place mine under a third-level heading like so:
+
+* Invoices
+** An Invoice Header
+*** [2008-11-25 Tue] An Invoice Item
+*** Invoice Report
+#+BEGIN: invoice
+#+END:"
+ (interactive "P")
+ (let ((report (org-invoice-in-report-p)))
+ (when (and (not report) jump)
+ (when (re-search-forward "^#\\+BEGIN:[ \t]+invoice" nil t)
+ (org-show-entry)
+ (beginning-of-line)
+ (setq report (point))))
+ (if report (goto-char report)
+ (org-create-dblock (list :name "invoice")))
+ (org-update-dblock)))
+
+(provide 'org-invoice)