| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 | 
							- ;;; 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)
 
 
  |