Personal finances application for Emacs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

185 lines
5.5 KiB

  1. ;;; elbank-budget.el --- Elbank budgeting functionality -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2017-2018 Nicolas Petton
  3. ;; Author: Nicolas Petton <nicolas@petton.fr>
  4. ;; This program is free software; you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation, either version 3 of the License, or
  7. ;; (at your option) any later version.
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;; You should have received a copy of the GNU General Public License
  13. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. ;;; Commentary:
  15. ;;
  16. ;;; Code:
  17. (require 'seq)
  18. (require 'map)
  19. (require 'cl-lib)
  20. (require 'elbank-common)
  21. (require 'elbank-progressbar)
  22. (declare-function elbank-report "elbank-report.el")
  23. ;;;###autoload
  24. (defgroup elbank-budget nil
  25. "Elbank budget settings"
  26. :group 'elbank)
  27. ;;;###autoload
  28. (defcustom elbank-budget nil
  29. "Monthly budget by category of transactions.
  30. Keys are category names as defined in `elbank-categories'."
  31. :type '(alist :key-type (string :tag "Category name")
  32. :value-type (number :tag "Monthly budget")))
  33. (defvar elbank-budget-mode-map
  34. (copy-keymap elbank-base-report-mode-map)
  35. "Keymap for `elbank-budget-mode'.")
  36. (define-derived-mode elbank-budget-mode elbank-base-report-mode "Elbank Budget"
  37. "Major mode for viewing a monthly budget.
  38. \\{elbank-budget-mode-map}"
  39. (add-hook 'elbank-base-report-refresh-hook 'elbank-budget-refresh nil t))
  40. ;;;###autoload
  41. (defun elbank-budget-report ()
  42. "Build a budget report for the last month.
  43. Return the report buffer."
  44. (interactive)
  45. (let ((buf (get-buffer-create "*elbank budget report*")))
  46. (pop-to-buffer buf)
  47. (elbank-budget-mode)
  48. (setq elbank-report-period
  49. `(month ,(car (last (elbank-transaction-months)))))
  50. (elbank-budget-refresh)
  51. buf))
  52. (defun elbank-budget-refresh ()
  53. "Update the budget report."
  54. (let ((inhibit-read-only t)
  55. (elbank-budget-data (elbank-budget--get-data)))
  56. (erase-buffer)
  57. (elbank-budget--insert-header)
  58. (elbank-budget--insert-customize-link)
  59. (seq-do #'elbank-budget--insert-line
  60. (seq-sort (lambda (a b)
  61. (string< (car a) (car b)))
  62. elbank-budget-data))
  63. (elbank-budget--insert-footer elbank-budget-data)))
  64. (defun elbank-budget--insert-header ()
  65. "Insert the header for the budget buffer."
  66. (insert (format "Budget report for %s"
  67. (elbank-format-period elbank-report-period)))
  68. (put-text-property (point-at-bol) (point)
  69. 'face 'elbank-header-face)
  70. (insert "\n\n"))
  71. (defun elbank-budget--insert-footer (data)
  72. "Insert the footer with a summary of the budget DATA."
  73. (insert "\n")
  74. (insert " Total: ")
  75. (let ((beg (point)))
  76. (insert (format "%.2f"
  77. (seq-reduce #'+
  78. (seq-map #'cadr data)
  79. 0)))
  80. (put-text-property beg (point) 'face 'bold))
  81. (insert " of ")
  82. (let ((beg (point)))
  83. (insert (format "%.2f"
  84. (seq-reduce #'+
  85. (seq-map #'cl-caddr data)
  86. 0)))
  87. (put-text-property beg (point) 'face 'bold))
  88. (insert " budgeted."))
  89. (defun elbank-budget--insert-customize-link ()
  90. "Insert a button to customize `elbank-budget'."
  91. (insert "[Customize budgets]")
  92. (make-text-button (point-at-bol) (point)
  93. 'follow-link t
  94. 'action (lambda (&rest _)
  95. (customize-variable 'elbank-budget)))
  96. (insert "\n\n"))
  97. (defun elbank-budget--insert-line (budget-entry)
  98. "Insert the value for BUDGET-ENTRY with a progress bar."
  99. (let* ((label (car budget-entry))
  100. (spent (cadr budget-entry))
  101. (budgeted (cl-caddr budget-entry))
  102. (percentage (if (zerop budgeted)
  103. 100
  104. (round (* 100 (/ spent budgeted))))))
  105. (elbank-budget--insert-line-header label)
  106. (elbank-insert-progressbar percentage 40)
  107. (insert (format " %.2f of %.2f budgeted" spent budgeted))
  108. (insert "\n\n")))
  109. (defun elbank-budget--insert-line-header (label)
  110. "Insert a header with LABEL for a budget category."
  111. (let ((width (1+ (seq-reduce (lambda (acc cat)
  112. (max (seq-length (car cat)) acc))
  113. elbank-budget
  114. 0))))
  115. (dotimes (_ (- width (seq-length label)))
  116. (insert " "))
  117. (let ((beg (point)))
  118. (insert (format "%s" label))
  119. (make-text-button beg (point)
  120. 'follow-link t
  121. 'action (lambda (&rest _)
  122. (elbank-report :period elbank-report-period
  123. :category label))))
  124. (insert " ")))
  125. (defun elbank-budget--get-data ()
  126. "Return an assocation list of budget data.
  127. Keys are budgeted categories.
  128. Values are lists of spent amounts and budgeted amounts for a category."
  129. (let ((data (map-apply (lambda (key val)
  130. `(,key ,(- (elbank-sum-transactions val))
  131. ,(map-elt elbank-budget key)))
  132. (elbank-budget--transactions-by-budget))))
  133. ;; Add budgeted categories without transaction.
  134. (seq-do (lambda (budget)
  135. (unless (map-elt data (car budget))
  136. (map-put data (car budget) `(0 ,(cdr budget)))))
  137. elbank-budget)
  138. data))
  139. (defun elbank-budget--transactions-by-budget ()
  140. "Return an assocation list of all transactions grouped by budget category."
  141. (seq-filter
  142. (lambda (elt)
  143. (not (null (car elt))))
  144. (seq-group-by
  145. (lambda (trans)
  146. (seq-some
  147. (lambda (cat)
  148. (when (elbank-transaction-in-category-p trans cat)
  149. cat))
  150. (seq-map #'car elbank-budget)))
  151. (elbank-filter-transactions (elbank-all-transactions)
  152. :period elbank-report-period))))
  153. (provide 'elbank-budget)
  154. ;;; elbank-budget.el ends here