Couple of minor cosmetic changes.
[emoney] / emoney.el
CommitLineData
a7161eda
SY
1;; emoney.el --- A home finance package.
2
ee98102e 3;; Copyright (C) 2003 - 2011 Steve Youngs
a7161eda
SY
4
5;; Author: Steve Youngs <steve@sxemacs.org>
6;; Maintainer: Steve Youngs <steve@sxemacs.org>
7;; Created: <2003-06-04>
8;; Keywords: money finance banking cash
9
10;; This file is part of eMoney.
11
12;; Redistribution and use in source and binary forms, with or without
13;; modification, are permitted provided that the following conditions
14;; are met:
15;;
16;; 1. Redistributions of source code must retain the above copyright
17;; notice, this list of conditions and the following disclaimer.
18;;
19;; 2. Redistributions in binary form must reproduce the above copyright
20;; notice, this list of conditions and the following disclaimer in the
21;; documentation and/or other materials provided with the distribution.
22;;
23;; 3. Neither the name of the author nor the names of any contributors
24;; may be used to endorse or promote products derived from this
25;; software without specific prior written permission.
26;;
27;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
28;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
29;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
30;; DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
31;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
34;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
35;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
36;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
37;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
39;;; Commentary:
40;;
41;; eMoney is based on balance.el, originally by Jason Baietto
42;; <jason@ssd.csd.harris.com> and later maintained by Bob Newell
43;; <bnewell@alum.mit.edu>.
44;;
45;; eMoney tries to give the user a reasonably self-contained finance
46;; solution. The target audience is the home user and possibly the
47;; small (very small) business operator. It probably will never be
48;; compatible with any commercial finance packages available.
49;;
50;; Right now, eMoney is nothing more than a very simple cash book
51;; program. Use it to balance your cheque book. See the "Todo:"
52;; section for a partial list of possible upcoming features.
53;;
54;; Installation (from source):
55;;
56;; tar zxf emoney-x.xx.tar.gz
57;; cd emoney-x.xx
58;; check the paths in Makefile
59;; make
60;; make install (you may need to be root for this)
61;;
62;; Installation (from XEmacs package tarball):
63;;
64;; cd /usr/local/share/sxemacs/site-packages
65;; (/usr/local/lib/xemacs/site-packages for XEmacs)
66;; tar zxf /path/to/emoney-x.xx-pkg.tar.gz
67;;
68;; (Re)start (S)XEmacs and do `M-x emoney RET'
69;;
70;; You can also invoke `emoney-mode' by simply visiting a file with a
71;; `.emy' extension.
72;;
73;; Please note that I use SXEmacs exclusively, I have no idea whether
74;; or not this will run or even byte-compile with GNU/Emacs (it _will_
75;; work fine with XEmacs). Also, I have no desire or intentions of
76;; making this package portable between XEmacs and GNU/Emacs. Harsh
77;; words? No, not really, I simply don't have the time or resources to
78;; invest in the extra work involved. And also there is no way that
79;; I'd be able to support GNU/Emacs users because I'm not familiar with
80;; that flavour of Emacs. If you want to port it, be my guest, this is
81;; an Open Source project.
82;;
83;; Another note: This package uses correct English spelling wherever
84;; possible. For example, "cheque" instead of "check", "summarise"
85;; instead of "summarize". Although I welcome and encourage _ALL_
86;; contributions, patches that s/que/ch/ or s/ise/ize/ will go straight
87;; to /dev/null.
88
89;;; ChangeLog:
90;;
91;; This is just a place holder so `emoney-commentary' will work
92;; properly. See the ChangeLog file for changes.
93
94;;; Code:
95
96;; Drag in what we need.
97(eval-and-compile
98 (autoload 'calc "calc" nil t)
99 (autoload 'clear-rectangle "rect" nil t)
100 (autoload 'completing-read "minibuf")
101 (autoload 'customize-group "cus-edit")
102 (autoload 'lm-commentary "lisp-mnt")
103 (autoload 'sort-numeric-fields "sort" nil t)
104 (autoload 'untabify "tabify" nil t)
105 (autoload 'with-electric-help "ehelp")
106 (require 'wid-edit))
107
108(eval-when-compile
109 (autoload 'eval-when "cl-macs" nil nil 'macro)
110 (autoload 'calc-quit "calc" nil t)
111 (require 'advice)
112 (defvar calc-was-split)
113 (autoload 'browse-url "browse-url" nil t)
114 (autoload 'regexp-opt "regexp-opt"))
115
116;; Custom.
117(defgroup emoney nil
118 "Customisations for `emoney-mode'."
119 :prefix "emoney-"
120 :group 'tools)
121
3d7af7e7 122(defcustom emoney-accounts-buffer-width 35
a7161eda
SY
123 "How wide in columns for the accounts buffer."
124 :type 'integer
125 :group 'emoney)
126
127(defcustom emoney-accounts-buffer-height 9
128 "How high in lines for the accounts buffer."
129 :type 'integer
130 :group 'emoney)
131
132(defcustom emoney-header-buffer-height 4
133 "How high in lines for the header buffer."
134 :type 'integer
135 :group 'emoney)
136
137(defcustom emoney-accounts-directory (file-name-as-directory
138 (expand-file-name ".emoney"
139 (user-home-directory)))
140 "*The directory where your eMoney account files are located."
141 :type 'directory
142 :group 'emoney)
143
144(defcustom emoney-history-directory (file-name-as-directory
145 (expand-file-name "history"
146 emoney-accounts-directory))
147 "*Directory containing previous years account files."
148 :type 'directory
149 :group 'emoney)
150
151(defcustom emoney-chart-of-accounts
152 (if (file-directory-p emoney-accounts-directory)
153 (directory-files emoney-accounts-directory nil "\\.emy$" nil t)
154 nil)
155 "*A list of eMoney accounts in `emoney-accounts-directory'."
156 :type '(repeat
157 (string :tag "Account Name"))
158 :group 'emoney)
159
160(defcustom emoney-default-account (or (car emoney-chart-of-accounts)
161 "default.emy")
162 "*The default eMoney account to use.
163
164This is the account that has the focus when you start eMoney."
165 :type 'string
166 :group 'emoney)
167
168(defcustom emoney-credit-transaction-types
169 '("autotellcr" "atmcr" "bankcredit" "bcr" "deposit" "dep" "directcr"
170 "dcr" "internetcr" "netcr" "phonecr" "phcr")
171 "*List of valid credit transaction types.
172
173The default types are:
174
175 autotellcr -- Automatic Teller Machine deposit
176 atmcr -- short version of 'autotellcr'
177
178 bankcredit -- Bank initiated credits like interest etc
179 bcr -- short version of 'bankcredit'
180
181 deposit -- Manual, non-direct deposits
182 dep -- short version of 'deposit'
183
184 directcr -- Direct credit transactions
185 dcr -- short version of 'directcr'
186
187 internetcr -- Internet credit transactions
188 netcr -- short version of 'internetcr'
189
190 phonecr -- Telephone credit transactions
191 phcr -- short version of 'phonecr'"
192 :type '(repeat string)
193 :group 'emoney)
194
195(defcustom emoney-debit-transaction-types
196 '("autotelldb" "atmdb" "bankfee" "fee" "directdb" "ddb" "eftpos"
197 "eft" "internetdb" "netdb" "phonedb" "phdb" "withdrawal" "wdl")
198 "*List of valid debit transaction types.
199
200The default types are:
201
202 autotelldb -- Automatic Teller Machine transaction
203 atmdb -- short version of 'autoteller'
204
205 bankfee -- Bank fees
206 fee -- short version of 'bankfee'
207
208 directdb -- Direct debit transactions
209 ddb -- short version of 'directdb'
210
211 eftpos -- Electronic Funds Transfer Point Of Sale
212 eft -- short version of 'eftpos'
213
214 internetdb -- Internet transactions
215 netdb -- short version of 'internet'
216
217 phonedb -- Telephone transactions
218 phcr -- short version of 'phone'
219
220 withdrawal -- Over the counter withdrawal.
221 wdl -- short version of 'withdrawal'"
222 :type '(repeat string)
223 :group 'emoney)
224
225(defcustom emoney-cr-transfer-transaction-type "xfrcr"
226 "*Credit transfer transaction types."
227 :type 'string
228 :group 'emoney)
229
230(defcustom emoney-db-transfer-transaction-type "xfrdb"
231 "*Debit transfer transaction type."
232 :type 'string
233 :group 'emoney)
234
235(defcustom emoney-date-format "%Y-%m-%d"
236 "*The format that `emoney-mode' uses for dates."
237 :type '(choice
238 (const :tag "yyyy-mm-dd"
239 :value "%Y-%m-%d")
240 (const :tag "yyyy/mm/dd"
241 :value "%Y/%m/%d")
242 (const :tag "mm/dd/yy"
243 :value "%m/%d/%y")
244 (const :tag "mm/dd/yyyy"
245 :value "%m/%d/%Y")
246 (const :tag "mm-dd-yy"
247 :value "%m-%d-%y")
248 (const :tag "mm-dd-yyyy"
249 :value "%m-%d-%Y")
250 (const :tag "dd/mm/yy"
251 :value "%d/%m/%y")
252 (const :tag "dd/mm/yyyy"
253 :value "%d/%m/%Y")
254 (const :tag "dd-mm-yy"
255 :value "%d-%m-%y")
256 (const :tag "dd-mm-yyyy"
257 :value "%d-%m-%Y")
258 (const :tag "yy/mm/dd"
259 :value "%y/%m/%d")
260 (const :tag "yy-mm-dd"
261 :value "%y-%m-%d"))
262 :group 'emoney)
263
264(defcustom emoney-uk-cheque-spelling t
265 "*When non-nil, use UK spelling: \"chq\" instead of \"chk\"."
266 :type 'boolean
267 :group 'emoney)
268
269(defcustom emoney-save-after-recalculate t
270 "*If non-nil, save the buffer after a recalculate.
271
272See `emoney-recalculate-buffer'."
273 :type 'boolean
274 :group 'emoney)
275
276(defcustom emoney-recalculate-on-quit nil
277 "*If non-nil, recalculate each eMoney account buffer when quitting eMoney."
278 :type 'boolean
279 :group 'emoney)
280
281(defcustom emoney-use-new-frame nil
282 "*If non-nil, eMoney will start in a new frame."
283 :type 'boolean
284 :group 'emoney)
285
286(defcustom emoney-bank-url "unset"
287 "The URL of your bank's web page."
288 :type 'string
289 :group 'emoney)
290
291;; Hooks
292(defgroup emoney-hooks nil
293 "Various hooks for eMoney."
294 :prefix "emoney-"
295 :group 'emoney)
296
297(defcustom emoney-mode-hooks nil
298 "*Hooks run after `emoney-mode' is entered."
299 :type 'hook
300 :group 'emoney-hooks)
301
302(defcustom emoney-switch-account-hook nil
303 "*Hooks run after switching accounts."
304 :type 'hook
305 :group 'emoney-hooks)
306
307(defcustom emoney-setup-accounts-buffer-hook nil
308 "*Hooks run after setting up the accounts buffer."
309 :type 'hook
310 :group 'emoney-hooks)
311
312(defcustom emoney-setup-header-buffer-hook nil
313 "*Hooks run after setting up the header buffer."
314 :type 'hook
315 :group 'emoney-hooks)
316
317(defcustom emoney-setup-control-buffer-hook nil
318 "*Hooks run after setting up the control buffer."
319 :type 'hook
320 :group 'emoney-hooks)
321
322(defcustom emoney-transaction-hook nil
323 "*Hooks run after appending a new transaction.
324
325These hooks are run after any new transaction, including cheque
326and transfers. If you want to do additional things with cheque or
327transfer transactions, see `emoney-transaction-cheque-hook' &
328`emoney-transaction-transfer-hook'."
329 :type 'hook
330 :group 'emoney-hooks)
331
332(defcustom emoney-transaction-cheque-hook nil
333 "*Hooks run after appending a cheque transaction."
334 :type 'hook
335 :group 'emoney-hooks)
336
337(defcustom emoney-transaction-transfer-hook nil
338 "*Hooks run after appending a transfer transaction."
339 :type 'hook
340 :group 'emoney-hooks)
341
342(defcustom emoney-recalculate-before-hook nil
343 "*Hooks run just prior to recalculating an eMoney buffer.
344
345See `emoney-recalculate-after-hook' for doing things after recalculating."
346 :type 'hook
347 :group 'emoney-hooks)
348
349(defcustom emoney-recalculate-after-hook nil
350 "*Hooks run just after recalculating an eMoney buffer.
351
352See `emoney-recalculate-before-hook' for doing things before recalculating."
353 :type 'hook
354 :group 'emoney-hooks)
355
356(defcustom emoney-summarise-cheques-hook nil
357 "*Hooks run after doing `emoney-summarise-cheques'."
358 :type 'hook
359 :group 'emoney-hooks)
360
361(defcustom emoney-new-account-hook nil
362 "*Hooks run after creating a new account."
363 :type 'hook
364 :group 'emoney-hooks)
365
366(defcustom emoney-quit-before-hook nil
367 "*Hooks run just prior to eMoney exiting."
368 :type 'hook
369 :group 'emoney-hooks)
370
371(defcustom emoney-quit-after-hook nil
372 "*Hooks run as the last thing when eMoney exits."
373 :type 'hook
374 :group 'emoney-hooks)
375
376;; Faces
377(defgroup emoney-faces nil
378 "eMoney faces."
379 :prefix "emoney-"
380 :group 'emoney)
381
382(make-face 'emoney-account-name-face)
383(set-face-parent 'emoney-account-name-face 'font-lock-variable-name-face)
384
385(make-face 'emoney-debit-face)
386(set-face-parent 'emoney-debit-face 'font-lock-warning-face)
387
388(make-face 'emoney-credit-face)
389(set-face-parent 'emoney-credit-face 'font-lock-function-name-face)
390
391(make-face 'emoney-date-face)
392(set-face-parent 'emoney-date-face 'font-lock-keyword-face)
393
394(make-face 'emoney-clear-tran-face)
395(set-face-parent 'emoney-clear-tran-face 'font-lock-string-face)
396
397(make-face 'emoney-unclear-tran-face)
398(set-face-parent 'emoney-unclear-tran-face 'font-lock-comment-face)
399
400(make-face 'emoney-header-face)
401(set-face-parent 'emoney-header-face 'font-lock-comment-face)
402
403(defcustom emoney-account-name-face 'emoney-account-name-face
404 "Face for highlighting eMoney account names."
405 :type 'face
406 :group 'emoney-faces)
407
408(defcustom emoney-debit-face 'emoney-debit-face
409 "Face for highlighting debit amounts in eMoney."
410 :type 'face
411 :group 'emoney-faces)
412
413(defcustom emoney-credit-face 'emoney-credit-face
414 "Face for highlighting credit amounts in eMoney."
415 :type 'face
416 :group 'emoney-faces)
417
418(defcustom emoney-date-face 'emoney-date-face
419 "Face for highlighting dates in eMoney."
420 :type 'face
421 :group 'emoney-faces)
422
423(defcustom emoney-clear-tran-face 'emoney-clear-tran-face
424 "Face for highlighting cleared transactions in eMoney."
425 :type 'face
426 :group 'emoney-faces)
427
428(defcustom emoney-unclear-tran-face 'emoney-unclear-tran-face
429 "Face for highlighting uncleared transactions in eMoney."
430 :type 'face
431 :group 'emoney-faces)
432
433(defcustom emoney-header-face 'emoney-header-face
434 "Face for highlighting the column header in eMoney."
435 :type 'face
436 :group 'emoney-faces)
437
438;;; Internal variables
ee98102e 439(defconst emoney-codename "Finance"
a7161eda
SY
440 "The codename of the current version of eMoney.")
441
442(defconst emoney-is-beta t
443 "Non-nil if the current version of eMoney is beta.")
444
445(eval-when (load eval)
446 (unless (file-directory-p emoney-accounts-directory)
447 (make-directory-path emoney-accounts-directory)))
448
449(require 'emoney-version)
450
451;;;###autoload
452(defun emoney-version (&optional arg)
453 "*Display the current version information for eMoney.
454
455Optional prefix Argument ARG, print version information at point
456in the current buffer."
457 (interactive "P")
458 (if emoney-is-beta
459 (if arg
460 (insert (format "eMoney: %s, \"%s\" [Beta]"
461 emoney-version emoney-codename))
462 (message (format "eMoney: %s, \"%s\" [Beta]"
463 emoney-version emoney-codename)))
464 (if arg
465 (insert (format "eMoney: %s, \"%s\""
466 emoney-version emoney-codename))
467 (message (format "eMoney: %s, \"%s\""
468 emoney-version emoney-codename)))))
469
470;;;###autoload
471(defun emoney-commentary ()
472 "*Display the commentary section of emoney.el."
473 (interactive)
474 (with-electric-help
475 '(lambda ()
476 (insert
477 (with-temp-buffer
478 (erase-buffer)
479 (insert (lm-commentary (locate-library "emoney.el")))
480 (goto-char (point-min))
481 (while (re-search-forward "^;+ ?" nil t)
482 (replace-match "" nil nil))
483 (buffer-string (current-buffer)))))
484 "*eMoney Commentary*"))
485
486;;;###autoload
487(defun emoney-copyright ()
488 "*Display the copyright notice for eMoney."
489 (interactive)
490 (with-electric-help
491 '(lambda ()
492 (insert
493 (with-temp-buffer
494 (erase-buffer)
495 (insert-file-contents (locate-library "emoney.el"))
496 (goto-char (point-min))
497 (re-search-forward ";;; Commentary" nil t)
498 (beginning-of-line)
499 (narrow-to-region (point-min) (point))
500 (while (re-search-backward "^;+ ?" nil t)
501 (replace-match "" nil nil))
502 (buffer-string (current-buffer)))))
503 "*eMoney Copyright Notice*"))
504
505(defvar emoney-frame nil
506 "The frame where eMoney is displayed, if in a new frame.")
507
508(defconst emoney-accounts-buffer "*eMoney A/C's*"
509 "The buffer that holds the list of eMoney accounts.")
510
511(defconst emoney-control-buffer "*eMoney Control*"
512 "The buffer containing the eMoney control buttons.")
513
514(defconst emoney-header-buffer "*eMoney Header*"
515 "The buffer for the eMoney account register header line.")
516
517(defvar emoney-current-account-name
518 (file-name-sans-extension emoney-default-account))
519
520(defun emoney-switch-to-account (&optional account)
521 "Switch to account, ACCOUNT."
522 (interactive)
523 (let ((switch-acc (or account
524 (emoney-completing-read "Switch to A/C: "
525 emoney-chart-of-accounts nil t))))
526 (select-window (get-buffer-window
527 (concat emoney-current-account-name ".emy")))
528 (switch-to-buffer switch-acc)
529 (goto-char (point-max))
530 (setq emoney-current-account-name
531 (file-name-sans-extension switch-acc))
532 (select-window (get-buffer-window emoney-header-buffer))
533 (emoney-setup-header-buffer)
534 (switch-to-buffer emoney-header-buffer)
535 (select-window (get-buffer-window
536 (concat emoney-current-account-name ".emy")))
537 (run-hooks 'emoney-switch-account-hook)))
538
539(defun emoney-mouse-switch-to-account (event)
540 "Switch to account under EVENT."
541 (interactive "e")
542 (save-excursion
543 (set-buffer (window-buffer (event-window event)))
544 (let ((switch-acc (extent-string (extent-at-event event))))
545 (select-window (get-buffer-window
546 (concat emoney-current-account-name ".emy")))
547 (switch-to-buffer (concat switch-acc ".emy"))
548 (goto-char (point-max))
549 (setq emoney-current-account-name switch-acc)
550 (select-window (get-buffer-window emoney-header-buffer))
551 (emoney-setup-header-buffer)
552 (switch-to-buffer emoney-header-buffer)))
553 (select-window (get-buffer-window
554 (concat emoney-current-account-name ".emy")))
555 (run-hooks 'emoney-switch-account-hook))
556
557(defconst emoney-accounts-buffer-map
558 (let* ((map (make-sparse-keymap 'emoney-accounts-buffer-map)))
559 (define-key map [button2] #'emoney-mouse-switch-to-account)
560 map)
561 "A keymap for the extents in eMoney Accounts buffer.")
562
563(defconst emoney-largest-balance "999999999.99"
564 "This is only used for formatting purposes.")
565
566(defun emoney-setup-accounts-buffer ()
567 "Set up the eMoney \"Accounts\" buffer."
568 (let ((buf emoney-accounts-buffer)
569 (accounts emoney-chart-of-accounts)
570 help-msg cbal)
571 (save-excursion
572 (when (buffer-live-p (get-buffer-create buf))
573 (kill-buffer buf))
574 (set-buffer (get-buffer-create buf))
575 (while accounts
576 (setq help-msg (concat "Switch to A/C: "
577 (file-name-sans-extension (car accounts))))
578 (insert (file-name-sans-extension (car accounts)))
579 (set-extent-properties
580 (make-extent (point-at-bol) (point))
581 `(face emoney-account-name-face
582 mouse-face highlight
583 help-echo ,help-msg
584 balloon-help ,help-msg
585 keymap ,emoney-accounts-buffer-map))
586 (with-current-buffer (car accounts)
587 (goto-char (point-max))
588 (re-search-backward "\\s-[\\+-=]\\s-" nil t)
589 (setq cbal (nth 2 (emoney-parse-transaction-data
590 (buffer-substring (point) (point-at-eol))))))
3d7af7e7 591 (move-to-column 18 'force)
a7161eda
SY
592 (if cbal
593 (insert-face
594 (format
595 (concat "%"
596 (number-to-string (- (length emoney-largest-balance)
597 (length (format "%.2f" cbal))))
598 "s$%.2f") "" cbal)
599 (if (< cbal 0)
600 'emoney-debit-face
601 'emoney-credit-face))
602 (insert " Needs Recalc"))
603 (insert "\n")
604 (setq cbal nil)
605 (setq accounts (cdr accounts)))
606 (set-specifier horizontal-scrollbar-visible-p nil (current-buffer))
607 (set-specifier has-modeline-p nil (current-buffer))
5d613965 608 (goto-char (point-min))
a7161eda
SY
609 (run-hooks 'emoney-setup-accounts-buffer-hook))))
610
611(defconst emoney-header
612 "Date Type C Description Amount Balance
613===============================================================================\n"
614 "The header inserted at the top of a `emoney-mode' buffer.")
615
616(defun emoney-setup-header-buffer ()
617 "Set up the eMoney \"Account Register Header\" buffer."
618 (let ((buf emoney-header-buffer))
619 (save-excursion
620 (when (buffer-live-p (get-buffer-create buf))
621 (kill-buffer buf))
622 (set-buffer (get-buffer-create buf))
5d613965
SY
623 (center-line
624 (insert-face (upcase emoney-current-account-name)
625 'emoney-header-face))
a7161eda
SY
626 (insert "\n\n")
627 (insert-face emoney-header 'emoney-header-face)
628 (set-specifier horizontal-scrollbar-visible-p nil (current-buffer))
629 (set-specifier vertical-scrollbar-visible-p nil (current-buffer))
630 (set-specifier has-modeline-p nil (current-buffer))
631 (goto-char (point-min))
632 (run-hooks 'emoney-setup-header-buffer-hook))))
633
634;;;###autoload
635(defun emoney-customise ()
636 "*Convenience function to customise eMoney."
637 (interactive)
638 (customize-group 'emoney))
639
640;; Because lots of people in the world can't spell.
641;;;###autoload
642(defalias 'emoney-customize 'emoney-customise)
643
644(defvar emoney-transaction-types '("init" "")
645 "A list of valid transaction types.
646
647It is a combination of `emoney-credit-transaction-types',
648`emoney-debit-transaction-types', and a blank type.")
649
650(defconst emoney-cheque-type
651 (if emoney-uk-cheque-spelling
652 "chq[ \t]+\\([0-9]+\\)"
653 "chk[ \t]+\\([0-9]+\\)")
654 "Type field for cheque transactions.
655
656This is defined separately from the other transaction types because
657it is used by functions that perform special operations on cheque.
658transactions.")
659
660(defconst emoney-date-column 0
661 "Column where transaction date begins.")
662
663(defconst emoney-type-column 11
664 "Column where transaction type begins.")
665
666(defconst emoney-clear-column 22
667 "Column where status appears.")
668
669(defconst emoney-description-column 24
670 "Column where transaction description begins.")
671
672(defconst emoney-sign-column 52
673 "Column where transaction sign begins.")
674
675(defconst emoney-amount-column 54
676 "Column where transaction amount begins.")
677
678(defconst emoney-current-balance-column 65
679 "Column where current emoney begins.")
680
681(defconst emoney-tab-stop-list
682 (list
683 emoney-date-column
684 emoney-type-column
685 emoney-clear-column
686 emoney-description-column
687 emoney-sign-column
688 emoney-amount-column
689 emoney-current-balance-column)
690 "List of tab stops that define the start of all transaction fields.")
691
692(defvar emoney-mode-map
693 (let ((map (make-sparse-keymap 'emoney-mode-map)))
694 (define-key map [(control c) (control b)] #'emoney-backward-field)
695 (define-key map [(control c) (control c)] #'emoney-recalculate-buffer)
696 (define-key map [(control c) (control d)] #'emoney-clear-current-field)
697 (define-key map [(control c) (control f)] #'emoney-forward-field)
698 (define-key map [(control c) (control n)] #'emoney-append-next-cheque)
699 (define-key map [(control c) (control r)] #'emoney-summarise-cheques-region)
700 (define-key map [(control c) (control s)] #'emoney-summarise-cheques-buffer)
701 (define-key map [(control c) (control t)] #'emoney-append-transaction)
702 (define-key map [(control c) (control x)] #'emoney-transfer-funds)
703 (define-key map [tab] #'emoney-forward-field)
704 (define-key map [(control c) b] #'emoney-go-to-bank)
705 (define-key map [(control c) c] #'emoney-calc)
706 (define-key map [(control c) s] #'emoney-switch-to-account)
707 (define-key map [(control c) q] #'emoney-quit)
708 (define-key map [(control c) (control q)] #'emoney-recalc-and-exit)
709 map)
710 "Keymap for emoney buffer.")
711
712(defconst emoney-mode-menu
713 '("eMoney"
714 ["New A/C" emoney-new-account t]
715 "---"
716 [(concat "New "
717 (if emoney-uk-cheque-spelling
718 "Cheque "
719 "Check ")
720 "Transaction") emoney-append-next-cheque t]
721 ["New Transaction" emoney-append-transaction t]
722 ["Transfer Funds" emoney-transfer-funds t]
723 "---"
724 ["Next Field" emoney-forward-field t]
725 ["Previous Field" emoney-backward-field t]
726 ["Clear Current Field" emoney-clear-current-field t]
727 "---"
728 [(concat "Summary of "
729 (if emoney-uk-cheque-spelling
730 "cheques "
731 "checks ")
732 "(buffer)") emoney-summarise-cheques-buffer t]
733 [(concat "Summary of "
734 (if emoney-uk-cheque-spelling
735 "cheques "
736 "checks ")
737 "(region)") emoney-summarise-cheques-region t]
738 "---"
739 ["Recalculate Buffer" emoney-recalculate-buffer t]
740 "---"
741 ["Go To The Bank!" emoney-go-to-bank t]
742 ["Calculator" emoney-calc t]
743 "---"
744 ["Recalc All A/C's and Exit" emoney-recalc-and-exit t]
745 ["Exit eMoney" emoney-quit t])
746 "Menu for `emoney-mode' buffers.")
747
748(easy-menu-define
749 emoney-mode-easymenu nil "eMoney" emoney-mode-menu)
750
751(defvar emoney-credit-type-keywords
752 (regexp-opt (append emoney-credit-transaction-types
753 (list emoney-cr-transfer-transaction-type)))
754 "eMoney font lock keywords for credit tran types")
755
756(defvar emoney-debit-type-keywords
757 (regexp-opt (append emoney-debit-transaction-types
758 (list emoney-db-transfer-transaction-type)))
759 "eMoney font lock keywords for debit tran types")
760
761(defvar emoney-font-lock-keywords
762 `(("x\\s-\\(.*\\)[\\+-]\\s-" (1 emoney-clear-tran-face))
763 ("o\\s-\\(.*\\)[\\+-]\\s-" (1 emoney-unclear-tran-face))
764 (,emoney-credit-type-keywords . emoney-credit-face)
765 (,emoney-debit-type-keywords . emoney-debit-face)
766 ("\\(^[0-9]+\\(-\\|\\/\\)[0-9]+\\(-\\|\\/\\)[0-9]+\\)"
767 (1 emoney-date-face))
768 ("-\\s-+\\([0-9]+\\.[0-9][0-9]\\)" (1 emoney-debit-face))
769 ("-[0-9]+\\.[0-9]+" . emoney-debit-face)
770 ("[^-]\\([0-9]+\\.[0-9]+$\\)" (1 emoney-credit-face))
771 ("\\+\\s-+\\([0-9]+\\.[0-9][0-9]\\)" (1 emoney-credit-face)))
772 "Font lock keywords for `emoney-mode'.")
773
774
775;;;###autoload
776(defun emoney-mode ()
777 "Major mode for editing a buffer containing financial transactions.
778The following bindings provide the main functionality of this mode:
779
780\\{emoney-mode-map}
781
782Transactions occur on a single line and have the following fields (in
783order):
784
785 date The transaction date. See `emoney-date-format'.
786 type This field must either be blank or match one of the
787 expressions defined in `emoney-credit-transaction-types'
788 and `emoney-debit-transaction-types'.
789 clear Status of transaction, 'o' is open, 'x' is cleared. New
790 transactions default to 'o'.
791 description A possibly blank transaction description.
792 sign This field must either be '+', '-' or '='. '+' means
793 credit, '-' means debit, and '=' resets balance. eMoney
794 will usually guess the correct sign to use.
795 amount The transaction amount.
796 balance The balance after this transaction. This field will be
797 computed upon recalculation, you _don't_ need to fill it
798 in. Just do: \\[emoney-recalculate-buffer].
799
800Any line in the buffer that does not begin with a date will be
801considered a comment and ignored. Among other things, this allows
802the transaction description to span several lines.
803
804Changing any amount and recalculating again will update all visible
805balances. Transactions may be commented out by putting a semi-colon
806\(or any other non-numerical character\) at the beginning of the line.
807
808Entering `emoney-mode' runs the `emoney-mode-hooks' if any exist."
809 (interactive)
810 (kill-all-local-variables)
811 (setq major-mode 'emoney-mode)
812 (setq mode-name "eMoney")
813 (use-local-map emoney-mode-map)
814 (easy-menu-add emoney-mode-easymenu)
815 (make-local-variable 'tab-stop-list)
816 (setq tab-stop-list emoney-tab-stop-list)
817 (make-local-variable 'indent-tabs-mode)
818 (setq indent-tabs-mode nil)
819 (setq indent-line-function 'emoney-forward-field)
820 (overwrite-mode 1)
821 (run-hooks 'emoney-mode-hooks))
822
823(defun emoney-current-line ()
824 "Return the current buffer line at point."
825 (save-excursion
826 (beginning-of-line)
827 (count-lines (point-min) (point))))
828
829(defun emoney-completing-read (prompt table &optional predicate require-match
830 initial-contents history default)
831 "Like `completing-read', but also accepts strings.
832
833Arguments PROMPT, TABLE, PREDICATE, REQUIRE-MATCH, INITIAL-CONTENTS,
834HISTORY, DEFAULT are as per `completing-read'."
835 (completing-read
836 prompt
837 (if (vectorp table)
838 table
839 (mapcar 'list table))
840 predicate require-match initial-contents history default))
841
842(defun emoney-forward-field ()
843 "Move the cursor to the next field on the current line."
844 (interactive)
845 (move-to-tab-stop))
846
847(defun emoney-go-to-bank (&optional url)
848 "Open your bank's URL with `browse-url'."
849 (interactive)
850 (let ((url (or url
851 emoney-bank-url)))
852 (if (or (equal url "unset")
853 (not url))
854 (message-or-box "Please customise `emoney-bank-url'.")
855 (browse-url url))))
856
857(defun emoney-calc ()
858 "Wrapper around `calc' to get around \"window-edges bug\"."
859 (interactive)
860 (add-hook 'calc-end-hook #'(lambda ()
861 (setq calc-was-split nil)))
862 (calc))
863
864(defun emoney-last (list)
865 "Return last element in LIST."
866 (cond
867 ((null list)
868 '())
869 ((null (cdr list))
870 (car list))
871 (t (emoney-last (cdr list)))))
872
873(defun emoney-find-largest-less-than (list item)
874 "Search a sorted LIST of numbers, return the largest number that is < ITEM."
875 (let ((list-car (car list))
876 (list-cdr (cdr list))
877 (last nil))
878 (while (and list-car (< list-car item))
879 (setq last list-car)
880 (setq list-car (car list-cdr))
881 (setq list-cdr (cdr list-cdr)))
882 last))
883
884(defun emoney-find-largest-less-than-equal (list item)
885 "Return cdr of LIST starting @ the largest number that is <= to ITEM."
886 (let ((list-car (car list))
887 (list-cdr (cdr list))
888 (last nil))
889 (while (and list-car (<= list-car item))
890 (setq last (cons list-car list-cdr))
891 (setq list-car (car list-cdr))
892 (setq list-cdr (cdr list-cdr)))
893 last))
894
895(defun emoney-find-field (column)
896 "Return a list of the start and end of the field around COLUMN.
897
898End may be nil if column is after the last defined tab stop."
899 (let ((field (emoney-find-largest-less-than-equal
900 emoney-tab-stop-list column)))
901 (if (equal 1 (length field))
902 (list (car field) nil)
903 (list (nth 0 field) (nth 1 field)))))
904
905(defun emoney-backward-field ()
906 "Move the cursor to the previous entry field on the current line."
907 (interactive)
908 (let* ((col (current-column))
909 (prev (emoney-find-largest-less-than emoney-tab-stop-list col)))
910 (if prev
911 (move-to-column prev)
912 (move-to-column (emoney-last emoney-tab-stop-list)))))
913
914(defun emoney-clear-current-field ()
915 "Fill the field around point with spaces, leave point at start of field."
916 (interactive)
917 (let* ((field (emoney-find-field (current-column)))
918 (line-start (progn (beginning-of-line) (point)))
919 (line-end (progn
920 (end-of-line)
921 (untabify line-start (point))
922 (point)))
923 (field-start (+ line-start (nth 0 field)))
924 (field-end (if (nth 1 field)
925 (+ line-start (nth 1 field))
926 line-end)))
927 (clear-rectangle field-start field-end)
928 (goto-char field-start)))
929
930(defsubst emoney-build-types-list ()
931 "Dynamically build a list of transaction types.
932
933This is done dynamically so that the user can change or add to the
934list of transaction types without having to reload eMoney."
935 ;; Initialise to ("init" "").
936 (setq emoney-transaction-types '("init" ""))
937 ;; Load the debit and credit transaction types.
938 (setq emoney-transaction-types
939 (append emoney-transaction-types
940 emoney-debit-transaction-types
941 emoney-credit-transaction-types
942 (list emoney-cr-transfer-transaction-type)
943 (list emoney-db-transfer-transaction-type))))
944
945;; Stolen from Gnus' time-date.el
946(defun emoney-days-to-time (days)
947 "Convert DAYS into a time value."
948 (let* ((seconds (* 1.0 days 60 60 24))
949 (rest (expt 2 16))
950 (ms (condition-case nil (floor (/ seconds rest))
951 (range-error (expt 2 16)))))
952 (list ms (condition-case nil (round (- seconds (* ms rest)))
953 (range-error (expt 2 16))))))
954
955;; Stolen from Gnus' time-date.el
956(defun emoney-time-add (t1 t2)
957 "Add two time values. One should represent a time difference."
958 (let ((high (car t1))
959 (low (if (consp (cdr t1)) (nth 1 t1) (cdr t1)))
960 (micro (if (numberp (car-safe (cdr-safe (cdr t1))))
961 (nth 2 t1)
962 0))
963 (high2 (car t2))
964 (low2 (if (consp (cdr t2)) (nth 1 t2) (cdr t2)))
965 (micro2 (if (numberp (car-safe (cdr-safe (cdr t2))))
966 (nth 2 t2)
967 0)))
968 ;; Add
969 (setq micro (+ micro micro2))
970 (setq low (+ low low2))
971 (setq high (+ high high2))
972
973 ;; Normalize
974 ;; `/' rounds towards zero while `mod' returns a positive number,
975 ;; so we can't rely on (= a (+ (* 100 (/ a 100)) (mod a 100))).
976 (setq low (+ low (/ micro 1000000) (if (< micro 0) -1 0)))
977 (setq micro (mod micro 1000000))
978 (setq high (+ high (/ low 65536) (if (< low 0) -1 0)))
979 (setq low (logand low 65535))
980
981 (list high low micro)))
982
983(defun emoney-append-transaction (&optional trans-type description amount)
984 "Add a transaction to the end of current buffer using today's date."
985 (interactive)
986 (goto-char (point-max))
987 (if (not (equal 0 (current-column)))
988 (newline))
989 (let* ((date-variance
990 (read-number "Date (RET for current; -DAYS past; DAYS future): "
991 'integers-only "0"))
992 (tran-date
993 (format-time-string emoney-date-format
994 (emoney-time-add
995 (current-time)
996 (emoney-days-to-time date-variance)))))
997 (insert tran-date))
998 (move-to-tab-stop)
999 (emoney-build-types-list)
1000 (let* ((type (or trans-type
1001 (emoney-completing-read "Transaction type: "
1002 emoney-transaction-types))))
1003 (insert type)
1004 (move-to-tab-stop)
1005 (insert "o")
1006 (move-to-tab-stop)
1007 (let ((before-descript (point))
1008 (start-column (current-column))
1009 (fill-column (- emoney-sign-column
1010 emoney-description-column
1011 1)))
1012 (save-excursion
1013 (insert (or description
1014 (read-string "Description: ")))
1015 (save-restriction
1016 (narrow-to-region before-descript (point))
1017 (goto-char before-descript)
1018 (while (progn (move-to-column fill-column) (not (eobp)))
1019 (search-forward " " nil t)
1020 (insert "\n"))
1021 (fill-paragraph 1)
1022 (goto-char before-descript)
1023 (forward-line 1)
1024 (while (progn (beginning-of-line) (not (eobp)))
1025 (indent-to-column start-column)
1026 (forward-line 1)))))
1027 (move-to-tab-stop)
1028 (cond ((or (member type emoney-credit-transaction-types)
1029 (string= type emoney-cr-transfer-transaction-type))
1030 (insert "+"))
1031 ((or (member type emoney-debit-transaction-types)
1032 (string= type emoney-db-transfer-transaction-type))
1033 (insert "-"))
1034 ((string= type "init")
1035 (insert "="))
1036 (t (if (y-or-n-p "Is this a credit transaction? ")
1037 (insert "+")
1038 (if (y-or-n-p "Is this a debit transaction? ")
1039 (insert "-")
1040 (if (y-or-n-p "Is this an initialising transaction? ")
1041 (insert "=")
1042 (warn "Couldn't determine +/-/=, leaving blank"))))))
1043 (move-to-tab-stop)
1044 (insert (if amount
1045 (number-to-string amount)
1046 (read-string "Amount: ")))
1047 (run-hooks 'emoney-transaction-hook)))
1048
1049(defun emoney-append-next-cheque ()
1050 "Add a cheque transaction to the end of the current buffer using today's date.
1051
1052Inserts the cheque number following the last cheque number written into the
1053transaction type column. Loses if you write cheques out of order."
1054 (interactive)
1055 (goto-char (point-max))
1056 (if (not (equal 0 (current-column)))
1057 (newline))
1058 (insert (format-time-string emoney-date-format))
1059 (move-to-tab-stop)
1060 (let (cheque cheque-number noinit)
1061 (save-excursion
1062 (if (search-backward-regexp emoney-cheque-type 0 t)
1063 (progn
1064 (setq cheque (buffer-substring (match-beginning 1) (match-end 1)))
1065 (setq noinit nil))
1066 (move-to-column emoney-type-column)
1067 (if (< (current-column) emoney-type-column)
1068 (indent-to-column emoney-type-column))
1069 (if emoney-uk-cheque-spelling
1070 (insert "chq 000001")
1071 (insert "chk 000001"))
1072 (setq noinit t)))
1073 (unless noinit
1074 (setq cheque-number (1+ (string-to-number cheque)))
1075 (move-to-column emoney-type-column)
1076 (if (< (current-column) emoney-type-column)
1077 (indent-to-column emoney-type-column))
1078 (if emoney-uk-cheque-spelling
1079 (insert (format "chq %06d" cheque-number))
1080 (insert (format "chk %06d" cheque-number))))
1081 (move-to-tab-stop)
1082 (insert "o")
1083 (move-to-tab-stop)
1084 (let ((before-descript (point))
1085 (start-column (current-column))
1086 (fill-column (- emoney-sign-column
1087 emoney-description-column
1088 1)))
1089 (save-excursion
1090 (insert (read-string "Payable To: "))
1091 (save-restriction
1092 (narrow-to-region before-descript (point))
1093 (goto-char before-descript)
1094 (while (progn (move-to-column fill-column) (not (eobp)))
1095 (search-forward " " nil t)
1096 (insert "\n"))
1097 (fill-paragraph 1)
1098 (goto-char before-descript)
1099 (forward-line 1)
1100 (while (progn (beginning-of-line) (not (eobp)))
1101 (indent-to-column start-column)
1102 (forward-line 1)))))
1103 (move-to-tab-stop)
1104 (insert "-")
1105 (move-to-tab-stop)
1106 (insert (read-string "Amount: "))
1107 (run-hooks 'emoney-transaction-hook)
1108 (run-hooks 'emoney-transaction-cheque-hook)))
1109
1110(defvar emoney-transfer-account-history nil)
1111
1112(defun emoney-transfer-funds (&optional from to amount)
1113 "Transfer funds from one eMoney account to another.
1114
1115Argument FROM is the account to transfer from.
1116Argument To is the account to transfer to.
1117Argument AMOUNT is how much to transfer."
1118 (interactive)
1119 (let ((from (or from
1120 (emoney-completing-read "Transfer from: "
1121 emoney-chart-of-accounts
1122 nil
1123 t
1124 (concat emoney-current-account-name
1125 ".emy")
1126 emoney-transfer-account-history)))
1127 (to (or to
1128 (emoney-completing-read "Transfer to: "
1129 emoney-chart-of-accounts nil t nil
1130 emoney-transfer-account-history)))
1131 (amount (or amount
1132 (read-number "Amount: ")))
1133 (current-ac (concat emoney-current-account-name ".emy")))
1134 (with-current-buffer from
1135 (emoney-append-transaction
1136 emoney-db-transfer-transaction-type
1137 (concat "T'fer to " (file-name-sans-extension to))
1138 amount))
1139 (with-current-buffer to
1140 (emoney-append-transaction
1141 emoney-cr-transfer-transaction-type
1142 (concat "T'fer from " (file-name-sans-extension from))
1143 amount))
1144 (loop for buf in '(from to)
1145 do (emoney-switch-to-account (symbol-value buf))
1146 do (emoney-recalculate-buffer))
1147 (run-hooks 'emoney-transaction-transfer-hook)
1148 (emoney-switch-to-account current-ac)))
1149
1150(defsubst emoney-build-type-regexp ()
1151 "Return a regular expression that will match any valid transaction type.
1152
1153This is done dynamically so users can redefine the valid transactions in
1154their `user-init-file' even after this file has been loaded."
1155 (emoney-build-types-list)
1156 (let ((types (append emoney-transaction-types
1157 (list emoney-cheque-type))))
1158 (concat
1159 "^\\("
1160 (mapconcat
1161 #'(lambda(x) x) types "\\|")
1162 "\\)[ \t]*$")))
1163
1164(defun emoney-check-transaction-type (line-start)
1165 "Check to make sure a valid transaction type has been used.
1166
1167Argument LINE-START is the starting point.
1168
1169Please note that the word \"check\" here means \"verify\" and it has
1170nothing to do with the American spelling of the word \"cheque\"."
1171 (let* ((type-regexp (emoney-build-type-regexp))
1172 (type-start (+ line-start emoney-type-column))
1173 (type-end (+ line-start emoney-clear-column))
1174 (type-string (buffer-substring type-start type-end)))
1175 (if (string-match type-regexp type-string)
1176 nil
1177 (error "Line %d, invalid type: %s"
1178 (1+ (emoney-current-line)) type-string))))
1179
1180(defun emoney-find-next-transaction ()
1181 "Find the next line that is a complete transaction.
1182
1183Return a list of the line start, numeric data start and line end
1184points."
1185 (let ((found nil)
1186 (line-regexp
1187 "^\\([0-90-9]\\)+.*$")
1188 line-start
1189 line-end
1190 data-start)
1191 (while (and
1192 (not found)
1193 (search-forward-regexp line-regexp (point-max) t))
1194 (setq line-start (match-beginning 0))
1195 (setq line-end (progn (end-of-line) (point)))
1196 (setq data-start (+ line-start emoney-sign-column))
1197 (if (> line-end data-start)
1198 (setq found t)))
1199 (if found
1200 (list line-start data-start line-end)
1201 nil)))
1202
1203(defun emoney-parse-transaction-data (data)
1204 "Return a list of floating point numbers from DATA.
1205
1206DATA is a string representing the sign, amount and optionally balance of a
1207transaction. Balance is nil if not present."
1208 (let ((data-regexp "\\([-+=]\\)[ \t]*\\([0-9.]+\\)?[ \t]*\\([-]?[0-9.]+\\)?")
1209 (balance nil)
1210 (reset nil)
1211 string sign amount)
1212 (string-match data-regexp data)
1213 (if (match-beginning 1)
1214 (setq sign (substring data (match-beginning 1) (match-end 1)))
1215 (error "Line %d, missing sign" (1+ (emoney-current-line))))
1216 (if (equal "=" sign)
1217 (progn
1218 (setq sign "+")
1219 (setq reset t)))
1220 (if (match-beginning 2)
1221 (progn
1222 (setq string (substring data (match-beginning 2) (match-end 2)))
1223 (setq amount (string-to-number (concat sign string))))
1224 (error "Line %d, missing amount" (1+ (emoney-current-line))))
1225 (if (match-beginning 3)
1226 (progn
1227 (setq string (substring data (match-beginning 3) (match-end 3)))
1228 (setq balance (string-to-number string))))
1229 (if reset (setq sign "="))
1230 (list sign amount balance)))
1231
1232(defun emoney-same (amount1 amount2)
1233 "Compare two dollar amounts, AMOUNT1 AMOUNT2, for equivalence."
1234 (let ((string1 (format "%10.2f" amount1))
1235 (string2 (format "%10.2f" amount2)))
1236 (equal string1 string2)))
1237
1238(defun emoney-form-transaction-data (sign amount balance)
1239 "Given SIGN, AMOUNT and a BALANCE, return a string.
1240
1241The string is suitable for placing in the numeric region of a
1242transaction, based on the defined input columns."
1243 (let* ((amount (abs amount))
1244 (width1 (- emoney-amount-column emoney-sign-column))
1245 (width2 (- emoney-current-balance-column emoney-amount-column))
1246 (len (length emoney-largest-balance))
1247 (gap (- width2 len))
1248 (value (concat "%" (number-to-string len) ".2f"))
1249 (format-string (concat "%-" (number-to-string width1)
1250 "s" value
1251 "%" (number-to-string gap) "s" value)))
1252 (format format-string sign amount "" balance)))
1253
1254(defun emoney-recalculate (start end)
1255 "Recalculate the balances for region START END.
1256
1257The final balance, uncleared total, and the number of balances that
1258changed, and the transaction count are returned in a list."
1259 (run-hooks 'emoney-recalculate-before-hook)
1260 (let ((current-balance 0)
1261 (changes 0)
1262 (uncleared 0)
1263 (transactions 0)
1264 line-points)
1265 (save-excursion
1266 (save-restriction
1267 (narrow-to-region start end)
1268 (untabify (point-min) (point-max))
1269 (goto-char (point-min))
1270 (while (setq line-points (emoney-find-next-transaction))
1271 (setq transactions (1+ transactions))
1272 (let* ((line-start (nth 0 line-points))
1273 (data-start (nth 1 line-points))
1274 (data-end (nth 2 line-points))
1275 (clear-flag (buffer-substring
1276 (+ line-start emoney-clear-column)
1277 (+ 1 line-start emoney-clear-column)))
1278 (data-string (buffer-substring data-start data-end))
1279 (data-values (emoney-parse-transaction-data data-string))
1280 (sign (nth 0 data-values))
1281 (amount (nth 1 data-values))
1282 (balance (nth 2 data-values))
1283 (new-balance (if (equal sign "=")
1284 amount
1285 (+ current-balance amount)))
1286 (new-uncleared (if (equal clear-flag "x")
1287 uncleared
1288 (+ uncleared amount)))
1289 (new-string
1290 (emoney-form-transaction-data sign amount new-balance)))
1291 (emoney-check-transaction-type line-start)
1292 (setq current-balance new-balance)
1293 (setq uncleared new-uncleared)
1294 (if (or (null balance) (not (emoney-same balance new-balance)))
1295 (setq changes (1+ changes)))
1296 (if (not (equal data-string new-string))
1297 (progn
1298 (delete-region data-start data-end)
1299 (goto-char data-start)
1300 (insert new-string)))))
1301 (widen)))
1302 (run-hooks 'emoney-recalculate-after-hook)
1303 (list current-balance uncleared changes transactions)))
1304
1305(defvar emoney-is-exiting nil
1306 "Non-nil when eMoney is in the process of quitting.")
1307
1308(defun emoney-update-acc-buf-bal (account balance)
1309 "Update ACCOUNT BALANCE in accounts buffer."
1310 (with-current-buffer emoney-accounts-buffer
1311 (goto-char (point-min))
1312 (search-forward account)
1313 (kill-line)
3d7af7e7 1314 (move-to-column 18 'force)
a7161eda
SY
1315 (insert-face
1316 (format (concat "%"
1317 (number-to-string (- (length emoney-largest-balance)
1318 (length (format "%.2f" balance))))
1319 "s$%.2f") "" balance)
1320 (if (< balance 0)
1321 'emoney-debit-face
1322 'emoney-credit-face))))
1323
1324(defun emoney-recalculate-buffer ()
1325 "Recalculate the current buffer.
1326
1327See `emoney-recalculate'."
1328 (interactive)
1329 (let* ((result (emoney-recalculate (point-min) (point-max)))
1330 (balance (nth 0 result))
1331 (uncleared (nth 1 result))
1332 (changes (nth 2 result))
1333 (total (nth 3 result)))
1334 (when emoney-save-after-recalculate
1335 (save-buffer (current-buffer)))
1336 (emoney-update-acc-buf-bal emoney-current-account-name balance)
1337 (if emoney-is-exiting
1338 (message
1339 (format "book bal %.2f unclrd %.2f bank bal %.2f (%d/%d recalcs)"
1340 balance uncleared (- balance uncleared) changes total))
1341 (message-or-box
1342 (format "book bal %.2f unclrd %.2f bank bal %.2f (%d/%d recalcs)"
1343 balance uncleared (- balance uncleared) changes total)))
1344 (if (> changes 0)
1345 (end-of-line))))
1346
1347(defun emoney-recalculate-region (start end)
1348 "Recalculate the current region, START END.
1349
1350See `emoney-recalculate'."
1351 (interactive "r")
1352 (let* ((result (emoney-recalculate start end))
1353 (balance (nth 0 result))
1354 (uncleared (nth 1 result))
1355 (changes (nth 2 result))
1356 (total (nth 3 result)))
1357 (message-or-box (format "Region balance %.2f uncleared %.2f (%d/%d recalcs)"
1358 balance uncleared changes total))
1359 (if (> changes 0)
1360 (end-of-line))))
1361
1362(defun emoney-summarise-cheques (start end)
1363 "Create a buffer that lists only the cheques in the specified region.
1364
1365The region is denoted by START END.
1366
1367The list is sorted on cheque number. Breaks in sequence are denoted by lines
1368containing an asterisk between the cheques where the break occurs. The buffer
1369is also recalculated, thus showing to total of the cheques summarised."
1370 (let ((emoney-buffer (current-buffer))
1371 (summary-buffer (get-buffer-create "*cheque summary*"))
1372 (cheque-count 0)
1373 (sequence-breaks 0)
1374 line-points)
1375 (save-excursion
1376 (save-restriction
1377 (set-buffer summary-buffer)
1378 (delete-region (point-min) (point-max))
1379 (set-buffer emoney-buffer)
1380 (narrow-to-region start end)
1381 (goto-char (point-min))
1382 (while (setq line-points (emoney-find-next-transaction))
1383 (let* ((line-start (nth 0 line-points))
1384 (line-end (nth 2 line-points))
1385 (type-start (+ line-start emoney-type-column))
1386 (type-end (+ line-start emoney-description-column))
1387 (type-string (buffer-substring type-start type-end)))
1388 (if (string-match emoney-cheque-type type-string)
1389 (progn
1390 (append-to-buffer
1391 summary-buffer line-start (1+ line-end))
1392 (setq cheque-count (1+ cheque-count))))))
1393 (widen)
1394 (set-buffer summary-buffer)
1395 (sort-numeric-fields 3 (point-min) (point-max))
1396 (setq sequence-breaks (emoney-find-sequence-breaks))
1397 (goto-char (point-max))
1398 (insert (format "\n%d cheque%s summarised, %d sequence break%s\n"
1399 cheque-count
1400 (if (equal 1 cheque-count) "" "s")
1401 sequence-breaks
1402 (if (equal 1 sequence-breaks) "" "s")))
1403 (set-buffer emoney-buffer)))
1404 (with-electric-help
1405 '(lambda ()
1406 (insert
1407 (with-temp-buffer
1408 (erase-buffer)
1409 (insert-buffer summary-buffer)
1410 (buffer-string (current-buffer)))))
1411 "*cheque summary*"))
1412 (run-hooks 'emoney-summarise-cheques-hook))
1413
1414(defun emoney-find-sequence-breaks ()
1415 "Find cheque sequence breaks in the current cheque summary buffer.
1416
1417Mark breaks in sequence by inserting a line with an asterisk between
1418the offending cheques. Return the count of sequence breaks found."
1419 (let ((last-cheque nil)
1420 (sequence-breaks 0))
1421 (goto-char (point-min))
1422 (while (search-forward-regexp
1423 (concat "\\([0-9/]+\\)[ \t]+" emoney-cheque-type)
1424 (point-max) t)
1425 (let* ((cheque-start (match-beginning 2))
1426 (cheque-end (match-end 2))
1427 (cheque-string (buffer-substring cheque-start cheque-end))
1428 (cheque-number (string-to-int cheque-string)))
1429 (if (not last-cheque)
1430 (setq last-cheque cheque-number)
1431 (if (not (equal cheque-number (1+ last-cheque)))
1432 (progn
1433 (setq sequence-breaks (1+ sequence-breaks))
1434 (beginning-of-line)
1435 (open-line 1)
1436 (insert "*")
1437 (next-line 2)))
1438 (setq last-cheque cheque-number))))
1439 sequence-breaks))
1440
1441(defun emoney-summarise-cheques-buffer ()
1442 "Summarise the cheque transactions in the current buffer."
1443 (interactive)
1444 (emoney-summarise-cheques (point-min) (point-max))
1445 (emoney-recalculate (point-min) (point-max)))
1446
1447(defun emoney-summarise-cheques-region (start end)
1448 "Summarise the cheque transactions in region START - END."
1449 (interactive "r")
1450 (emoney-summarise-cheques start end)
1451 (emoney-recalculate (point-min) (point-max)))
1452
1453(defun emoney-new-account (&optional name balance)
1454 "*Create a new A/C named NAME for use with eMoney."
1455 (interactive)
1456 (let ((new-acc (concat (or name
1457 (read-string "New A/C Name: ")) ".emy"))
1458 (bal (or balance
1459 (read-number "Initial Balance: " nil 0))))
1460 (find-file-noselect
1461 (expand-file-name new-acc emoney-accounts-directory))
1462 (setq emoney-chart-of-accounts
1463 (push new-acc emoney-chart-of-accounts))
1464 (select-window (get-buffer-window
1465 (concat emoney-current-account-name ".emy")))
1466 (switch-to-buffer new-acc)
1467 (setq emoney-current-account-name
1468 (file-name-sans-extension new-acc))
1469 (emoney-append-transaction "init" "Opening Balance" bal)
1470 (emoney-recalculate-buffer)
1471 (emoney-show-buffers)
1472 (switch-to-buffer new-acc)
1473 (goto-char (point-max))
1474 (run-hooks 'emoney-new-account-hook)))
1475
1476(defun emoney-setup-control-buffer ()
1477 "Set up the eMoney \"Control\" buffer."
1478 (let ((buf emoney-control-buffer))
1479 (save-excursion
1480 (when (buffer-live-p (get-buffer-create buf))
1481 (kill-buffer buf))
1482 (set-buffer (get-buffer-create buf))
1483 (widget-create 'push-button
1484 :notify (lambda (&rest ignore)
1485 (emoney-new-account))
1486 :help-echo "Create a new eMoney account."
1487 " New A/C ")
1488 (widget-insert " ")
1489 (widget-create 'push-button
1490 :notify (lambda (&rest ignore)
1491 (save-excursion
1492 (set-buffer
1493 (concat emoney-current-account-name ".emy"))
1494 (emoney-append-transaction)))
1495 :help-echo "Add a new transaction\n
1496If you want to add a cheque transaction
1497use \"Add Chq\" instead."
1498 "Add Trans")
1499 (widget-insert " ")
1500 (widget-create 'push-button
1501 :notify (lambda (&rest ignore)
1502 (save-excursion
1503 (set-buffer
1504 (concat emoney-current-account-name ".emy"))
1505 (emoney-append-next-cheque)))
1506 :help-echo (if emoney-uk-cheque-spelling
1507 "Add a new cheque transaction."
1508 "Add a new check transaction.")
1509 (if emoney-uk-cheque-spelling
1510 " Add Chq "
1511 " Add Chk "))
1512 (widget-insert " ")
1513 (widget-create 'push-button
1514 :notify (lambda (&rest ignore)
1515 (save-excursion
1516 (set-buffer
1517 (concat emoney-current-account-name ".emy"))
1518 (emoney-recalculate-buffer)))
1519 :help-echo "Record the last transaction.\n
1520Also recalculates the buffer."
1521 "End Trans")
1522 (widget-insert "\n\n")
1523 (widget-create 'push-button
1524 :notify (lambda (&rest ignore)
1525 (save-excursion
1526 (set-buffer
1527 (concat emoney-current-account-name ".emy"))
1528 (emoney-transfer-funds)))
1529 :help-echo "Transfer funds between accounts."
1530 "Transfer ")
1531 (widget-insert " ")
1532 (widget-create 'push-button
1533 :notify (lambda (&rest ignore)
1534 (save-excursion
1535 (set-buffer
1536 (concat emoney-current-account-name ".emy"))
1537 (emoney-summarise-cheques-buffer)))
1538 :help-echo (if emoney-uk-cheque-spelling
1539 "Display a summary of cheques."
1540 "Display a summary of checks.")
1541 (if emoney-uk-cheque-spelling
1542 " Chq Sum "
1543 " Chk Sum "))
1544 (widget-insert " ")
1545 (widget-create 'push-button
1546 :notify (lambda (&rest ignore)
1547 (emoney-calc))
1548 :help-echo "Start the Emacs Calculator.\n
1549So you can count up all
1550of your millions!"
1551 " Calc ")
1552 (widget-insert " ")
1553 (widget-create 'push-button
1554 :notify (lambda (&rest ignore)
1555 (if emoney-is-beta
1556 (message-or-box
1557 "eMoney: %s, \"%s\" [Beta]"
1558 emoney-version emoney-codename)
1559 (message-or-box
1560 "eMoney: %s, \"%s\""
1561 emoney-version emoney-codename)))
1562 :help-echo "Display eMoney version info."
1563 " Version ")
1564 (widget-insert "\n\n")
1565 (widget-create 'push-button
1566 :notify (lambda (&rest ignore)
1567 (emoney-go-to-bank))
1568 :help-echo "Open your bank's web site in your browser."
1569 " Bank ")
1570 (widget-insert " ")
1571 (widget-create 'push-button
1572 :notify (lambda (&rest ignore)
1573 (emoney-quit))
1574 :help-echo "Exit eMoney."
1575 " Exit ")
1576 (set-specifier horizontal-scrollbar-visible-p nil (current-buffer))
1577 (set-specifier vertical-scrollbar-visible-p nil (current-buffer))
1578 (set-specifier has-modeline-p nil (current-buffer))
1579 (run-hooks 'emoney-setup-control-buffer-hook))))
1580
1581(defun emoney-show-buffers ()
1582 "Display all the eMoney buffers."
1583 (emoney-setup-accounts-buffer)
1584 (emoney-setup-control-buffer)
1585 (emoney-setup-header-buffer)
1586 (delete-other-windows nil)
1587 (switch-to-buffer emoney-accounts-buffer)
1588 (split-window nil emoney-accounts-buffer-height)
1589 (split-window nil emoney-accounts-buffer-width t)
1590 (other-window 1)
1591 (switch-to-buffer emoney-control-buffer)
1592 (other-window 1)
1593 (switch-to-buffer emoney-header-buffer)
1594 (split-window nil emoney-header-buffer-height)
1595 (other-window 1))
1596
1597(defun emoney-quit ()
1598 "*Exit from eMoney, optionally recalculating all accounts first.
1599
1600To have all of your accounts recalculated before eMoney exits set
1601`emoney-recalculate-on-quit' to `t'
1602
1603If `emoney-save-after-recalculate' is also `t' the account buffers
1604will be saved before eMoney exits."
1605 (interactive)
1606 (run-hooks 'emoney-quit-before-hook)
1607 (let ((accounts emoney-chart-of-accounts))
1608 (setq emoney-is-exiting t)
1609 (dolist (buf accounts)
1610 (when emoney-recalculate-on-quit
1611 (set-buffer buf)
1612 (emoney-recalculate-buffer))
1613 (kill-buffer buf))
1614 (kill-buffer emoney-accounts-buffer)
1615 (kill-buffer emoney-control-buffer)
1616 (kill-buffer emoney-header-buffer)
1617 (run-hooks 'emoney-quit-after-hook)
1618 (when (and emoney-use-new-frame
1619 (frame-live-p emoney-frame))
1620 (delete-frame emoney-frame))
1621 (setq emoney-frame nil)
1622 (unless emoney-use-new-frame
1623 (jump-to-register ?\$))
1624 (setq emoney-is-exiting nil)))
1625
1626(defun emoney-recalc-and-exit ()
1627 "*Exit form eMoney, recalculating all accounts first."
1628 (interactive)
1629 (let ((old-recalc-val emoney-recalculate-on-quit))
1630 (setq emoney-recalculate-on-quit t)
1631 (emoney-quit)
1632 (setq emoney-recalculate-on-quit old-recalc-val)))
1633
1634;;;###autoload
1635(defun emoney ()
1636 "*Start a new eMoney session."
1637 (interactive)
1638 (unless emoney-use-new-frame
1639 (window-configuration-to-register ?\$))
1640 (unless (frame-live-p emoney-frame)
1641 (setq emoney-frame
1642 (if emoney-use-new-frame
3d7af7e7
SY
1643 ;; FIXME: make the frame props customisable
1644 (make-frame '((name . "eMoney")
1645 (height . 40)
1646 (width . 80)))
a7161eda
SY
1647 (selected-frame))))
1648 (select-frame emoney-frame)
1649 (let ((accounts emoney-chart-of-accounts)
1650 (dir emoney-accounts-directory))
1651 (while accounts
1652 (find-file (expand-file-name (car accounts) dir))
1653 (unless (eq major-mode 'emoney-mode)
1654 (emoney-mode))
1655 (setq accounts (cdr accounts)))
1656 (setq emoney-current-account-name
1657 (file-name-sans-extension emoney-default-account))
1658 (emoney-show-buffers)
1659 (switch-to-buffer emoney-default-account)
1660 (goto-char (point-max)))
1661 (focus-frame emoney-frame))
1662
1663;; Work around a problem with Emacs calc. If you start calc in a
1664;; frame with multiple buffers visible when calc exits it doesn't
1665;; return point to the place it was when you called calc. These
1666;; advices overcome that.
1667(defadvice calc (before em-calc-win-save first activate)
1668 "Before starting calc, save the window config.
1669This is so we can restore the window config when calc exits because
1670calc doesn't DTRT in this regard by itself."
1671 (push-window-configuration))
1672
1673(defadvice calc-quit (after em-calc-win-restore last activate)
1674 "Restore the \"pre calc\" window config on calc exit."
1675 (pop-window-configuration))
1676
1677
1678;;;###autoload(add-to-list 'auto-mode-alist '("\\.emy$" . emoney-mode))
1679
1680(provide 'emoney)
1681
1682;;; emoney.el ends here