process-financisto.lisp 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. (defvar *financisto-backup-path* #P"/home/enikesha/dev/lisp/timeliner/")
  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 (float (/ (parse-integer (getf e :from-amount)) from-decimals)))
  50. (from-balance (float (/ (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
  54. (float (/ (parse-integer (getf e :original-from-amount))
  55. (expt 10 (parse-integer (getf original-currency :decimals)))))))
  56. (payee (gethash (parse-integer (getf e :payee-id)) (gethash "payee" db)))
  57. (category (gethash (parse-integer (getf e :category-id)) (gethash "category" db)))
  58. (to-account (gethash (parse-integer (getf e :to-account-id)) accounts))
  59. (to-currency (and to-account (gethash (parse-integer (getf to-account :currency-id)) currencies)))
  60. (to-decimals (and to-account (expt 10 (parse-integer (getf to-currency :decimals)))))
  61. (to-amount (and to-account (float (/ (parse-integer (getf e :to-amount)) to-decimals))))
  62. (to-balance (and to-account (float (/ (gethash (parse-integer (getf to-account :-id)) balances 0) to-decimals))))
  63. (transfer (cond
  64. ((and to-account (equal (getf from-account :type)
  65. "LIABILITY")
  66. (< from-balance 0)) :borrow)
  67. ((and to-account (equal (getf to-account :type)
  68. "LIABILITY")
  69. (<= to-balance 0)) :repay)
  70. ((and to-account (equal (getf to-account :type)
  71. "LIABILITY")
  72. (> to-balance 0)) :lend)
  73. ((and to-account (equal (getf from-account :type)
  74. "LIABILITY")
  75. (>= from-balance 0)) :repaid)
  76. (to-account :transfer)
  77. (t nil)))
  78. (title (if transfer
  79. (format
  80. nil "~A ~$~A from ~A (bal ~$~A) to ~A~:[~*~;~:* ~$~A~] (bal ~$~A)~@[ (~A)~]"
  81. (ecase transfer
  82. (:borrow "Borrowed")
  83. (:repay "Repaid")
  84. (:lend "Lend")
  85. (:repaid "Got repaid")
  86. (:transfer "Transferred"))
  87. (abs from-amount) (getf from-currency :symbol)
  88. (getf from-account :title)
  89. from-balance (getf from-currency :symbol)
  90. (getf to-account :title)
  91. (and (not (equal (getf from-account :currency-id)
  92. (getf to-account :currency-id)))
  93. (abs to-amount))
  94. (getf to-currency :symbol)
  95. to-balance (getf to-currency :symbol)
  96. (getf e :note))
  97. (format
  98. nil "~A ~$~A~:[~*~;~:* (~$~A)~] ~A ~A~@[ for ~A~]~@[ at ~A~]~@[ (~A)~]. Balance ~$~A"
  99. (if spend "Spend" "Earned")
  100. (abs from-amount) (getf from-currency :symbol)
  101. (and original-from-amount (abs original-from-amount))
  102. (getf original-currency :symbol)
  103. (if spend "from" "to")
  104. (getf from-account :title)
  105. (and category (not (equal (getf category :-id) "0")) (getf category :title))
  106. (and payee (getf payee :title))
  107. (getf e :note)
  108. from-balance (getf from-currency :symbol))))
  109. (type (if transfer (string-downcase (string transfer)) (if spend "spend" "earned")))
  110. (financisto (if transfer
  111. (kv "transfer" (kv
  112. (kv "from_account" (getf from-account :title))
  113. (kv "from_amount" from-amount)
  114. (kv "from_currency" (getf from-currency :name))
  115. (kv "from_balance" from-balance)
  116. (kv "to_account" (getf to-account :title))
  117. (kv "to_amount" to-amount)
  118. (kv "to_currency" (getf to-currency :name))
  119. (kv "to_balance" to-balance)))
  120. (kv "transaction" (kv
  121. (kv "account" (getf from-account :title))
  122. (kv "amount" from-amount)
  123. (kv "currency" (getf from-currency :name))
  124. (kv "balance" from-balance)
  125. (kv "original_amount" original-from-amount)
  126. (kv "original_currency" (getf original-currency :name)))))))
  127. (kv
  128. (kv "ts" (parse-integer (getf e :datetime)))
  129. (kv "type" "finance")
  130. (kv "title" title)
  131. (kv "financisto"
  132. (kv
  133. (kv "id" (parse-integer (getf e :-id)))
  134. (kv "type" type)
  135. (kv "payee" (getf payee :title))
  136. (kv "category" (and category (not (equal (getf category :-id) "0")) (getf category :title)))
  137. (kv "note" (getf e :note))
  138. financisto)))))
  139. (defun financisto-import ()
  140. (let* ((filename (find-last-backup))
  141. (db (load-backup filename))
  142. (transactions (sort (loop for entity being the hash-values of (gethash "transactions" db)
  143. when (not (string= (getf entity :category-id) "-1"))
  144. collect entity)
  145. #'<
  146. :key #'(lambda (i) (parse-integer (getf i :datetime)))))
  147. (balances (make-hash-table :size (hash-table-count (gethash "account" db)))))
  148. (format t "Got ~A with ~A transactions~%" filename (length transactions))
  149. (dolist (entity transactions)
  150. (destructuring-bind (&key from-account-id from-amount
  151. to-account-id to-amount
  152. -id datetime
  153. &allow-other-keys) entity
  154. (incf (gethash (parse-integer from-account-id) balances 0) (parse-integer from-amount))
  155. (when (not (string= to-account-id "0"))
  156. (incf (gethash (parse-integer to-account-id) balances 0) (parse-integer to-amount)))
  157. (let* ((new-doc (make-finance-doc entity db balances))
  158. (existing-doc (first (docs (db.find "events"
  159. (kv ($ "type" "finance")
  160. ($ "financisto.id" (parse-integer -id)))))))
  161. (new-ts (parse-integer datetime)))
  162. (cl-mongo::kv-container-add
  163. (kv "loc"
  164. (if (and existing-doc (= new-ts (get-element "ts" existing-doc)))
  165. (get-element "loc" existing-doc)
  166. (point->doc (find-location-at new-ts))))
  167. new-doc)
  168. (if existing-doc
  169. (progn
  170. (cl-mongo::kv-container-add (kv "_id" (doc-id existing-doc)) new-doc)
  171. (db.save "events" new-doc))
  172. (db.insert "events" new-doc)))))))