--- /dev/null
+;; emoney.el --- A home finance package.
+
+;; Copyright (C) 2003 - 2009 Steve Youngs
+
+;; Author: Steve Youngs <steve@sxemacs.org>
+;; Maintainer: Steve Youngs <steve@sxemacs.org>
+;; Created: <2003-06-04>
+;; Keywords: money finance banking cash
+
+;; This file is part of eMoney.
+
+;; Redistribution and use in source and binary forms, with or without
+;; modification, are permitted provided that the following conditions
+;; are met:
+;;
+;; 1. Redistributions of source code must retain the above copyright
+;; notice, this list of conditions and the following disclaimer.
+;;
+;; 2. Redistributions in binary form must reproduce the above copyright
+;; notice, this list of conditions and the following disclaimer in the
+;; documentation and/or other materials provided with the distribution.
+;;
+;; 3. Neither the name of the author nor the names of any contributors
+;; may be used to endorse or promote products derived from this
+;; software without specific prior written permission.
+;;
+;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
+;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+;; DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+;;; Commentary:
+;;
+;; eMoney is based on balance.el, originally by Jason Baietto
+;; <jason@ssd.csd.harris.com> and later maintained by Bob Newell
+;; <bnewell@alum.mit.edu>.
+;;
+;; eMoney tries to give the user a reasonably self-contained finance
+;; solution. The target audience is the home user and possibly the
+;; small (very small) business operator. It probably will never be
+;; compatible with any commercial finance packages available.
+;;
+;; Right now, eMoney is nothing more than a very simple cash book
+;; program. Use it to balance your cheque book. See the "Todo:"
+;; section for a partial list of possible upcoming features.
+;;
+;; Installation (from source):
+;;
+;; tar zxf emoney-x.xx.tar.gz
+;; cd emoney-x.xx
+;; check the paths in Makefile
+;; make
+;; make install (you may need to be root for this)
+;;
+;; Installation (from XEmacs package tarball):
+;;
+;; cd /usr/local/share/sxemacs/site-packages
+;; (/usr/local/lib/xemacs/site-packages for XEmacs)
+;; tar zxf /path/to/emoney-x.xx-pkg.tar.gz
+;;
+;; (Re)start (S)XEmacs and do `M-x emoney RET'
+;;
+;; You can also invoke `emoney-mode' by simply visiting a file with a
+;; `.emy' extension.
+;;
+;; Please note that I use SXEmacs exclusively, I have no idea whether
+;; or not this will run or even byte-compile with GNU/Emacs (it _will_
+;; work fine with XEmacs). Also, I have no desire or intentions of
+;; making this package portable between XEmacs and GNU/Emacs. Harsh
+;; words? No, not really, I simply don't have the time or resources to
+;; invest in the extra work involved. And also there is no way that
+;; I'd be able to support GNU/Emacs users because I'm not familiar with
+;; that flavour of Emacs. If you want to port it, be my guest, this is
+;; an Open Source project.
+;;
+;; Another note: This package uses correct English spelling wherever
+;; possible. For example, "cheque" instead of "check", "summarise"
+;; instead of "summarize". Although I welcome and encourage _ALL_
+;; contributions, patches that s/que/ch/ or s/ise/ize/ will go straight
+;; to /dev/null.
+
+;;; ChangeLog:
+;;
+;; This is just a place holder so `emoney-commentary' will work
+;; properly. See the ChangeLog file for changes.
+
+;;; Code:
+
+;; Drag in what we need.
+(eval-and-compile
+ (autoload 'calc "calc" nil t)
+ (autoload 'clear-rectangle "rect" nil t)
+ (autoload 'completing-read "minibuf")
+ (autoload 'customize-group "cus-edit")
+ (autoload 'lm-commentary "lisp-mnt")
+ (autoload 'sort-numeric-fields "sort" nil t)
+ (autoload 'untabify "tabify" nil t)
+ (autoload 'with-electric-help "ehelp")
+ (require 'wid-edit))
+
+(eval-when-compile
+ (autoload 'eval-when "cl-macs" nil nil 'macro)
+ (autoload 'calc-quit "calc" nil t)
+ (require 'advice)
+ (defvar calc-was-split)
+ (autoload 'browse-url "browse-url" nil t)
+ (autoload 'regexp-opt "regexp-opt"))
+
+;; Custom.
+(defgroup emoney nil
+ "Customisations for `emoney-mode'."
+ :prefix "emoney-"
+ :group 'tools)
+
+(defcustom emoney-accounts-buffer-width 30
+ "How wide in columns for the accounts buffer."
+ :type 'integer
+ :group 'emoney)
+
+(defcustom emoney-accounts-buffer-height 9
+ "How high in lines for the accounts buffer."
+ :type 'integer
+ :group 'emoney)
+
+(defcustom emoney-header-buffer-height 4
+ "How high in lines for the header buffer."
+ :type 'integer
+ :group 'emoney)
+
+(defcustom emoney-accounts-directory (file-name-as-directory
+ (expand-file-name ".emoney"
+ (user-home-directory)))
+ "*The directory where your eMoney account files are located."
+ :type 'directory
+ :group 'emoney)
+
+(defcustom emoney-history-directory (file-name-as-directory
+ (expand-file-name "history"
+ emoney-accounts-directory))
+ "*Directory containing previous years account files."
+ :type 'directory
+ :group 'emoney)
+
+(defcustom emoney-chart-of-accounts
+ (if (file-directory-p emoney-accounts-directory)
+ (directory-files emoney-accounts-directory nil "\\.emy$" nil t)
+ nil)
+ "*A list of eMoney accounts in `emoney-accounts-directory'."
+ :type '(repeat
+ (string :tag "Account Name"))
+ :group 'emoney)
+
+(defcustom emoney-default-account (or (car emoney-chart-of-accounts)
+ "default.emy")
+ "*The default eMoney account to use.
+
+This is the account that has the focus when you start eMoney."
+ :type 'string
+ :group 'emoney)
+
+(defcustom emoney-credit-transaction-types
+ '("autotellcr" "atmcr" "bankcredit" "bcr" "deposit" "dep" "directcr"
+ "dcr" "internetcr" "netcr" "phonecr" "phcr")
+ "*List of valid credit transaction types.
+
+The default types are:
+
+ autotellcr -- Automatic Teller Machine deposit
+ atmcr -- short version of 'autotellcr'
+
+ bankcredit -- Bank initiated credits like interest etc
+ bcr -- short version of 'bankcredit'
+
+ deposit -- Manual, non-direct deposits
+ dep -- short version of 'deposit'
+
+ directcr -- Direct credit transactions
+ dcr -- short version of 'directcr'
+
+ internetcr -- Internet credit transactions
+ netcr -- short version of 'internetcr'
+
+ phonecr -- Telephone credit transactions
+ phcr -- short version of 'phonecr'"
+ :type '(repeat string)
+ :group 'emoney)
+
+(defcustom emoney-debit-transaction-types
+ '("autotelldb" "atmdb" "bankfee" "fee" "directdb" "ddb" "eftpos"
+ "eft" "internetdb" "netdb" "phonedb" "phdb" "withdrawal" "wdl")
+ "*List of valid debit transaction types.
+
+The default types are:
+
+ autotelldb -- Automatic Teller Machine transaction
+ atmdb -- short version of 'autoteller'
+
+ bankfee -- Bank fees
+ fee -- short version of 'bankfee'
+
+ directdb -- Direct debit transactions
+ ddb -- short version of 'directdb'
+
+ eftpos -- Electronic Funds Transfer Point Of Sale
+ eft -- short version of 'eftpos'
+
+ internetdb -- Internet transactions
+ netdb -- short version of 'internet'
+
+ phonedb -- Telephone transactions
+ phcr -- short version of 'phone'
+
+ withdrawal -- Over the counter withdrawal.
+ wdl -- short version of 'withdrawal'"
+ :type '(repeat string)
+ :group 'emoney)
+
+(defcustom emoney-cr-transfer-transaction-type "xfrcr"
+ "*Credit transfer transaction types."
+ :type 'string
+ :group 'emoney)
+
+(defcustom emoney-db-transfer-transaction-type "xfrdb"
+ "*Debit transfer transaction type."
+ :type 'string
+ :group 'emoney)
+
+(defcustom emoney-date-format "%Y-%m-%d"
+ "*The format that `emoney-mode' uses for dates."
+ :type '(choice
+ (const :tag "yyyy-mm-dd"
+ :value "%Y-%m-%d")
+ (const :tag "yyyy/mm/dd"
+ :value "%Y/%m/%d")
+ (const :tag "mm/dd/yy"
+ :value "%m/%d/%y")
+ (const :tag "mm/dd/yyyy"
+ :value "%m/%d/%Y")
+ (const :tag "mm-dd-yy"
+ :value "%m-%d-%y")
+ (const :tag "mm-dd-yyyy"
+ :value "%m-%d-%Y")
+ (const :tag "dd/mm/yy"
+ :value "%d/%m/%y")
+ (const :tag "dd/mm/yyyy"
+ :value "%d/%m/%Y")
+ (const :tag "dd-mm-yy"
+ :value "%d-%m-%y")
+ (const :tag "dd-mm-yyyy"
+ :value "%d-%m-%Y")
+ (const :tag "yy/mm/dd"
+ :value "%y/%m/%d")
+ (const :tag "yy-mm-dd"
+ :value "%y-%m-%d"))
+ :group 'emoney)
+
+(defcustom emoney-uk-cheque-spelling t
+ "*When non-nil, use UK spelling: \"chq\" instead of \"chk\"."
+ :type 'boolean
+ :group 'emoney)
+
+(defcustom emoney-save-after-recalculate t
+ "*If non-nil, save the buffer after a recalculate.
+
+See `emoney-recalculate-buffer'."
+ :type 'boolean
+ :group 'emoney)
+
+(defcustom emoney-recalculate-on-quit nil
+ "*If non-nil, recalculate each eMoney account buffer when quitting eMoney."
+ :type 'boolean
+ :group 'emoney)
+
+(defcustom emoney-use-new-frame nil
+ "*If non-nil, eMoney will start in a new frame."
+ :type 'boolean
+ :group 'emoney)
+
+(defcustom emoney-bank-url "unset"
+ "The URL of your bank's web page."
+ :type 'string
+ :group 'emoney)
+
+;; Hooks
+(defgroup emoney-hooks nil
+ "Various hooks for eMoney."
+ :prefix "emoney-"
+ :group 'emoney)
+
+(defcustom emoney-mode-hooks nil
+ "*Hooks run after `emoney-mode' is entered."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-switch-account-hook nil
+ "*Hooks run after switching accounts."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-setup-accounts-buffer-hook nil
+ "*Hooks run after setting up the accounts buffer."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-setup-header-buffer-hook nil
+ "*Hooks run after setting up the header buffer."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-setup-control-buffer-hook nil
+ "*Hooks run after setting up the control buffer."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-transaction-hook nil
+ "*Hooks run after appending a new transaction.
+
+These hooks are run after any new transaction, including cheque
+and transfers. If you want to do additional things with cheque or
+transfer transactions, see `emoney-transaction-cheque-hook' &
+`emoney-transaction-transfer-hook'."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-transaction-cheque-hook nil
+ "*Hooks run after appending a cheque transaction."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-transaction-transfer-hook nil
+ "*Hooks run after appending a transfer transaction."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-recalculate-before-hook nil
+ "*Hooks run just prior to recalculating an eMoney buffer.
+
+See `emoney-recalculate-after-hook' for doing things after recalculating."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-recalculate-after-hook nil
+ "*Hooks run just after recalculating an eMoney buffer.
+
+See `emoney-recalculate-before-hook' for doing things before recalculating."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-summarise-cheques-hook nil
+ "*Hooks run after doing `emoney-summarise-cheques'."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-new-account-hook nil
+ "*Hooks run after creating a new account."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-quit-before-hook nil
+ "*Hooks run just prior to eMoney exiting."
+ :type 'hook
+ :group 'emoney-hooks)
+
+(defcustom emoney-quit-after-hook nil
+ "*Hooks run as the last thing when eMoney exits."
+ :type 'hook
+ :group 'emoney-hooks)
+
+;; Faces
+(defgroup emoney-faces nil
+ "eMoney faces."
+ :prefix "emoney-"
+ :group 'emoney)
+
+(make-face 'emoney-account-name-face)
+(set-face-parent 'emoney-account-name-face 'font-lock-variable-name-face)
+
+(make-face 'emoney-debit-face)
+(set-face-parent 'emoney-debit-face 'font-lock-warning-face)
+
+(make-face 'emoney-credit-face)
+(set-face-parent 'emoney-credit-face 'font-lock-function-name-face)
+
+(make-face 'emoney-date-face)
+(set-face-parent 'emoney-date-face 'font-lock-keyword-face)
+
+(make-face 'emoney-clear-tran-face)
+(set-face-parent 'emoney-clear-tran-face 'font-lock-string-face)
+
+(make-face 'emoney-unclear-tran-face)
+(set-face-parent 'emoney-unclear-tran-face 'font-lock-comment-face)
+
+(make-face 'emoney-header-face)
+(set-face-parent 'emoney-header-face 'font-lock-comment-face)
+
+(defcustom emoney-account-name-face 'emoney-account-name-face
+ "Face for highlighting eMoney account names."
+ :type 'face
+ :group 'emoney-faces)
+
+(defcustom emoney-debit-face 'emoney-debit-face
+ "Face for highlighting debit amounts in eMoney."
+ :type 'face
+ :group 'emoney-faces)
+
+(defcustom emoney-credit-face 'emoney-credit-face
+ "Face for highlighting credit amounts in eMoney."
+ :type 'face
+ :group 'emoney-faces)
+
+(defcustom emoney-date-face 'emoney-date-face
+ "Face for highlighting dates in eMoney."
+ :type 'face
+ :group 'emoney-faces)
+
+(defcustom emoney-clear-tran-face 'emoney-clear-tran-face
+ "Face for highlighting cleared transactions in eMoney."
+ :type 'face
+ :group 'emoney-faces)
+
+(defcustom emoney-unclear-tran-face 'emoney-unclear-tran-face
+ "Face for highlighting uncleared transactions in eMoney."
+ :type 'face
+ :group 'emoney-faces)
+
+(defcustom emoney-header-face 'emoney-header-face
+ "Face for highlighting the column header in eMoney."
+ :type 'face
+ :group 'emoney-faces)
+
+;;; Internal variables
+(defconst emoney-codename "Equity"
+ "The codename of the current version of eMoney.")
+
+(defconst emoney-is-beta t
+ "Non-nil if the current version of eMoney is beta.")
+
+(eval-when (load eval)
+ (unless (file-directory-p emoney-accounts-directory)
+ (make-directory-path emoney-accounts-directory)))
+
+(require 'emoney-version)
+
+;;;###autoload
+(defun emoney-version (&optional arg)
+ "*Display the current version information for eMoney.
+
+Optional prefix Argument ARG, print version information at point
+in the current buffer."
+ (interactive "P")
+ (if emoney-is-beta
+ (if arg
+ (insert (format "eMoney: %s, \"%s\" [Beta]"
+ emoney-version emoney-codename))
+ (message (format "eMoney: %s, \"%s\" [Beta]"
+ emoney-version emoney-codename)))
+ (if arg
+ (insert (format "eMoney: %s, \"%s\""
+ emoney-version emoney-codename))
+ (message (format "eMoney: %s, \"%s\""
+ emoney-version emoney-codename)))))
+
+;;;###autoload
+(defun emoney-commentary ()
+ "*Display the commentary section of emoney.el."
+ (interactive)
+ (with-electric-help
+ '(lambda ()
+ (insert
+ (with-temp-buffer
+ (erase-buffer)
+ (insert (lm-commentary (locate-library "emoney.el")))
+ (goto-char (point-min))
+ (while (re-search-forward "^;+ ?" nil t)
+ (replace-match "" nil nil))
+ (buffer-string (current-buffer)))))
+ "*eMoney Commentary*"))
+
+;;;###autoload
+(defun emoney-copyright ()
+ "*Display the copyright notice for eMoney."
+ (interactive)
+ (with-electric-help
+ '(lambda ()
+ (insert
+ (with-temp-buffer
+ (erase-buffer)
+ (insert-file-contents (locate-library "emoney.el"))
+ (goto-char (point-min))
+ (re-search-forward ";;; Commentary" nil t)
+ (beginning-of-line)
+ (narrow-to-region (point-min) (point))
+ (while (re-search-backward "^;+ ?" nil t)
+ (replace-match "" nil nil))
+ (buffer-string (current-buffer)))))
+ "*eMoney Copyright Notice*"))
+
+(defvar emoney-frame nil
+ "The frame where eMoney is displayed, if in a new frame.")
+
+(defconst emoney-accounts-buffer "*eMoney A/C's*"
+ "The buffer that holds the list of eMoney accounts.")
+
+(defconst emoney-control-buffer "*eMoney Control*"
+ "The buffer containing the eMoney control buttons.")
+
+(defconst emoney-header-buffer "*eMoney Header*"
+ "The buffer for the eMoney account register header line.")
+
+(defvar emoney-current-account-name
+ (file-name-sans-extension emoney-default-account))
+
+(defun emoney-switch-to-account (&optional account)
+ "Switch to account, ACCOUNT."
+ (interactive)
+ (let ((switch-acc (or account
+ (emoney-completing-read "Switch to A/C: "
+ emoney-chart-of-accounts nil t))))
+ (select-window (get-buffer-window
+ (concat emoney-current-account-name ".emy")))
+ (switch-to-buffer switch-acc)
+ (goto-char (point-max))
+ (setq emoney-current-account-name
+ (file-name-sans-extension switch-acc))
+ (select-window (get-buffer-window emoney-header-buffer))
+ (emoney-setup-header-buffer)
+ (switch-to-buffer emoney-header-buffer)
+ (select-window (get-buffer-window
+ (concat emoney-current-account-name ".emy")))
+ (run-hooks 'emoney-switch-account-hook)))
+
+(defun emoney-mouse-switch-to-account (event)
+ "Switch to account under EVENT."
+ (interactive "e")
+ (save-excursion
+ (set-buffer (window-buffer (event-window event)))
+ (let ((switch-acc (extent-string (extent-at-event event))))
+ (select-window (get-buffer-window
+ (concat emoney-current-account-name ".emy")))
+ (switch-to-buffer (concat switch-acc ".emy"))
+ (goto-char (point-max))
+ (setq emoney-current-account-name switch-acc)
+ (select-window (get-buffer-window emoney-header-buffer))
+ (emoney-setup-header-buffer)
+ (switch-to-buffer emoney-header-buffer)))
+ (select-window (get-buffer-window
+ (concat emoney-current-account-name ".emy")))
+ (run-hooks 'emoney-switch-account-hook))
+
+(defconst emoney-accounts-buffer-map
+ (let* ((map (make-sparse-keymap 'emoney-accounts-buffer-map)))
+ (define-key map [button2] #'emoney-mouse-switch-to-account)
+ map)
+ "A keymap for the extents in eMoney Accounts buffer.")
+
+(defconst emoney-largest-balance "999999999.99"
+ "This is only used for formatting purposes.")
+
+(defun emoney-setup-accounts-buffer ()
+ "Set up the eMoney \"Accounts\" buffer."
+ (let ((buf emoney-accounts-buffer)
+ (accounts emoney-chart-of-accounts)
+ help-msg cbal)
+ (save-excursion
+ (when (buffer-live-p (get-buffer-create buf))
+ (kill-buffer buf))
+ (set-buffer (get-buffer-create buf))
+ (while accounts
+ (setq help-msg (concat "Switch to A/C: "
+ (file-name-sans-extension (car accounts))))
+ (insert (file-name-sans-extension (car accounts)))
+ (set-extent-properties
+ (make-extent (point-at-bol) (point))
+ `(face emoney-account-name-face
+ mouse-face highlight
+ help-echo ,help-msg
+ balloon-help ,help-msg
+ keymap ,emoney-accounts-buffer-map))
+ (with-current-buffer (car accounts)
+ (goto-char (point-max))
+ (re-search-backward "\\s-[\\+-=]\\s-" nil t)
+ (setq cbal (nth 2 (emoney-parse-transaction-data
+ (buffer-substring (point) (point-at-eol))))))
+ (move-to-column 13 'force)
+ (if cbal
+ (insert-face
+ (format
+ (concat "%"
+ (number-to-string (- (length emoney-largest-balance)
+ (length (format "%.2f" cbal))))
+ "s$%.2f") "" cbal)
+ (if (< cbal 0)
+ 'emoney-debit-face
+ 'emoney-credit-face))
+ (insert " Needs Recalc"))
+ (insert "\n")
+ (setq cbal nil)
+ (setq accounts (cdr accounts)))
+ (set-specifier horizontal-scrollbar-visible-p nil (current-buffer))
+ (set-specifier has-modeline-p nil (current-buffer))
+ (run-hooks 'emoney-setup-accounts-buffer-hook))))
+
+(defconst emoney-header
+ "Date Type C Description Amount Balance
+===============================================================================\n"
+ "The header inserted at the top of a `emoney-mode' buffer.")
+
+(defun emoney-setup-header-buffer ()
+ "Set up the eMoney \"Account Register Header\" buffer."
+ (let ((buf emoney-header-buffer))
+ (save-excursion
+ (when (buffer-live-p (get-buffer-create buf))
+ (kill-buffer buf))
+ (set-buffer (get-buffer-create buf))
+ (insert-face emoney-current-account-name 'emoney-account-name-face)
+ (insert "\n\n")
+ (insert-face emoney-header 'emoney-header-face)
+ (set-specifier horizontal-scrollbar-visible-p nil (current-buffer))
+ (set-specifier vertical-scrollbar-visible-p nil (current-buffer))
+ (set-specifier has-modeline-p nil (current-buffer))
+ (goto-char (point-min))
+ (run-hooks 'emoney-setup-header-buffer-hook))))
+
+;;;###autoload
+(defun emoney-customise ()
+ "*Convenience function to customise eMoney."
+ (interactive)
+ (customize-group 'emoney))
+
+;; Because lots of people in the world can't spell.
+;;;###autoload
+(defalias 'emoney-customize 'emoney-customise)
+
+(defvar emoney-transaction-types '("init" "")
+ "A list of valid transaction types.
+
+It is a combination of `emoney-credit-transaction-types',
+`emoney-debit-transaction-types', and a blank type.")
+
+(defconst emoney-cheque-type
+ (if emoney-uk-cheque-spelling
+ "chq[ \t]+\\([0-9]+\\)"
+ "chk[ \t]+\\([0-9]+\\)")
+ "Type field for cheque transactions.
+
+This is defined separately from the other transaction types because
+it is used by functions that perform special operations on cheque.
+transactions.")
+
+(defconst emoney-date-column 0
+ "Column where transaction date begins.")
+
+(defconst emoney-type-column 11
+ "Column where transaction type begins.")
+
+(defconst emoney-clear-column 22
+ "Column where status appears.")
+
+(defconst emoney-description-column 24
+ "Column where transaction description begins.")
+
+(defconst emoney-sign-column 52
+ "Column where transaction sign begins.")
+
+(defconst emoney-amount-column 54
+ "Column where transaction amount begins.")
+
+(defconst emoney-current-balance-column 65
+ "Column where current emoney begins.")
+
+(defconst emoney-tab-stop-list
+ (list
+ emoney-date-column
+ emoney-type-column
+ emoney-clear-column
+ emoney-description-column
+ emoney-sign-column
+ emoney-amount-column
+ emoney-current-balance-column)
+ "List of tab stops that define the start of all transaction fields.")
+
+(defvar emoney-mode-map
+ (let ((map (make-sparse-keymap 'emoney-mode-map)))
+ (define-key map [(control c) (control b)] #'emoney-backward-field)
+ (define-key map [(control c) (control c)] #'emoney-recalculate-buffer)
+ (define-key map [(control c) (control d)] #'emoney-clear-current-field)
+ (define-key map [(control c) (control f)] #'emoney-forward-field)
+ (define-key map [(control c) (control n)] #'emoney-append-next-cheque)
+ (define-key map [(control c) (control r)] #'emoney-summarise-cheques-region)
+ (define-key map [(control c) (control s)] #'emoney-summarise-cheques-buffer)
+ (define-key map [(control c) (control t)] #'emoney-append-transaction)
+ (define-key map [(control c) (control x)] #'emoney-transfer-funds)
+ (define-key map [tab] #'emoney-forward-field)
+ (define-key map [(control c) b] #'emoney-go-to-bank)
+ (define-key map [(control c) c] #'emoney-calc)
+ (define-key map [(control c) s] #'emoney-switch-to-account)
+ (define-key map [(control c) q] #'emoney-quit)
+ (define-key map [(control c) (control q)] #'emoney-recalc-and-exit)
+ map)
+ "Keymap for emoney buffer.")
+
+(defconst emoney-mode-menu
+ '("eMoney"
+ ["New A/C" emoney-new-account t]
+ "---"
+ [(concat "New "
+ (if emoney-uk-cheque-spelling
+ "Cheque "
+ "Check ")
+ "Transaction") emoney-append-next-cheque t]
+ ["New Transaction" emoney-append-transaction t]
+ ["Transfer Funds" emoney-transfer-funds t]
+ "---"
+ ["Next Field" emoney-forward-field t]
+ ["Previous Field" emoney-backward-field t]
+ ["Clear Current Field" emoney-clear-current-field t]
+ "---"
+ [(concat "Summary of "
+ (if emoney-uk-cheque-spelling
+ "cheques "
+ "checks ")
+ "(buffer)") emoney-summarise-cheques-buffer t]
+ [(concat "Summary of "
+ (if emoney-uk-cheque-spelling
+ "cheques "
+ "checks ")
+ "(region)") emoney-summarise-cheques-region t]
+ "---"
+ ["Recalculate Buffer" emoney-recalculate-buffer t]
+ "---"
+ ["Go To The Bank!" emoney-go-to-bank t]
+ ["Calculator" emoney-calc t]
+ "---"
+ ["Recalc All A/C's and Exit" emoney-recalc-and-exit t]
+ ["Exit eMoney" emoney-quit t])
+ "Menu for `emoney-mode' buffers.")
+
+(easy-menu-define
+ emoney-mode-easymenu nil "eMoney" emoney-mode-menu)
+
+(defvar emoney-credit-type-keywords
+ (regexp-opt (append emoney-credit-transaction-types
+ (list emoney-cr-transfer-transaction-type)))
+ "eMoney font lock keywords for credit tran types")
+
+(defvar emoney-debit-type-keywords
+ (regexp-opt (append emoney-debit-transaction-types
+ (list emoney-db-transfer-transaction-type)))
+ "eMoney font lock keywords for debit tran types")
+
+(defvar emoney-font-lock-keywords
+ `(("x\\s-\\(.*\\)[\\+-]\\s-" (1 emoney-clear-tran-face))
+ ("o\\s-\\(.*\\)[\\+-]\\s-" (1 emoney-unclear-tran-face))
+ (,emoney-credit-type-keywords . emoney-credit-face)
+ (,emoney-debit-type-keywords . emoney-debit-face)
+ ("\\(^[0-9]+\\(-\\|\\/\\)[0-9]+\\(-\\|\\/\\)[0-9]+\\)"
+ (1 emoney-date-face))
+ ("-\\s-+\\([0-9]+\\.[0-9][0-9]\\)" (1 emoney-debit-face))
+ ("-[0-9]+\\.[0-9]+" . emoney-debit-face)
+ ("[^-]\\([0-9]+\\.[0-9]+$\\)" (1 emoney-credit-face))
+ ("\\+\\s-+\\([0-9]+\\.[0-9][0-9]\\)" (1 emoney-credit-face)))
+ "Font lock keywords for `emoney-mode'.")
+
+
+;;;###autoload
+(defun emoney-mode ()
+ "Major mode for editing a buffer containing financial transactions.
+The following bindings provide the main functionality of this mode:
+
+\\{emoney-mode-map}
+
+Transactions occur on a single line and have the following fields (in
+order):
+
+ date The transaction date. See `emoney-date-format'.
+ type This field must either be blank or match one of the
+ expressions defined in `emoney-credit-transaction-types'
+ and `emoney-debit-transaction-types'.
+ clear Status of transaction, 'o' is open, 'x' is cleared. New
+ transactions default to 'o'.
+ description A possibly blank transaction description.
+ sign This field must either be '+', '-' or '='. '+' means
+ credit, '-' means debit, and '=' resets balance. eMoney
+ will usually guess the correct sign to use.
+ amount The transaction amount.
+ balance The balance after this transaction. This field will be
+ computed upon recalculation, you _don't_ need to fill it
+ in. Just do: \\[emoney-recalculate-buffer].
+
+Any line in the buffer that does not begin with a date will be
+considered a comment and ignored. Among other things, this allows
+the transaction description to span several lines.
+
+Changing any amount and recalculating again will update all visible
+balances. Transactions may be commented out by putting a semi-colon
+\(or any other non-numerical character\) at the beginning of the line.
+
+Entering `emoney-mode' runs the `emoney-mode-hooks' if any exist."
+ (interactive)
+ (kill-all-local-variables)
+ (setq major-mode 'emoney-mode)
+ (setq mode-name "eMoney")
+ (use-local-map emoney-mode-map)
+ (easy-menu-add emoney-mode-easymenu)
+ (make-local-variable 'tab-stop-list)
+ (setq tab-stop-list emoney-tab-stop-list)
+ (make-local-variable 'indent-tabs-mode)
+ (setq indent-tabs-mode nil)
+ (setq indent-line-function 'emoney-forward-field)
+ (overwrite-mode 1)
+ (run-hooks 'emoney-mode-hooks))
+
+(defun emoney-current-line ()
+ "Return the current buffer line at point."
+ (save-excursion
+ (beginning-of-line)
+ (count-lines (point-min) (point))))
+
+(defun emoney-completing-read (prompt table &optional predicate require-match
+ initial-contents history default)
+ "Like `completing-read', but also accepts strings.
+
+Arguments PROMPT, TABLE, PREDICATE, REQUIRE-MATCH, INITIAL-CONTENTS,
+HISTORY, DEFAULT are as per `completing-read'."
+ (completing-read
+ prompt
+ (if (vectorp table)
+ table
+ (mapcar 'list table))
+ predicate require-match initial-contents history default))
+
+(defun emoney-forward-field ()
+ "Move the cursor to the next field on the current line."
+ (interactive)
+ (move-to-tab-stop))
+
+(defun emoney-go-to-bank (&optional url)
+ "Open your bank's URL with `browse-url'."
+ (interactive)
+ (let ((url (or url
+ emoney-bank-url)))
+ (if (or (equal url "unset")
+ (not url))
+ (message-or-box "Please customise `emoney-bank-url'.")
+ (browse-url url))))
+
+(defun emoney-calc ()
+ "Wrapper around `calc' to get around \"window-edges bug\"."
+ (interactive)
+ (add-hook 'calc-end-hook #'(lambda ()
+ (setq calc-was-split nil)))
+ (calc))
+
+(defun emoney-last (list)
+ "Return last element in LIST."
+ (cond
+ ((null list)
+ '())
+ ((null (cdr list))
+ (car list))
+ (t (emoney-last (cdr list)))))
+
+(defun emoney-find-largest-less-than (list item)
+ "Search a sorted LIST of numbers, return the largest number that is < ITEM."
+ (let ((list-car (car list))
+ (list-cdr (cdr list))
+ (last nil))
+ (while (and list-car (< list-car item))
+ (setq last list-car)
+ (setq list-car (car list-cdr))
+ (setq list-cdr (cdr list-cdr)))
+ last))
+
+(defun emoney-find-largest-less-than-equal (list item)
+ "Return cdr of LIST starting @ the largest number that is <= to ITEM."
+ (let ((list-car (car list))
+ (list-cdr (cdr list))
+ (last nil))
+ (while (and list-car (<= list-car item))
+ (setq last (cons list-car list-cdr))
+ (setq list-car (car list-cdr))
+ (setq list-cdr (cdr list-cdr)))
+ last))
+
+(defun emoney-find-field (column)
+ "Return a list of the start and end of the field around COLUMN.
+
+End may be nil if column is after the last defined tab stop."
+ (let ((field (emoney-find-largest-less-than-equal
+ emoney-tab-stop-list column)))
+ (if (equal 1 (length field))
+ (list (car field) nil)
+ (list (nth 0 field) (nth 1 field)))))
+
+(defun emoney-backward-field ()
+ "Move the cursor to the previous entry field on the current line."
+ (interactive)
+ (let* ((col (current-column))
+ (prev (emoney-find-largest-less-than emoney-tab-stop-list col)))
+ (if prev
+ (move-to-column prev)
+ (move-to-column (emoney-last emoney-tab-stop-list)))))
+
+(defun emoney-clear-current-field ()
+ "Fill the field around point with spaces, leave point at start of field."
+ (interactive)
+ (let* ((field (emoney-find-field (current-column)))
+ (line-start (progn (beginning-of-line) (point)))
+ (line-end (progn
+ (end-of-line)
+ (untabify line-start (point))
+ (point)))
+ (field-start (+ line-start (nth 0 field)))
+ (field-end (if (nth 1 field)
+ (+ line-start (nth 1 field))
+ line-end)))
+ (clear-rectangle field-start field-end)
+ (goto-char field-start)))
+
+(defsubst emoney-build-types-list ()
+ "Dynamically build a list of transaction types.
+
+This is done dynamically so that the user can change or add to the
+list of transaction types without having to reload eMoney."
+ ;; Initialise to ("init" "").
+ (setq emoney-transaction-types '("init" ""))
+ ;; Load the debit and credit transaction types.
+ (setq emoney-transaction-types
+ (append emoney-transaction-types
+ emoney-debit-transaction-types
+ emoney-credit-transaction-types
+ (list emoney-cr-transfer-transaction-type)
+ (list emoney-db-transfer-transaction-type))))
+
+;; Stolen from Gnus' time-date.el
+(defun emoney-days-to-time (days)
+ "Convert DAYS into a time value."
+ (let* ((seconds (* 1.0 days 60 60 24))
+ (rest (expt 2 16))
+ (ms (condition-case nil (floor (/ seconds rest))
+ (range-error (expt 2 16)))))
+ (list ms (condition-case nil (round (- seconds (* ms rest)))
+ (range-error (expt 2 16))))))
+
+;; Stolen from Gnus' time-date.el
+(defun emoney-time-add (t1 t2)
+ "Add two time values. One should represent a time difference."
+ (let ((high (car t1))
+ (low (if (consp (cdr t1)) (nth 1 t1) (cdr t1)))
+ (micro (if (numberp (car-safe (cdr-safe (cdr t1))))
+ (nth 2 t1)
+ 0))
+ (high2 (car t2))
+ (low2 (if (consp (cdr t2)) (nth 1 t2) (cdr t2)))
+ (micro2 (if (numberp (car-safe (cdr-safe (cdr t2))))
+ (nth 2 t2)
+ 0)))
+ ;; Add
+ (setq micro (+ micro micro2))
+ (setq low (+ low low2))
+ (setq high (+ high high2))
+
+ ;; Normalize
+ ;; `/' rounds towards zero while `mod' returns a positive number,
+ ;; so we can't rely on (= a (+ (* 100 (/ a 100)) (mod a 100))).
+ (setq low (+ low (/ micro 1000000) (if (< micro 0) -1 0)))
+ (setq micro (mod micro 1000000))
+ (setq high (+ high (/ low 65536) (if (< low 0) -1 0)))
+ (setq low (logand low 65535))
+
+ (list high low micro)))
+
+(defun emoney-append-transaction (&optional trans-type description amount)
+ "Add a transaction to the end of current buffer using today's date."
+ (interactive)
+ (goto-char (point-max))
+ (if (not (equal 0 (current-column)))
+ (newline))
+ (let* ((date-variance
+ (read-number "Date (RET for current; -DAYS past; DAYS future): "
+ 'integers-only "0"))
+ (tran-date
+ (format-time-string emoney-date-format
+ (emoney-time-add
+ (current-time)
+ (emoney-days-to-time date-variance)))))
+ (insert tran-date))
+ (move-to-tab-stop)
+ (emoney-build-types-list)
+ (let* ((type (or trans-type
+ (emoney-completing-read "Transaction type: "
+ emoney-transaction-types))))
+ (insert type)
+ (move-to-tab-stop)
+ (insert "o")
+ (move-to-tab-stop)
+ (let ((before-descript (point))
+ (start-column (current-column))
+ (fill-column (- emoney-sign-column
+ emoney-description-column
+ 1)))
+ (save-excursion
+ (insert (or description
+ (read-string "Description: ")))
+ (save-restriction
+ (narrow-to-region before-descript (point))
+ (goto-char before-descript)
+ (while (progn (move-to-column fill-column) (not (eobp)))
+ (search-forward " " nil t)
+ (insert "\n"))
+ (fill-paragraph 1)
+ (goto-char before-descript)
+ (forward-line 1)
+ (while (progn (beginning-of-line) (not (eobp)))
+ (indent-to-column start-column)
+ (forward-line 1)))))
+ (move-to-tab-stop)
+ (cond ((or (member type emoney-credit-transaction-types)
+ (string= type emoney-cr-transfer-transaction-type))
+ (insert "+"))
+ ((or (member type emoney-debit-transaction-types)
+ (string= type emoney-db-transfer-transaction-type))
+ (insert "-"))
+ ((string= type "init")
+ (insert "="))
+ (t (if (y-or-n-p "Is this a credit transaction? ")
+ (insert "+")
+ (if (y-or-n-p "Is this a debit transaction? ")
+ (insert "-")
+ (if (y-or-n-p "Is this an initialising transaction? ")
+ (insert "=")
+ (warn "Couldn't determine +/-/=, leaving blank"))))))
+ (move-to-tab-stop)
+ (insert (if amount
+ (number-to-string amount)
+ (read-string "Amount: ")))
+ (run-hooks 'emoney-transaction-hook)))
+
+(defun emoney-append-next-cheque ()
+ "Add a cheque transaction to the end of the current buffer using today's date.
+
+Inserts the cheque number following the last cheque number written into the
+transaction type column. Loses if you write cheques out of order."
+ (interactive)
+ (goto-char (point-max))
+ (if (not (equal 0 (current-column)))
+ (newline))
+ (insert (format-time-string emoney-date-format))
+ (move-to-tab-stop)
+ (let (cheque cheque-number noinit)
+ (save-excursion
+ (if (search-backward-regexp emoney-cheque-type 0 t)
+ (progn
+ (setq cheque (buffer-substring (match-beginning 1) (match-end 1)))
+ (setq noinit nil))
+ (move-to-column emoney-type-column)
+ (if (< (current-column) emoney-type-column)
+ (indent-to-column emoney-type-column))
+ (if emoney-uk-cheque-spelling
+ (insert "chq 000001")
+ (insert "chk 000001"))
+ (setq noinit t)))
+ (unless noinit
+ (setq cheque-number (1+ (string-to-number cheque)))
+ (move-to-column emoney-type-column)
+ (if (< (current-column) emoney-type-column)
+ (indent-to-column emoney-type-column))
+ (if emoney-uk-cheque-spelling
+ (insert (format "chq %06d" cheque-number))
+ (insert (format "chk %06d" cheque-number))))
+ (move-to-tab-stop)
+ (insert "o")
+ (move-to-tab-stop)
+ (let ((before-descript (point))
+ (start-column (current-column))
+ (fill-column (- emoney-sign-column
+ emoney-description-column
+ 1)))
+ (save-excursion
+ (insert (read-string "Payable To: "))
+ (save-restriction
+ (narrow-to-region before-descript (point))
+ (goto-char before-descript)
+ (while (progn (move-to-column fill-column) (not (eobp)))
+ (search-forward " " nil t)
+ (insert "\n"))
+ (fill-paragraph 1)
+ (goto-char before-descript)
+ (forward-line 1)
+ (while (progn (beginning-of-line) (not (eobp)))
+ (indent-to-column start-column)
+ (forward-line 1)))))
+ (move-to-tab-stop)
+ (insert "-")
+ (move-to-tab-stop)
+ (insert (read-string "Amount: "))
+ (run-hooks 'emoney-transaction-hook)
+ (run-hooks 'emoney-transaction-cheque-hook)))
+
+(defvar emoney-transfer-account-history nil)
+
+(defun emoney-transfer-funds (&optional from to amount)
+ "Transfer funds from one eMoney account to another.
+
+Argument FROM is the account to transfer from.
+Argument To is the account to transfer to.
+Argument AMOUNT is how much to transfer."
+ (interactive)
+ (let ((from (or from
+ (emoney-completing-read "Transfer from: "
+ emoney-chart-of-accounts
+ nil
+ t
+ (concat emoney-current-account-name
+ ".emy")
+ emoney-transfer-account-history)))
+ (to (or to
+ (emoney-completing-read "Transfer to: "
+ emoney-chart-of-accounts nil t nil
+ emoney-transfer-account-history)))
+ (amount (or amount
+ (read-number "Amount: ")))
+ (current-ac (concat emoney-current-account-name ".emy")))
+ (with-current-buffer from
+ (emoney-append-transaction
+ emoney-db-transfer-transaction-type
+ (concat "T'fer to " (file-name-sans-extension to))
+ amount))
+ (with-current-buffer to
+ (emoney-append-transaction
+ emoney-cr-transfer-transaction-type
+ (concat "T'fer from " (file-name-sans-extension from))
+ amount))
+ (loop for buf in '(from to)
+ do (emoney-switch-to-account (symbol-value buf))
+ do (emoney-recalculate-buffer))
+ (run-hooks 'emoney-transaction-transfer-hook)
+ (emoney-switch-to-account current-ac)))
+
+(defsubst emoney-build-type-regexp ()
+ "Return a regular expression that will match any valid transaction type.
+
+This is done dynamically so users can redefine the valid transactions in
+their `user-init-file' even after this file has been loaded."
+ (emoney-build-types-list)
+ (let ((types (append emoney-transaction-types
+ (list emoney-cheque-type))))
+ (concat
+ "^\\("
+ (mapconcat
+ #'(lambda(x) x) types "\\|")
+ "\\)[ \t]*$")))
+
+(defun emoney-check-transaction-type (line-start)
+ "Check to make sure a valid transaction type has been used.
+
+Argument LINE-START is the starting point.
+
+Please note that the word \"check\" here means \"verify\" and it has
+nothing to do with the American spelling of the word \"cheque\"."
+ (let* ((type-regexp (emoney-build-type-regexp))
+ (type-start (+ line-start emoney-type-column))
+ (type-end (+ line-start emoney-clear-column))
+ (type-string (buffer-substring type-start type-end)))
+ (if (string-match type-regexp type-string)
+ nil
+ (error "Line %d, invalid type: %s"
+ (1+ (emoney-current-line)) type-string))))
+
+(defun emoney-find-next-transaction ()
+ "Find the next line that is a complete transaction.
+
+Return a list of the line start, numeric data start and line end
+points."
+ (let ((found nil)
+ (line-regexp
+ "^\\([0-90-9]\\)+.*$")
+ line-start
+ line-end
+ data-start)
+ (while (and
+ (not found)
+ (search-forward-regexp line-regexp (point-max) t))
+ (setq line-start (match-beginning 0))
+ (setq line-end (progn (end-of-line) (point)))
+ (setq data-start (+ line-start emoney-sign-column))
+ (if (> line-end data-start)
+ (setq found t)))
+ (if found
+ (list line-start data-start line-end)
+ nil)))
+
+(defun emoney-parse-transaction-data (data)
+ "Return a list of floating point numbers from DATA.
+
+DATA is a string representing the sign, amount and optionally balance of a
+transaction. Balance is nil if not present."
+ (let ((data-regexp "\\([-+=]\\)[ \t]*\\([0-9.]+\\)?[ \t]*\\([-]?[0-9.]+\\)?")
+ (balance nil)
+ (reset nil)
+ string sign amount)
+ (string-match data-regexp data)
+ (if (match-beginning 1)
+ (setq sign (substring data (match-beginning 1) (match-end 1)))
+ (error "Line %d, missing sign" (1+ (emoney-current-line))))
+ (if (equal "=" sign)
+ (progn
+ (setq sign "+")
+ (setq reset t)))
+ (if (match-beginning 2)
+ (progn
+ (setq string (substring data (match-beginning 2) (match-end 2)))
+ (setq amount (string-to-number (concat sign string))))
+ (error "Line %d, missing amount" (1+ (emoney-current-line))))
+ (if (match-beginning 3)
+ (progn
+ (setq string (substring data (match-beginning 3) (match-end 3)))
+ (setq balance (string-to-number string))))
+ (if reset (setq sign "="))
+ (list sign amount balance)))
+
+(defun emoney-same (amount1 amount2)
+ "Compare two dollar amounts, AMOUNT1 AMOUNT2, for equivalence."
+ (let ((string1 (format "%10.2f" amount1))
+ (string2 (format "%10.2f" amount2)))
+ (equal string1 string2)))
+
+(defun emoney-form-transaction-data (sign amount balance)
+ "Given SIGN, AMOUNT and a BALANCE, return a string.
+
+The string is suitable for placing in the numeric region of a
+transaction, based on the defined input columns."
+ (let* ((amount (abs amount))
+ (width1 (- emoney-amount-column emoney-sign-column))
+ (width2 (- emoney-current-balance-column emoney-amount-column))
+ (len (length emoney-largest-balance))
+ (gap (- width2 len))
+ (value (concat "%" (number-to-string len) ".2f"))
+ (format-string (concat "%-" (number-to-string width1)
+ "s" value
+ "%" (number-to-string gap) "s" value)))
+ (format format-string sign amount "" balance)))
+
+(defun emoney-recalculate (start end)
+ "Recalculate the balances for region START END.
+
+The final balance, uncleared total, and the number of balances that
+changed, and the transaction count are returned in a list."
+ (run-hooks 'emoney-recalculate-before-hook)
+ (let ((current-balance 0)
+ (changes 0)
+ (uncleared 0)
+ (transactions 0)
+ line-points)
+ (save-excursion
+ (save-restriction
+ (narrow-to-region start end)
+ (untabify (point-min) (point-max))
+ (goto-char (point-min))
+ (while (setq line-points (emoney-find-next-transaction))
+ (setq transactions (1+ transactions))
+ (let* ((line-start (nth 0 line-points))
+ (data-start (nth 1 line-points))
+ (data-end (nth 2 line-points))
+ (clear-flag (buffer-substring
+ (+ line-start emoney-clear-column)
+ (+ 1 line-start emoney-clear-column)))
+ (data-string (buffer-substring data-start data-end))
+ (data-values (emoney-parse-transaction-data data-string))
+ (sign (nth 0 data-values))
+ (amount (nth 1 data-values))
+ (balance (nth 2 data-values))
+ (new-balance (if (equal sign "=")
+ amount
+ (+ current-balance amount)))
+ (new-uncleared (if (equal clear-flag "x")
+ uncleared
+ (+ uncleared amount)))
+ (new-string
+ (emoney-form-transaction-data sign amount new-balance)))
+ (emoney-check-transaction-type line-start)
+ (setq current-balance new-balance)
+ (setq uncleared new-uncleared)
+ (if (or (null balance) (not (emoney-same balance new-balance)))
+ (setq changes (1+ changes)))
+ (if (not (equal data-string new-string))
+ (progn
+ (delete-region data-start data-end)
+ (goto-char data-start)
+ (insert new-string)))))
+ (widen)))
+ (run-hooks 'emoney-recalculate-after-hook)
+ (list current-balance uncleared changes transactions)))
+
+(defvar emoney-is-exiting nil
+ "Non-nil when eMoney is in the process of quitting.")
+
+(defun emoney-update-acc-buf-bal (account balance)
+ "Update ACCOUNT BALANCE in accounts buffer."
+ (with-current-buffer emoney-accounts-buffer
+ (goto-char (point-min))
+ (search-forward account)
+ (kill-line)
+ (move-to-column 13 'force)
+ (insert-face
+ (format (concat "%"
+ (number-to-string (- (length emoney-largest-balance)
+ (length (format "%.2f" balance))))
+ "s$%.2f") "" balance)
+ (if (< balance 0)
+ 'emoney-debit-face
+ 'emoney-credit-face))))
+
+(defun emoney-recalculate-buffer ()
+ "Recalculate the current buffer.
+
+See `emoney-recalculate'."
+ (interactive)
+ (let* ((result (emoney-recalculate (point-min) (point-max)))
+ (balance (nth 0 result))
+ (uncleared (nth 1 result))
+ (changes (nth 2 result))
+ (total (nth 3 result)))
+ (when emoney-save-after-recalculate
+ (save-buffer (current-buffer)))
+ (emoney-update-acc-buf-bal emoney-current-account-name balance)
+ (if emoney-is-exiting
+ (message
+ (format "book bal %.2f unclrd %.2f bank bal %.2f (%d/%d recalcs)"
+ balance uncleared (- balance uncleared) changes total))
+ (message-or-box
+ (format "book bal %.2f unclrd %.2f bank bal %.2f (%d/%d recalcs)"
+ balance uncleared (- balance uncleared) changes total)))
+ (if (> changes 0)
+ (end-of-line))))
+
+(defun emoney-recalculate-region (start end)
+ "Recalculate the current region, START END.
+
+See `emoney-recalculate'."
+ (interactive "r")
+ (let* ((result (emoney-recalculate start end))
+ (balance (nth 0 result))
+ (uncleared (nth 1 result))
+ (changes (nth 2 result))
+ (total (nth 3 result)))
+ (message-or-box (format "Region balance %.2f uncleared %.2f (%d/%d recalcs)"
+ balance uncleared changes total))
+ (if (> changes 0)
+ (end-of-line))))
+
+(defun emoney-summarise-cheques (start end)
+ "Create a buffer that lists only the cheques in the specified region.
+
+The region is denoted by START END.
+
+The list is sorted on cheque number. Breaks in sequence are denoted by lines
+containing an asterisk between the cheques where the break occurs. The buffer
+is also recalculated, thus showing to total of the cheques summarised."
+ (let ((emoney-buffer (current-buffer))
+ (summary-buffer (get-buffer-create "*cheque summary*"))
+ (cheque-count 0)
+ (sequence-breaks 0)
+ line-points)
+ (save-excursion
+ (save-restriction
+ (set-buffer summary-buffer)
+ (delete-region (point-min) (point-max))
+ (set-buffer emoney-buffer)
+ (narrow-to-region start end)
+ (goto-char (point-min))
+ (while (setq line-points (emoney-find-next-transaction))
+ (let* ((line-start (nth 0 line-points))
+ (line-end (nth 2 line-points))
+ (type-start (+ line-start emoney-type-column))
+ (type-end (+ line-start emoney-description-column))
+ (type-string (buffer-substring type-start type-end)))
+ (if (string-match emoney-cheque-type type-string)
+ (progn
+ (append-to-buffer
+ summary-buffer line-start (1+ line-end))
+ (setq cheque-count (1+ cheque-count))))))
+ (widen)
+ (set-buffer summary-buffer)
+ (sort-numeric-fields 3 (point-min) (point-max))
+ (setq sequence-breaks (emoney-find-sequence-breaks))
+ (goto-char (point-max))
+ (insert (format "\n%d cheque%s summarised, %d sequence break%s\n"
+ cheque-count
+ (if (equal 1 cheque-count) "" "s")
+ sequence-breaks
+ (if (equal 1 sequence-breaks) "" "s")))
+ (set-buffer emoney-buffer)))
+ (with-electric-help
+ '(lambda ()
+ (insert
+ (with-temp-buffer
+ (erase-buffer)
+ (insert-buffer summary-buffer)
+ (buffer-string (current-buffer)))))
+ "*cheque summary*"))
+ (run-hooks 'emoney-summarise-cheques-hook))
+
+(defun emoney-find-sequence-breaks ()
+ "Find cheque sequence breaks in the current cheque summary buffer.
+
+Mark breaks in sequence by inserting a line with an asterisk between
+the offending cheques. Return the count of sequence breaks found."
+ (let ((last-cheque nil)
+ (sequence-breaks 0))
+ (goto-char (point-min))
+ (while (search-forward-regexp
+ (concat "\\([0-9/]+\\)[ \t]+" emoney-cheque-type)
+ (point-max) t)
+ (let* ((cheque-start (match-beginning 2))
+ (cheque-end (match-end 2))
+ (cheque-string (buffer-substring cheque-start cheque-end))
+ (cheque-number (string-to-int cheque-string)))
+ (if (not last-cheque)
+ (setq last-cheque cheque-number)
+ (if (not (equal cheque-number (1+ last-cheque)))
+ (progn
+ (setq sequence-breaks (1+ sequence-breaks))
+ (beginning-of-line)
+ (open-line 1)
+ (insert "*")
+ (next-line 2)))
+ (setq last-cheque cheque-number))))
+ sequence-breaks))
+
+(defun emoney-summarise-cheques-buffer ()
+ "Summarise the cheque transactions in the current buffer."
+ (interactive)
+ (emoney-summarise-cheques (point-min) (point-max))
+ (emoney-recalculate (point-min) (point-max)))
+
+(defun emoney-summarise-cheques-region (start end)
+ "Summarise the cheque transactions in region START - END."
+ (interactive "r")
+ (emoney-summarise-cheques start end)
+ (emoney-recalculate (point-min) (point-max)))
+
+(defun emoney-new-account (&optional name balance)
+ "*Create a new A/C named NAME for use with eMoney."
+ (interactive)
+ (let ((new-acc (concat (or name
+ (read-string "New A/C Name: ")) ".emy"))
+ (bal (or balance
+ (read-number "Initial Balance: " nil 0))))
+ (find-file-noselect
+ (expand-file-name new-acc emoney-accounts-directory))
+ (setq emoney-chart-of-accounts
+ (push new-acc emoney-chart-of-accounts))
+ (select-window (get-buffer-window
+ (concat emoney-current-account-name ".emy")))
+ (switch-to-buffer new-acc)
+ (setq emoney-current-account-name
+ (file-name-sans-extension new-acc))
+ (emoney-append-transaction "init" "Opening Balance" bal)
+ (emoney-recalculate-buffer)
+ (emoney-show-buffers)
+ (switch-to-buffer new-acc)
+ (goto-char (point-max))
+ (run-hooks 'emoney-new-account-hook)))
+
+(defun emoney-setup-control-buffer ()
+ "Set up the eMoney \"Control\" buffer."
+ (let ((buf emoney-control-buffer))
+ (save-excursion
+ (when (buffer-live-p (get-buffer-create buf))
+ (kill-buffer buf))
+ (set-buffer (get-buffer-create buf))
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (emoney-new-account))
+ :help-echo "Create a new eMoney account."
+ " New A/C ")
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (save-excursion
+ (set-buffer
+ (concat emoney-current-account-name ".emy"))
+ (emoney-append-transaction)))
+ :help-echo "Add a new transaction\n
+If you want to add a cheque transaction
+use \"Add Chq\" instead."
+ "Add Trans")
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (save-excursion
+ (set-buffer
+ (concat emoney-current-account-name ".emy"))
+ (emoney-append-next-cheque)))
+ :help-echo (if emoney-uk-cheque-spelling
+ "Add a new cheque transaction."
+ "Add a new check transaction.")
+ (if emoney-uk-cheque-spelling
+ " Add Chq "
+ " Add Chk "))
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (save-excursion
+ (set-buffer
+ (concat emoney-current-account-name ".emy"))
+ (emoney-recalculate-buffer)))
+ :help-echo "Record the last transaction.\n
+Also recalculates the buffer."
+ "End Trans")
+ (widget-insert "\n\n")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (save-excursion
+ (set-buffer
+ (concat emoney-current-account-name ".emy"))
+ (emoney-transfer-funds)))
+ :help-echo "Transfer funds between accounts."
+ "Transfer ")
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (save-excursion
+ (set-buffer
+ (concat emoney-current-account-name ".emy"))
+ (emoney-summarise-cheques-buffer)))
+ :help-echo (if emoney-uk-cheque-spelling
+ "Display a summary of cheques."
+ "Display a summary of checks.")
+ (if emoney-uk-cheque-spelling
+ " Chq Sum "
+ " Chk Sum "))
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (emoney-calc))
+ :help-echo "Start the Emacs Calculator.\n
+So you can count up all
+of your millions!"
+ " Calc ")
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (if emoney-is-beta
+ (message-or-box
+ "eMoney: %s, \"%s\" [Beta]"
+ emoney-version emoney-codename)
+ (message-or-box
+ "eMoney: %s, \"%s\""
+ emoney-version emoney-codename)))
+ :help-echo "Display eMoney version info."
+ " Version ")
+ (widget-insert "\n\n")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (emoney-go-to-bank))
+ :help-echo "Open your bank's web site in your browser."
+ " Bank ")
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (emoney-quit))
+ :help-echo "Exit eMoney."
+ " Exit ")
+ (set-specifier horizontal-scrollbar-visible-p nil (current-buffer))
+ (set-specifier vertical-scrollbar-visible-p nil (current-buffer))
+ (set-specifier has-modeline-p nil (current-buffer))
+ (run-hooks 'emoney-setup-control-buffer-hook))))
+
+(defun emoney-show-buffers ()
+ "Display all the eMoney buffers."
+ (emoney-setup-accounts-buffer)
+ (emoney-setup-control-buffer)
+ (emoney-setup-header-buffer)
+ (delete-other-windows nil)
+ (switch-to-buffer emoney-accounts-buffer)
+ (split-window nil emoney-accounts-buffer-height)
+ (split-window nil emoney-accounts-buffer-width t)
+ (other-window 1)
+ (switch-to-buffer emoney-control-buffer)
+ (other-window 1)
+ (switch-to-buffer emoney-header-buffer)
+ (split-window nil emoney-header-buffer-height)
+ (other-window 1))
+
+(defun emoney-quit ()
+ "*Exit from eMoney, optionally recalculating all accounts first.
+
+To have all of your accounts recalculated before eMoney exits set
+`emoney-recalculate-on-quit' to `t'
+
+If `emoney-save-after-recalculate' is also `t' the account buffers
+will be saved before eMoney exits."
+ (interactive)
+ (run-hooks 'emoney-quit-before-hook)
+ (let ((accounts emoney-chart-of-accounts))
+ (setq emoney-is-exiting t)
+ (dolist (buf accounts)
+ (when emoney-recalculate-on-quit
+ (set-buffer buf)
+ (emoney-recalculate-buffer))
+ (kill-buffer buf))
+ (kill-buffer emoney-accounts-buffer)
+ (kill-buffer emoney-control-buffer)
+ (kill-buffer emoney-header-buffer)
+ (run-hooks 'emoney-quit-after-hook)
+ (when (and emoney-use-new-frame
+ (frame-live-p emoney-frame))
+ (delete-frame emoney-frame))
+ (setq emoney-frame nil)
+ (unless emoney-use-new-frame
+ (jump-to-register ?\$))
+ (setq emoney-is-exiting nil)))
+
+(defun emoney-recalc-and-exit ()
+ "*Exit form eMoney, recalculating all accounts first."
+ (interactive)
+ (let ((old-recalc-val emoney-recalculate-on-quit))
+ (setq emoney-recalculate-on-quit t)
+ (emoney-quit)
+ (setq emoney-recalculate-on-quit old-recalc-val)))
+
+;;;###autoload
+(defun emoney ()
+ "*Start a new eMoney session."
+ (interactive)
+ (unless emoney-use-new-frame
+ (window-configuration-to-register ?\$))
+ (unless (frame-live-p emoney-frame)
+ (setq emoney-frame
+ (if emoney-use-new-frame
+ (make-frame '((name . "eMoney")))
+ (selected-frame))))
+ (select-frame emoney-frame)
+ (let ((accounts emoney-chart-of-accounts)
+ (dir emoney-accounts-directory))
+ (while accounts
+ (find-file (expand-file-name (car accounts) dir))
+ (unless (eq major-mode 'emoney-mode)
+ (emoney-mode))
+ (setq accounts (cdr accounts)))
+ (setq emoney-current-account-name
+ (file-name-sans-extension emoney-default-account))
+ (emoney-show-buffers)
+ (switch-to-buffer emoney-default-account)
+ (goto-char (point-max)))
+ (focus-frame emoney-frame))
+
+;; Work around a problem with Emacs calc. If you start calc in a
+;; frame with multiple buffers visible when calc exits it doesn't
+;; return point to the place it was when you called calc. These
+;; advices overcome that.
+(defadvice calc (before em-calc-win-save first activate)
+ "Before starting calc, save the window config.
+This is so we can restore the window config when calc exits because
+calc doesn't DTRT in this regard by itself."
+ (push-window-configuration))
+
+(defadvice calc-quit (after em-calc-win-restore last activate)
+ "Restore the \"pre calc\" window config on calc exit."
+ (pop-window-configuration))
+
+
+;;;###autoload(add-to-list 'auto-mode-alist '("\\.emy$" . emoney-mode))
+
+(provide 'emoney)
+
+;;; emoney.el ends here