| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- (defvar *financisto-backup-path* #P"/home/enikesha/Documents/backups/financisto/")
- (defun find-last-backup ()
- (first (sort (mapcar #'namestring
- (directory
- (make-pathname
- :name :wild
- :type "backup"
- :defaults *financisto-backup-path*)))
- #'string>)))
- (defun starts-with (str with)
- (string= (subseq str 0 (min (length str)
- (length with)))
- with))
- (defmacro aget (key alist)
- `(cdr (assoc ,key ,alist :test #'string=)))
- (defun make-keyword (name) (values (intern (string-upcase name) "KEYWORD")))
- (defun load-backup (filename)
- (gzip-stream:with-open-gzip-file (stream filename)
- (setq stream (flexi-streams:make-flexi-stream stream :external-format :utf-8))
- (loop
- for line = (read-line stream nil)
- with data = (make-hash-table :test 'equal) and table and entity and table-name
- while line
- do (cond
- ((starts-with line "$ENTITY:")
- (setf table-name (subseq line 8)
- table (gethash table-name data (make-hash-table :test 'equal))
- (gethash table-name data) table
- entity nil))
- ((string= line "$$")
- (if (getf entity :-id)
- (setf (gethash (parse-integer (getf entity :-id)) table) entity
- table-name nil)
- (progn
- (when (eql (type-of table) 'hash-table)
- (setf table nil))
- (setf (gethash table-name data) (push entity table)
- table-name nil))))
- (table-name (let ((i (position #\: line)))
- (setf (getf entity (make-keyword (substitute #\- #\_ (subseq line 0 i))))
- (subseq line (1+ i))))))
- finally (return data))))
- (defun make-finance-doc (e db balances)
- (let* ((accounts (gethash "account" db))
- (currencies (gethash "currency" db))
- (from-account (gethash (parse-integer (getf e :from-account-id)) accounts))
- (from-currency (gethash (parse-integer (getf from-account :currency-id)) currencies))
- (from-decimals (expt 10d0 (parse-integer (getf from-currency :decimals))))
- (from-amount (/ (parse-integer (getf e :from-amount)) from-decimals))
- (from-balance (/ (gethash (parse-integer (getf from-account :-id)) balances 0) from-decimals))
- (spend (< from-amount 0))
- (original-currency (gethash (parse-integer (getf e :original-currency-id)) currencies))
- (original-from-amount (and original-currency
- (/ (parse-integer (getf e :original-from-amount))
- (expt 10d0 (parse-integer (getf original-currency :decimals))))))
- (payee (gethash (parse-integer (getf e :payee-id)) (gethash "payee" db)))
- (category (gethash (parse-integer (getf e :category-id)) (gethash "category" db)))
-
- (to-account (gethash (parse-integer (getf e :to-account-id)) accounts))
- (to-currency (and to-account (gethash (parse-integer (getf to-account :currency-id)) currencies)))
- (to-decimals (and to-account (expt 10d0 (parse-integer (getf to-currency :decimals)))))
- (to-amount (and to-account (/ (parse-integer (getf e :to-amount)) to-decimals)))
- (to-balance (and to-account (/ (gethash (parse-integer (getf to-account :-id)) balances 0) to-decimals)))
- (transfer (cond
- ((and to-account (equal (getf from-account :type)
- "LIABILITY")
- (< from-balance 0)) :borrow)
- ((and to-account (equal (getf to-account :type)
- "LIABILITY")
- (<= to-balance 0)) :repay)
- ((and to-account (equal (getf to-account :type)
- "LIABILITY")
- (> to-balance 0)) :lend)
- ((and to-account (equal (getf from-account :type)
- "LIABILITY")
- (>= from-balance 0)) :repaid)
- (to-account :transfer)
- (t nil)))
- (title (if transfer
- (format
- nil "~A ~$~A from ~A (bal ~$~A) to ~A~:[~*~;~:* ~$~A~] (bal ~$~A)~@[ (~A)~]"
- (ecase transfer
- (:borrow "Borrowed")
- (:repay "Repaid")
- (:lend "Lend")
- (:repaid "Got repaid")
- (:transfer "Transferred"))
- (abs from-amount) (getf from-currency :symbol)
- (getf from-account :title)
- from-balance (getf from-currency :symbol)
- (getf to-account :title)
- (and (not (equal (getf from-account :currency-id)
- (getf to-account :currency-id)))
- (abs to-amount))
- (getf to-currency :symbol)
- to-balance (getf to-currency :symbol)
- (getf e :note))
- (format
- nil "~A ~$~A~:[~*~;~:* (~$~A)~] ~A ~A~@[ for ~A~]~@[ at ~A~]~@[ (~A)~]. Balance ~$~A"
- (if spend "Spend" "Earned")
- (abs from-amount) (getf from-currency :symbol)
- (and original-from-amount (abs original-from-amount))
- (getf original-currency :symbol)
- (if spend "from" "to")
- (getf from-account :title)
- (and category (not (equal (getf category :-id) "0")) (getf category :title))
- (and payee (getf payee :title))
- (getf e :note)
- from-balance (getf from-currency :symbol))))
- (type (if transfer (string-downcase (string transfer)) (if spend "spend" "earned")))
- (financisto (if transfer
- (kv "transfer" (kv
- (kv "from_account" (getf from-account :title))
- (kv "from_amount" from-amount)
- (kv "from_currency" (getf from-currency :name))
- (kv "from_balance" from-balance)
- (kv "to_account" (getf to-account :title))
- (kv "to_amount" to-amount)
- (kv "to_currency" (getf to-currency :name))
- (kv "to_balance" to-balance)))
- (kv "transaction" (kv
- (kv "account" (getf from-account :title))
- (kv "amount" from-amount)
- (kv "currency" (getf from-currency :name))
- (kv "balance" from-balance)
- (kv "original_amount" original-from-amount)
- (kv "original_currency" (getf original-currency :name)))))))
- (kv
- (kv "ts" (ms->ts (parse-integer (getf e :datetime))))
- (kv "type" "finance")
- (kv "title" title)
- (kv "financisto"
- (kv
- (kv "id" (parse-integer (getf e :-id)))
- (kv "type" type)
- (kv "payee" (getf payee :title))
- (kv "category" (and category (not (equal (getf category :-id) "0")) (getf category :title)))
- (kv "note" (getf e :note))
- financisto)))))
- (defun financisto-import ()
- (let* ((filename (find-last-backup))
- (db (load-backup filename))
- (transactions (sort (loop for entity being the hash-values of (gethash "transactions" db)
- when (not (string= (getf entity :category-id) "-1"))
- collect entity)
- #'<
- :key #'(lambda (i) (parse-integer (getf i :datetime)))))
- (balances (make-hash-table :size (hash-table-count (gethash "account" db)))))
- (format t "Got ~A with ~A transactions~%" filename (length transactions))
- (dolist (entity transactions)
- (destructuring-bind (&key from-account-id from-amount
- to-account-id to-amount
- -id datetime
- &allow-other-keys) entity
- (incf (gethash (parse-integer from-account-id) balances 0) (parse-integer from-amount))
- (when (not (string= to-account-id "0"))
- (incf (gethash (parse-integer to-account-id) balances 0) (parse-integer to-amount)))
- (let* ((new-doc (make-finance-doc entity db balances))
- (existing-doc (first (docs (db.find "events"
- (kv ($ "type" "finance")
- ($ "financisto.id" (parse-integer -id)))))))
- (new-ts (parse-integer datetime)))
- (cl-mongo::kv-container-add
- (kv "loc"
- (if (and existing-doc (= new-ts (cl-mongo::raw
- (get-element "ts" existing-doc))))
- (get-element "loc" existing-doc)
- (point->doc (find-location-at new-ts))))
- new-doc)
- (if existing-doc
- (progn
- (cl-mongo::kv-container-add (kv "_id" (doc-id existing-doc)) new-doc)
- (db.save "events" new-doc))
- (db.insert "events" new-doc)))))))
|