process-financisto.lisp 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. (defvar *financisto-backup-path* #P"/home/enikesha/Documents/backups/financisto/")
  2. (defun find-last-backup ()
  3. (first (sort (mapcar #'namestring
  4. (directory
  5. (make-pathname
  6. :name :wild
  7. :type "backup"
  8. :defaults *financisto-backup-path*)))
  9. #'string>)))
  10. (defun starts-with (str with)
  11. (string= (subseq str 0 (min (length str)
  12. (length with)))
  13. with))
  14. (defmacro aget (key alist)
  15. `(cdr (assoc ,key ,alist :test #'string=)))
  16. (defun make-keyword (name) (values (intern (string-upcase name) "KEYWORD")))
  17. (defun load-backup (filename)
  18. (gzip-stream:with-open-gzip-file (stream filename)
  19. (setq stream (flexi-streams:make-flexi-stream stream :external-format :utf-8))
  20. (loop
  21. for line = (read-line stream nil)
  22. with data = (make-hash-table :test 'equal) and table and entity and table-name
  23. while line
  24. do (cond
  25. ((starts-with line "$ENTITY:")
  26. (setf table-name (subseq line 8)
  27. table (gethash table-name data (make-hash-table :test 'equal))
  28. (gethash table-name data) table
  29. entity nil))
  30. ((string= line "$$")
  31. (if (getf entity :-id)
  32. (setf (gethash (parse-integer (getf entity :-id)) table) entity
  33. table-name nil)
  34. (progn
  35. (when (eql (type-of table) 'hash-table)
  36. (setf table nil))
  37. (setf (gethash table-name data) (push entity table)
  38. table-name nil))))
  39. (table-name (let ((i (position #\: line)))
  40. (setf (getf entity (make-keyword (substitute #\- #\_ (subseq line 0 i))))
  41. (subseq line (1+ i))))))
  42. finally (return data))))
  43. (defun make-finance-doc (e db balances)
  44. (let* ((accounts (gethash "account" db))
  45. (currencies (gethash "currency" db))
  46. (from-account (gethash (parse-integer (getf e :from-account-id)) accounts))
  47. (from-currency (gethash (parse-integer (getf from-account :currency-id)) currencies))
  48. (from-decimals (expt 10 (parse-integer (getf from-currency :decimals))))
  49. (from-amount (/ (parse-integer (getf e :from-amount)) from-decimals))
  50. (from-balance (/ (gethash (parse-integer (getf from-account :-id)) balances 0) from-decimals))
  51. (spend (< from-amount 0))
  52. (original-currency (gethash (parse-integer (getf e :original-currency-id)) currencies))
  53. (original-from-amount (and original-currency (/ (parse-integer (getf e :original-from-amount)) (expt 10 (parse-integer (getf original-currency :decimals))))))
  54. (payee (gethash (parse-integer (getf e :payee-id)) (gethash "payee" db)))
  55. (category (gethash (parse-integer (getf e :category-id)) (gethash "category" db)))
  56. (to-account (gethash (parse-integer (getf e :to-account-id)) accounts))
  57. (to-currency (and to-account (gethash (parse-integer (getf to-account :currency-id)) currencies)))
  58. (to-decimals (and to-account (expt 10 (parse-integer (getf to-currency :decimals)))))
  59. (to-amount (and to-account (/ (parse-integer (getf e :to-amount)) to-decimals)))
  60. (to-balance (and to-account (/ (gethash (parse-integer (getf to-account :-id)) balances 0) to-decimals)))
  61. (transfer (cond
  62. ((and to-account (equal (getf from-account :type)
  63. "LIABILITY")
  64. (< from-balance 0)) :borrow)
  65. ((and to-account (equal (getf to-account :type)
  66. "LIABILITY")
  67. (<= to-balance 0)) :repay)
  68. ((and to-account (equal (getf to-account :type)
  69. "LIABILITY")
  70. (> to-balance 0)) :lend)
  71. ((and to-account (equal (getf from-account :type)
  72. "LIABILITY")
  73. (>= from-balance 0)) :repaid)
  74. (to-account :transfer)
  75. (t nil)))
  76. (title (if transfer
  77. (format
  78. nil "~A ~$~A from ~A (bal ~$~A) to ~A~:[~*~;~:* ~$~A~] (bal ~$~A)~@[ (~A)~]"
  79. (ecase transfer
  80. (:borrow "Borrowed")
  81. (:repay "Repaid")
  82. (:lend "Lend")
  83. (:repaid "Got repaid")
  84. (:transfer "Transferred"))
  85. (abs from-amount) (getf from-currency :symbol)
  86. (getf from-account :title)
  87. from-balance (getf from-currency :symbol)
  88. (getf to-account :title)
  89. (and (not (equal (getf from-account :currency-id)
  90. (getf to-account :currency-id)))
  91. (abs to-amount))
  92. (getf to-currency :symbol)
  93. to-balance (getf to-currency :symbol)
  94. (getf e :note))
  95. (format
  96. nil "~A ~$~A~:[~*~;~:* (~$~A)~] ~A ~A~@[ for ~A~]~@[ at ~A~]~@[ (~A)~]. Balance ~$~A"
  97. (if spend "Spend" "Earned")
  98. (abs from-amount) (getf from-currency :symbol)
  99. (and original-from-amount (abs original-from-amount))
  100. (getf original-currency :symbol)
  101. (if spend "from" "to")
  102. (getf from-account :title)
  103. (and category (not (equal (getf category :-id) "0")) (getf category :title))
  104. (and payee (getf payee :title))
  105. (getf e :note)
  106. from-balance (getf from-currency :symbol))))
  107. (type (if transfer (string-downcase (string transfer)) (if spend "spend" "earned"))))
  108. (kv
  109. (kv "ts" (parse-integer (getf e :datetime)))
  110. (kv "type" "finance")
  111. (kv "title" title)
  112. (kv "financisto"
  113. (kv
  114. (kv "id" (parse-integer (getf e :-id)))
  115. (kv "type" type)
  116. ))
  117. )
  118. ))
  119. (defun financisto-import ()
  120. (let* ((filename (find-last-backup))
  121. (db (load-backup filename))
  122. (transactions (sort (loop for entity being the hash-values of (gethash "transactions" db)
  123. when (not (string= (getf entity :category-id) "-1"))
  124. collect entity)
  125. #'<
  126. :key #'(lambda (i) (parse-integer (getf i :datetime)))))
  127. (balances (make-hash-table :size (hash-table-count (gethash "account" db)))))
  128. (format t "Got ~A with ~A transactions" filename (length transactions))
  129. (dolist (entity transactions)
  130. (destructuring-bind (&key from-account-id from-amount
  131. to-account-id to-amount
  132. &allow-other-keys) entity
  133. (incf (gethash (parse-integer from-account-id) balances 0) (parse-integer from-amount))
  134. (when (not (string= to-account-id "0"))
  135. (incf (gethash (parse-integer to-account-id) balances 0) (parse-integer to-amount)))
  136. ))
  137. (loop for k being the hash-keys in balances using (hash-value v)
  138. collect (list k (getf (gethash k (gethash "account" db)) :title) v))))