From mboxrd@z Thu Jan 1 00:00:00 1970 From: Ben Alexander Subject: org-mode and ledger cli accounting Date: Wed, 17 Dec 2008 22:29:40 +0000 Message-ID: <1E08E93A-0F48-49D6-A486-41D0E5444BE6@alexanderonline.org> Reply-To: ledger-cli-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org Mime-Version: 1.0 (Apple Message framework v929.2) Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes Content-Transfer-Encoding: quoted-printable Return-path: Sender: ledger-cli-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org List-Post: List-Help: List-Unsubscribe: , To: ledger-cli-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org, emacs-orgmode Org-Mode List-Id: emacs-orgmode.gnu.org *** org-mode and ledger cli accounting - two great tastes that taste =20 great together :PROPERTIES: :COLUMNS: %16LEDGER_DATE %5LEDGER_REFERENCE %10ITEM =20 %25LEDGER_ACCOUNT %10AMOUNT :END: ***** TODO Email the org-mode and ledger mailing lists I'm starting to use 'ledger' = (http://www.newartisans.com/software/ledger.html=20 ). There is an emacs mode for editing the native plain text file =20 format, but I really like the org-mode UI. Date handling is great =20 (imho) and column mode holds some promise (although I find it awkward =20= to add a new headline while in column mode). And remember templates =20 could ease data entry, too I think it would be great to use 'ledger' backend computation and org-=20= mode's UI together! After a quick exchange of emails where I asked about the future =20= plans of 'ledger' I was told that a new file format for 'ledger' could =20= be added, if the org-mode community decided on a common format. I'd like to suggest a format, get some feedback on it, and get =20= a sense of how many others are interested. I'm hoping to interest org-=20= mode users in ledger, and ledger users in org-mode! As a temporary measure, I've used the org-map-entries API to =20 iterate over entries looking for properties of the form "LEDGER_.*" to =20= extract the appropriate details, format them like the current plain =20 text 'ledger' file. Since there's a lot about 'ledger' I don't know (as well as =20 elisp, org-mode, git, programming in general, accounting) I'd expect =20 that my code will be ineffecient, inelegant, and dangerous. It comes =20= with no warrenty. If you have some programming skills, I'd appreciate =20= feedback on style, design, tests and documentation. For example, I'd like to use a :LEDGER: drawer instead of =20 polluting the :PROPERTIES: namespace, but then I'd miss out on the =20 easy org-entry-get API. Any suggestions? Here's an example tree, with a column-mode definition above, and a =20 single minimal entry below. Here's how to use it at the moment =20 (caveat, I wrote this assuming you already have ledger (the shell =20 program) downloaded and ledger.el in your loaded into emacs. ***** How to use the sample code and data 1. In the file bens-org-ledger.el, modify the variable bens-org-ledger-=20= file-name to point to a file you don't mind overwriting without =20 warning. (Did I mention that my code might be dangerous?) 2. Load bens-org-ledger.el 3. Run the defun (bens-org-ledger) while the current buffer contains =20 this org-tree 4. Now (find-file bens-org-ledger-file-name). In my setup, the =20 extension automaticaly loads ledger.el and the file is in ledger-mode. 5. C-c C-o C-r reg in a ledger-mode buffer (visting a native =20 ledger file) will run a basic register report. Try C-c C-o C-r bal =20 for a basic balance report. ***** Wegman's :PROPERTIES: :LEDGER: entry :LEDGER_DATE: [2008-11-07 Fri] :LEDGER_REFERENCE: chq 1001 :END: ******* :PROPERTIES: :LEDGER: transaction :LEDGER_ACCOUNT: Expenses:Food and Drink:Groceries :LEDGER_AMOUNT: -=A310.00 :END: Since all text except the headline and specific properties is ignored, I can comment my transactions with body text! ******* Assets:Checking :PROPERTIES: :LEDGER: transaction :LEDGER_DATE: [2008-11-13 Thu] :END: Instead of using the :LEDGER_ACCOUNT: property, I can use the headline. Properties come with completion (big win!) but headlines are easier to see outside of column mode. Also, in this case, the :LEDGER_DATE: property is ignored. Maybe it should become the 'effective date' in the future? *** This is my lisp code, from a file named bens-org-ledger.el It starts from the comments below to the end of the message. I'm =20 sorry to be wordy but I thought having something that could be used =20 (even awkwardly) would jump start the conversation ;; I'm naming every function I can with the bens-org-ledger prefix, ;; because these are temporary, pre-alpha attempts, intended to ;; elict comments from real programmers ;; ;; This code is copyright Ben Alexander ;; You have license to use, copy, and create derivative works ;; under the terms of the General Public License (version 2 or later). ;; (defvar bens-org-ledger-file-name "bens-org-ledger-sample.ledger" "This variable is the name of a file that will be overwritten during =20= the processing of an org-mode file. OVERWRITTEN WITHOUT WARNING!") ;; =20= except for this warning here (defun bens-org-ledger-create-entry () "this function retreives the LEDGER_DATE, LEDGER_REFERENCE (if it exists), and the LEDGER_DESCRIPTION properites of the current headline and formats them as the beginning of a new ledger entry" (concat "\n" ;; a blank line separates ledger entries (format-time-string "%Y/%m/%d" (org-time-string-to-time (org-entry-get (point) =20 "LEDGER_DATE") ) t) ;; Wouldn't it be nice if the timestamps repeat intervals were =20= converted ;; to appropriate ledger syntax here? Does org-mode have some =20= timestamp ;; property retrievals I could use? ;; ;; Also, I need to add support for "effective dates" (if (> (length (org-entry-get (point) "LEDGER_REFERENCE")) 0) (concat " (" (org-entry-get (point) "LEDGER_REFERENCE") ") ") " ") (or (org-entry-get (point) "LEDGER_DESCRIPTION") (org-get-=20 heading)) "\n" ;; I assume the cursor starts on a newline -- make it so! ) ) (defun bens-org-ledger-create-transaction () "this function retreives the LEDGER_ACCOUNT (or headline), =20 LEDGER_AMOUNT and formats them as a continuation of the existing ledger entry" (concat " " ;; ledger transactions must begin with whitespace (or (org-entry-get (point) "LEDGER_ACCOUNT") (org-get-heading)) " " ;; two spaces separate account name from amount (org-entry-get (point) "LEDGER_AMOUNT") "\n" ;; I assume the cursor starts on a newline -- make it so! ) ) (defun bens-org-ledger-map-entries-helper () "this function passed to org-map-entries to format headlines to =20 ledger style plain text. It chooses bens-org-ledger-create-entry or bens-org-ledger-create-transation based on the LEDGER property. =20 Those functions return the actual data that org-map-entries collects" (cond ((string=3D (org-entry-get (point) "LEDGER") "entry") (bens-org-=20= ledger-create-entry)) ((string=3D (org-entry-get (point) "LEDGER") "transaction") = (bens-=20 org-ledger-create-transaction)) )) (defun bens-org-ledger () "this function generates a properly formatted ledger file from the =20= org-mode headlines based on properties stored in the org-mode property =20= drawer. The file refered to by bens-org-ledger-file-name is =20 OVERWRITTEN WITHOUT WARNING. (you have been warned, again) The properties used are of the form LEDGER_.* :LEDGER: entry|transaction =3D> entries (headings) contain transactions = =20 (sub-headings) For ledger entries, the following properties are used :LEDGER: entry =3D> Means this is the beginning of a new ledger =20= entry. Entries (in the parlance of ledger) may =20 contain multiple transactions :LEDGER_DATE: <2009-01-01 Thu> =3D> An org-mode timestamp. :LEDGER_REFERENCE: chq 101 =3D> There's a place for this in the ledger =20= syntax. Any string is legal. :LEDGER_DESCRIPTION: =3D> if this is missing, the org-mode headline is = =20 used For ledger transactions, the follwoing properties are used :LEDGER: transaction =3D> Means this headline is a transaction. It should be a sub-heading of some 'entry' :LEDGER_ACCOUNT: =3D> a colon:delimited:account:as:used:by:ledger :LEDGER_AMOUNT: =A310 =3D> Any legal amount for ledger. Any currency symbol or code is considered =20 legal, e.g. :LEDGER_AMOUNT: 10 GBP :LEDGER_AMOUNT: $10 :LEDGER_AMOUNT: 10 STICKS_OR_STONES " (interactive) (let ((ledger-text (org-map-entries 'bens-org-ledger-map-entries-=20 helper "+LEDGER=3D{transaction\\\|entry}"))) (with-temp-file bens-org-ledger-file-name (dolist (str ledger-text) =20= (insert str))) ))