emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
blob ca25711b157250de814a6e3986201c032bf7ce64 15945 bytes (raw)
name: contrib/lisp/org-invoice.el 	 # note: path name is non-authoritative(*)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
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)

debug log:

solving ca25711 ...
found ca25711 in https://git.savannah.gnu.org/cgit/emacs/org-mode.git

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).