Merge branch 'maint'
[org-mode.git] / contrib / lisp / org-invoice.el
blob686889411b65cb0a00cb8631d28ffa4306eaeba5
1 ;;; org-invoice.el --- Help manage client invoices in OrgMode
2 ;;
3 ;; Copyright (C) 2008-2014 pmade inc. (Peter Jones pjones@pmade.com)
4 ;;
5 ;; This file is not part of GNU Emacs.
6 ;;
7 ;; Permission is hereby granted, free of charge, to any person obtaining
8 ;; a copy of this software and associated documentation files (the
9 ;; "Software"), to deal in the Software without restriction, including
10 ;; without limitation the rights to use, copy, modify, merge, publish,
11 ;; distribute, sublicense, and/or sell copies of the Software, and to
12 ;; permit persons to whom the Software is furnished to do so, subject to
13 ;; the following conditions:
15 ;; The above copyright notice and this permission notice shall be
16 ;; included in all copies or substantial portions of the Software.
18 ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 ;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 ;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 ;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 ;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 ;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 ;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 ;;; Commentary:
28 ;; Building on top of the terrific OrgMode, org-invoice tries to
29 ;; provide functionality for managing invoices. Currently, it does
30 ;; this by implementing an OrgMode dynamic block where invoice
31 ;; information is aggregated so that it can be exported.
33 ;; It also provides a library of functions that can be used to collect
34 ;; this invoice information and use it in other ways, such as
35 ;; submitting it to on-line invoicing tools.
37 ;; I'm already working on an elisp package to submit this invoice data
38 ;; to the FreshBooks on-line accounting tool.
40 ;; Usage:
42 ;; In your ~/.emacs:
43 ;; (autoload 'org-invoice-report "org-invoice")
44 ;; (autoload 'org-dblock-write:invoice "org-invoice")
46 ;; See the documentation in the following functions:
48 ;; `org-invoice-report'
49 ;; `org-dblock-write:invoice'
51 ;; Latest version:
53 ;; git clone git://pmade.com/elisp
54 (eval-when-compile
55 (require 'cl)
56 (require 'org))
58 (declare-function org-duration-from-minutes "org-duration" (minutes &optional fmt fractional))
60 (defgroup org-invoice nil
61 "OrgMode Invoice Helper"
62 :tag "Org-Invoice" :group 'org)
64 (defcustom org-invoice-long-date-format "%A, %B %d, %Y"
65 "The format string for long dates."
66 :type 'string :group 'org-invoice)
68 (defcustom org-invoice-strip-ts t
69 "Remove org timestamps that appear in headings."
70 :type 'boolean :group 'org-invoice)
72 (defcustom org-invoice-default-level 2
73 "The heading level at which a new invoice starts. This value
74 is used if you don't specify a scope option to the invoice block,
75 and when other invoice helpers are trying to find the heading
76 that starts an invoice.
78 The default is 2, assuming that you structure your invoices so
79 that they fall under a single heading like below:
81 * Invoices
82 ** This is invoice number 1...
83 ** This is invoice number 2...
85 If you don't structure your invoices using those conventions,
86 change this setting to the number that corresponds to the heading
87 at which an invoice begins."
88 :type 'integer :group 'org-invoice)
90 (defcustom org-invoice-start-hook nil
91 "Hook called when org-invoice is about to collect data from an
92 invoice heading. When this hook is called, point will be on the
93 heading where the invoice begins.
95 When called, `org-invoice-current-invoice' will be set to the
96 alist that represents the info for this invoice."
97 :type 'hook :group 'org-invoice)
99 (defcustom org-invoice-heading-hook nil
100 "Hook called when org-invoice is collecting data from a
101 heading. You can use this hook to add additional information to
102 the alist that represents the heading.
104 When this hook is called, point will be on the current heading
105 being processed, and `org-invoice-current-item' will contain the
106 alist for the current heading.
108 This hook is called repeatedly for each invoice item processed."
109 :type 'hook :group 'org-invoice)
111 (defvar org-invoice-current-invoice nil
112 "Information about the current invoice.")
114 (defvar org-invoice-current-item nil
115 "Information about the current invoice item.")
117 (defvar org-invoice-table-params nil
118 "The table parameters currently being used.")
120 (defvar org-invoice-total-time nil
121 "The total invoice time for the summary line.")
123 (defvar org-invoice-total-price nil
124 "The total invoice price for the summary line.")
126 (defconst org-invoice-version "1.0.0"
127 "The org-invoice version number.")
129 (defun org-invoice-goto-tree (&optional tree)
130 "Move point to the heading that represents the head of the
131 current invoice. The heading level will be taken from
132 `org-invoice-default-level' unless tree is set to a string that
133 looks like tree2, where the level is 2."
134 (let ((level org-invoice-default-level))
135 (save-match-data
136 (when (and tree (string-match "^tree\\([0-9]+\\)$" tree))
137 (setq level (string-to-number (match-string 1 tree)))))
138 (org-back-to-heading)
139 (while (and (> (org-reduced-level (org-outline-level)) level)
140 (org-up-heading-safe)))))
142 (defun org-invoice-heading-info ()
143 "Return invoice information from the current heading."
144 (let ((title (org-no-properties (org-get-heading t)))
145 (date (org-entry-get nil "TIMESTAMP" 'selective))
146 (work (org-entry-get nil "WORK" nil))
147 (rate (or (org-entry-get nil "RATE" t) "0"))
148 (level (org-outline-level))
149 raw-date long-date)
150 (unless date (setq date (org-entry-get nil "TIMESTAMP_IA" 'selective)))
151 (unless date (setq date (org-entry-get nil "TIMESTAMP" t)))
152 (unless date (setq date (org-entry-get nil "TIMESTAMP_IA" t)))
153 (unless work (setq work (org-entry-get nil "CLOCKSUM" nil)))
154 (unless work (setq work "00:00"))
155 (when date
156 (setq raw-date (apply 'encode-time (org-parse-time-string date)))
157 (setq long-date (format-time-string org-invoice-long-date-format raw-date)))
158 (when (and org-invoice-strip-ts (string-match org-ts-regexp-both title))
159 (setq title (replace-match "" nil nil title)))
160 (when (string-match "^[ \t]+" title)
161 (setq title (replace-match "" nil nil title)))
162 (when (string-match "[ \t]+$" title)
163 (setq title (replace-match "" nil nil title)))
164 (setq work (org-duration-to-minutes work))
165 (setq rate (string-to-number rate))
166 (setq org-invoice-current-item (list (cons 'title title)
167 (cons 'date date)
168 (cons 'raw-date raw-date)
169 (cons 'long-date long-date)
170 (cons 'work work)
171 (cons 'rate rate)
172 (cons 'level level)
173 (cons 'price (* rate (/ work 60.0)))))
174 (run-hook-with-args 'org-invoice-heading-hook)
175 org-invoice-current-item))
177 (defun org-invoice-level-min-max (ls)
178 "Return a list where the car is the min level, and the cdr the max."
179 (let ((max 0) min level)
180 (dolist (info ls)
181 (when (cdr (assq 'date info))
182 (setq level (cdr (assq 'level info)))
183 (when (or (not min) (< level min)) (setq min level))
184 (when (> level max) (setq max level))))
185 (cons (or min 0) max)))
187 (defun org-invoice-collapse-list (ls)
188 "Reorganize the given list by dates."
189 (let ((min-max (org-invoice-level-min-max ls)) new)
190 (dolist (info ls)
191 (let* ((date (cdr (assq 'date info)))
192 (work (cdr (assq 'work info)))
193 (price (cdr (assq 'price info)))
194 (long-date (cdr (assq 'long-date info)))
195 (level (cdr (assq 'level info)))
196 (bucket (cdr (assoc date new))))
197 (if (and (/= (car min-max) (cdr min-max))
198 (= (car min-max) level)
199 (= work 0) (not bucket) date)
200 (progn
201 (setq info (assq-delete-all 'work info))
202 (push (cons 'total-work 0) info)
203 (push (cons date (list info)) new)
204 (setq bucket (cdr (assoc date new))))
205 (when (and date (not bucket))
206 (setq bucket (list (list (cons 'date date)
207 (cons 'title long-date)
208 (cons 'total-work 0)
209 (cons 'price 0))))
210 (push (cons date bucket) new)
211 (setq bucket (cdr (assoc date new))))
212 (when (and date bucket)
213 (setcdr (assq 'total-work (car bucket))
214 (+ work (cdr (assq 'total-work (car bucket)))))
215 (setcdr (assq 'price (car bucket))
216 (+ price (cdr (assq 'price (car bucket)))))
217 (nconc bucket (list info))))))
218 (nreverse new)))
220 (defun org-invoice-info-to-table (info)
221 "Create a single org table row from the given info alist."
222 (let ((title (cdr (assq 'title info)))
223 (total (cdr (assq 'total-work info)))
224 (work (cdr (assq 'work info)))
225 (price (cdr (assq 'price info)))
226 (with-price (plist-get org-invoice-table-params :price)))
227 (unless total
228 (setq
229 org-invoice-total-time (+ org-invoice-total-time work)
230 org-invoice-total-price (+ org-invoice-total-price price)))
231 (setq total (and total (org-duration-from-minutes total)))
232 (setq work (and work (org-duration-from-minutes work)))
233 (insert-before-markers
234 (concat "|" title
235 (cond
236 (total (concat "|" total))
237 (work (concat "|" work)))
238 (and with-price price (concat "|" (format "%.2f" price)))
239 "|" "\n"))))
241 (defun org-invoice-list-to-table (ls)
242 "Convert a list of heading info to an org table"
243 (let ((with-price (plist-get org-invoice-table-params :price))
244 (with-summary (plist-get org-invoice-table-params :summary))
245 (with-header (plist-get org-invoice-table-params :headers))
246 (org-invoice-total-time 0)
247 (org-invoice-total-price 0))
248 (insert-before-markers
249 (concat "| Task / Date | Time" (and with-price "| Price") "|\n"))
250 (dolist (info ls)
251 (insert-before-markers "|-\n")
252 (mapc 'org-invoice-info-to-table (if with-header (cdr info) (cdr (cdr info)))))
253 (when with-summary
254 (insert-before-markers
255 (concat "|-\n|Total:|"
256 (org-duration-from-minutes org-invoice-total-time)
257 (and with-price (concat "|" (format "%.2f" org-invoice-total-price)))
258 "|\n")))))
260 (defun org-invoice-collect-invoice-data ()
261 "Collect all the invoice data from the current OrgMode tree and
262 return it. Before you call this function, move point to the
263 heading that begins the invoice data, usually using the
264 `org-invoice-goto-tree' function."
265 (let ((org-invoice-current-invoice
266 (list (cons 'point (point)) (cons 'buffer (current-buffer))))
267 (org-invoice-current-item nil))
268 (save-restriction
269 (org-narrow-to-subtree)
270 (org-clock-sum)
271 (run-hook-with-args 'org-invoice-start-hook)
272 (cons org-invoice-current-invoice
273 (org-invoice-collapse-list
274 (org-map-entries 'org-invoice-heading-info t 'tree 'archive))))))
276 (defun org-dblock-write:invoice (params)
277 "Function called by OrgMode to write the invoice dblock. To
278 create an invoice dblock you can use the `org-invoice-report'
279 function.
281 The following parameters can be given to the invoice block (for
282 information about dblock parameters, please see the Org manual):
284 :scope Allows you to override the `org-invoice-default-level'
285 variable. The only supported values right now are ones
286 that look like :tree1, :tree2, etc.
288 :prices Set to nil to turn off the price column.
290 :headers Set to nil to turn off the group headers.
292 :summary Set to nil to turn off the final summary line."
293 (let ((scope (plist-get params :scope))
294 (org-invoice-table-params params)
295 (zone (point-marker))
296 table)
297 (unless scope (setq scope 'default))
298 (unless (plist-member params :price) (plist-put params :price t))
299 (unless (plist-member params :summary) (plist-put params :summary t))
300 (unless (plist-member params :headers) (plist-put params :headers t))
301 (save-excursion
302 (cond
303 ((eq scope 'tree) (org-invoice-goto-tree "tree1"))
304 ((eq scope 'default) (org-invoice-goto-tree))
305 ((symbolp scope) (org-invoice-goto-tree (symbol-name scope))))
306 (setq table (org-invoice-collect-invoice-data))
307 (goto-char zone)
308 (org-invoice-list-to-table (cdr table))
309 (goto-char zone)
310 (org-table-align)
311 (move-marker zone nil))))
313 (defun org-invoice-in-report-p ()
314 "Check to see if point is inside an invoice report."
315 (let ((pos (point)) start)
316 (save-excursion
317 (end-of-line 1)
318 (and (re-search-backward "^#\\+BEGIN:[ \t]+invoice" nil t)
319 (setq start (match-beginning 0))
320 (re-search-forward "^#\\+END:.*" nil t)
321 (>= (match-end 0) pos)
322 start))))
324 (defun org-invoice-report (&optional jump)
325 "Create or update an invoice dblock report. If point is inside
326 an existing invoice report, the report is updated. If point
327 isn't inside an invoice report, a new report is created.
329 When called with a prefix argument, move to the first invoice
330 report after point and update it.
332 For information about various settings for the invoice report,
333 see the `org-dblock-write:invoice' function documentation.
335 An invoice report is created by reading a heading tree and
336 collecting information from various properties. It is assumed
337 that all invoices start at a second level heading, but this can
338 be configured using the `org-invoice-default-level' variable.
340 Here is an example, where all invoices fall under the first-level
341 heading Invoices:
343 * Invoices
344 ** Client Foo (Jan 01 - Jan 15)
345 *** [2008-01-01 Tue] Built New Server for Production
346 *** [2008-01-02 Wed] Meeting with Team to Design New System
347 ** Client Bar (Jan 01 - Jan 15)
348 *** [2008-01-01 Tue] Searched for Widgets on Google
349 *** [2008-01-02 Wed] Billed You for Taking a Nap
351 In this layout, invoices begin at level two, and invoice
352 items (tasks) are at level three. You'll notice that each level
353 three heading starts with an inactive timestamp. The timestamp
354 can actually go anywhere you want, either in the heading, or in
355 the text under the heading. But you must have a timestamp
356 somewhere so that the invoice report can group your items by
357 date.
359 Properties are used to collect various bits of information for
360 the invoice. All properties can be set on the invoice item
361 headings, or anywhere in the tree. The invoice report will scan
362 up the tree looking for each of the properties.
364 Properties used:
366 CLOCKSUM: You can use the Org clock-in and clock-out commands to
367 create a CLOCKSUM property. Also see WORK.
369 WORK: An alternative to the CLOCKSUM property. This property
370 should contain the amount of work that went into this
371 invoice item formatted as HH:MM (e.g. 01:30).
373 RATE: Used to calculate the total price for an invoice item.
374 Should be the price per hour that you charge (e.g. 45.00).
375 It might make more sense to place this property higher in
376 the hierarchy than on the invoice item headings.
378 Using this information, a report is generated that details the
379 items grouped by days. For each day you will be able to see the
380 total number of hours worked, the total price, and the items
381 worked on.
383 You can place the invoice report anywhere in the tree you want.
384 I place mine under a third-level heading like so:
386 * Invoices
387 ** An Invoice Header
388 *** [2008-11-25 Tue] An Invoice Item
389 *** Invoice Report
390 #+BEGIN: invoice
391 #+END:"
392 (interactive "P")
393 (let ((report (org-invoice-in-report-p)))
394 (when (and (not report) jump)
395 (when (re-search-forward "^#\\+BEGIN:[ \t]+invoice" nil t)
396 (org-show-entry)
397 (beginning-of-line)
398 (setq report (point))))
399 (if report (goto-char report)
400 (org-create-dblock (list :name "invoice")))
401 (org-update-dblock)))
403 (provide 'org-invoice)