we had to start over because i leaked passwords...
This commit is contained in:
commit
2c820b5895
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
documents/**
|
30
deps.edn
Normal file
30
deps.edn
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{:deps
|
||||||
|
{selmer/selmer {:mvn/version "1.12.50"},
|
||||||
|
org.clojure/clojure {:mvn/version "1.11.1"},
|
||||||
|
org.clojure/tools.logging {:mvn/version "1.1.0"},
|
||||||
|
insn/insn {:mvn/version "0.5.2"},
|
||||||
|
org.clojure/tools.cli {:mvn/version "1.0.206"},
|
||||||
|
org.babashka/babashka.impl.reify {:mvn/version "0.1.1"},
|
||||||
|
rewrite-clj/rewrite-clj {:mvn/version "1.0.699-alpha"},
|
||||||
|
babashka/babashka.core
|
||||||
|
{:git/url "https://github.com/babashka/babashka.core",
|
||||||
|
:git/sha "52a6037bd4b632bffffb04394fb4efd0cdab6b1e"},
|
||||||
|
hiccup/hiccup {:mvn/version "2.0.0-alpha2"},
|
||||||
|
org.clojure/data.xml {:mvn/version "0.2.0-alpha6"},
|
||||||
|
org.clojure/data.csv {:mvn/version "1.0.0"},
|
||||||
|
com.taoensso/timbre {:mvn/version "5.2.1"},
|
||||||
|
com.cognitect/transit-clj {:mvn/version "1.0.329"},
|
||||||
|
babashka/babashka.curl {:mvn/version "0.1.1"},
|
||||||
|
clj-commons/clj-yaml {:mvn/version "0.7.108"},
|
||||||
|
org.clojure/core.rrb-vector {:mvn/version "0.1.2"},
|
||||||
|
org.babashka/sci.impl.types {:mvn/version "0.0.2"},
|
||||||
|
cheshire/cheshire {:mvn/version "5.11.0"},
|
||||||
|
org.clojure/core.match {:mvn/version "1.0.0"},
|
||||||
|
org.clojure/test.check {:mvn/version "1.1.1"},
|
||||||
|
babashka/fs {:mvn/version "0.1.2"},
|
||||||
|
cljc.java-time/cljc.java-time {:mvn/version "0.1.12"},
|
||||||
|
org.clojure/data.priority-map {:mvn/version "1.1.0"},
|
||||||
|
nrepl/bencode {:mvn/version "1.1.0"},
|
||||||
|
org.clojure/core.async {:mvn/version "1.5.648"},
|
||||||
|
http-kit/http-kit {:mvn/version "2.6.0-RC1"}
|
||||||
|
etaoin/etaoin {:mvn/version "1.0.38"}}}
|
17
docs/how-to-rules.rst
Normal file
17
docs/how-to-rules.rst
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
=====
|
||||||
|
Rules
|
||||||
|
=====
|
||||||
|
|
||||||
|
A rule is a function that takes:
|
||||||
|
|
||||||
|
Amount, Transaction Details, Category, Merchant Name, and f
|
||||||
|
It is matched on Account Number + Transaction Type
|
||||||
|
f is a map that returns the account-to of the transaction, based on it's applies-to and condition.
|
||||||
|
This means we can describe a set of rules as map containing::
|
||||||
|
|
||||||
|
{ :applies-to (the Account No + Transaction Type combo)
|
||||||
|
:function (the function which will return account-to)
|
||||||
|
:condition (whether this is the rule for the transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
We then need a standard way to pass all the elements we care about to applies-to
|
26
flake.lock
Normal file
26
flake.lock
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1665753223,
|
||||||
|
"narHash": "sha256-RsRUlQG3WaeqdWvuCaPvjptMEoq4yck2Gsj0lTklbk0=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "6594eb2fc86e22005f48aaeb1011b5a72d99e58e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
38
flake.nix
Normal file
38
flake.nix
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
description = "Beancount Importer";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
packages.x86_64-linux.muneem = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "muneem";
|
||||||
|
src = ./.;
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
cp -r src/ $out/
|
||||||
|
cp bb.edn $out/
|
||||||
|
cp deps.edn $out/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultPackage.x86_64-linux = self.packages.x86_64-linux.muneem;
|
||||||
|
devShell.x86_64-linux = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
jdk11
|
||||||
|
babashka
|
||||||
|
geckodriver
|
||||||
|
chromedriver
|
||||||
|
chromium
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
52
readme.rst
Normal file
52
readme.rst
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
======
|
||||||
|
Muneem
|
||||||
|
======
|
||||||
|
|
||||||
|
TODO
|
||||||
|
----
|
||||||
|
- [ ] Download Savings acct CSV
|
||||||
|
- [ ] Import Savings acct CSV
|
||||||
|
- [ ] RECONCILE
|
||||||
|
- [ ] Download NAB trade transactions and value
|
||||||
|
- [ ] Download spaceship returns
|
||||||
|
- [ ] `pad` checking / savings accounts against some known value way in the past
|
||||||
|
- [ ] MAYBE gonna have to write a rule engine that handles this
|
||||||
|
- [ ] MAYBE Parse the transaction only once at the start, and then use the parsed map instead of passing around a vector of strings
|
||||||
|
- [ ] Mention uncategorized transactions in matrix chat so that i can categorize it
|
||||||
|
- [ ] Post weekly spending in matrix chat
|
||||||
|
|
||||||
|
|
||||||
|
DONE
|
||||||
|
----
|
||||||
|
- [x] Read the CSV(s)
|
||||||
|
- [x] Figure out a way to download both CSVs from each account
|
||||||
|
- [x] Figure out what columns match to what data we care about
|
||||||
|
- [x] Marshall the data into a way that bean count understands
|
||||||
|
- [x] Open accounts that previously didn't exist
|
||||||
|
- [x] Write the transactions to the file
|
||||||
|
|
||||||
|
beancount::
|
||||||
|
|
||||||
|
Beancount Format
|
||||||
|
2016-12-06 * "Biang!" "Dinner"
|
||||||
|
Liabilities:CreditCard -47.23 USD
|
||||||
|
Expenses:Restaurants
|
||||||
|
|
||||||
|
|
||||||
|
-----------
|
||||||
|
Assumptions
|
||||||
|
-----------
|
||||||
|
|
||||||
|
We'll also be making assumptions about the way accounts are structured. Since NAB provides categories we can put that into the account
|
||||||
|
|
||||||
|
=================== ==================================== =============== =======
|
||||||
|
Transaction Type To From Misc
|
||||||
|
=================== ==================================== =============== =======
|
||||||
|
EFTPOS DEBIT Expenses:<Category>:<Merchant Name> Assets:Checking
|
||||||
|
TRANSFER CREDIT Assets:Checking Assets:Savings
|
||||||
|
MISCELLANEOUS DEBIT Expenses:<Category>:<Merchant Name> Assets:Checking
|
||||||
|
TRANSFER DEBIT Expenses:Rent Assets:Checking IFF: (= amount 170)
|
||||||
|
|
||||||
|
Additionally:
|
||||||
|
- we'll may need to use account nunmber for distinguishing different CSVs
|
||||||
|
- The actual date is stored in Transaction details, which we can use as the comment
|
43
src/muneem/etaoin.clj
Normal file
43
src/muneem/etaoin.clj
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
(ns muneem.etaoin
|
||||||
|
(:require [etaoin.api :as e]
|
||||||
|
[clojure.java.shell :refer [sh]]
|
||||||
|
[etaoin.keys :as k]))
|
||||||
|
|
||||||
|
(def driver (e/chrome {:args ["--disable-blink-features=AutomationControlled" "--maximize"]}))
|
||||||
|
;:download-dir "./documents/"
|
||||||
|
|
||||||
|
(defn download-csv
|
||||||
|
[]
|
||||||
|
(e/js-execute driver "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
|
||||||
|
(e/go driver "https://ib.nab.com.au/nabib/index.jsp")
|
||||||
|
(e/wait-visible driver [{:id :username}])
|
||||||
|
; Don't advertise that we're a bot
|
||||||
|
(e/fill driver {:id :username} (System/getenv "NAB_USER"))
|
||||||
|
(e/fill driver {:id :password} (System/getenv "NAB_PASSWORD"))
|
||||||
|
(e/fill driver {:id :password} k/enter)
|
||||||
|
(e/wait 4)
|
||||||
|
(e/click driver {:tag :button :id :hamburger-btn})
|
||||||
|
(e/click-el driver (e/query driver {:css "li.selected>button"}))
|
||||||
|
(e/click-el driver (e/query driver {:tag :a :fn/text "Transaction history"}))
|
||||||
|
(e/wait 4)
|
||||||
|
; Update to last 7 days
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"#dropdown-toggle-button-DateDropdown\").click()")
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"#dropdown-listbox-DateDropdown-0\").click()")
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"div > div > div > div.Containerstyle__StyledWrapper-sc-10ei1r0-0.dnudTa > div > div > div > div.StyledScreenView-jlloJx.gsnnCS > form > div > div:nth-child(3) > div > button\").click()")
|
||||||
|
(e/wait 2)
|
||||||
|
; Download checking file
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"#export-button > div\").click()")
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"div > div > div > div:nth-child(2) > div > div > div > div > div.StyledScreenView-jlloJx.gsnnCS > div > div > div > div.StyledTopRow-cijGbF.StyledExportCol-fYxmhQ.ibndGm.cAjmIO.sc-dkPtRN.Colstyle__StyledCol-sc-1evc4kf-0.ketKXK.fqofCs > div > div > div > ul > li:nth-child(1) > button\").click()")
|
||||||
|
(e/wait 3)
|
||||||
|
; Download savings file
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"#dropdown-toggle-button-accountsListDropdown > div > div > div\")")
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"#dropdown-listbox-accountsListDropdown-1 > div > div > div.AccountCardstyle__StyledAccountTypeContainer-sc-16p3i3w-1.cLXpwB\")")
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"#export-button > div\").click()")
|
||||||
|
(e/js-execute driver "document.querySelector(\"#bodyContent > miniapp-transactions\").shadowRoot.querySelector(\"div > div > div > div:nth-child(2) > div > div > div > div > div.StyledScreenView-jlloJx.gsnnCS > div > div > div > div.StyledTopRow-cijGbF.StyledExportCol-fYxmhQ.ibndGm.cAjmIO.sc-dkPtRN.Colstyle__StyledCol-sc-1evc4kf-0.ketKXK.fqofCs > div > div > div > ul > li:nth-child(1) > button\").click()")
|
||||||
|
(e/wait 3)
|
||||||
|
(e/quit driver))
|
||||||
|
|
||||||
|
(defn move-download
|
||||||
|
[]
|
||||||
|
(sh "mv" "~/Downloads/Transactions.csv" "./documents/checking.csv")
|
||||||
|
(sh "mv" "~/Downloads/Transactions\(1\).csv" "./documents/savings.csv"))
|
346
src/muneem/import.clj
Normal file
346
src/muneem/import.clj
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
(ns muneem.import
|
||||||
|
(:require [clojure.data.csv :as csv]
|
||||||
|
[clojure.java.io]
|
||||||
|
[cljc.java-time.local-date :as jt]
|
||||||
|
[cljc.java-time.format.date-time-formatter :as pt]
|
||||||
|
[clojure.string]))
|
||||||
|
|
||||||
|
(def beancount-file "./documents/test.beancount")
|
||||||
|
(def transaction-details (partial #(nth % 5)))
|
||||||
|
(def category (partial #(nth % 7)))
|
||||||
|
(defn merchant-name [trans]
|
||||||
|
(if (= (count trans) 9)
|
||||||
|
(nth trans 8)
|
||||||
|
""))
|
||||||
|
(def amount (partial #(nth % 1)))
|
||||||
|
(def trans-date (partial #(nth % 0)))
|
||||||
|
(def transaction-type (partial #(nth % 4)))
|
||||||
|
|
||||||
|
(defn take-csv
|
||||||
|
"Takes file name and reads data."
|
||||||
|
[file]
|
||||||
|
(-> (csv/read-csv (slurp file))
|
||||||
|
; We don't care about the header
|
||||||
|
(rest)))
|
||||||
|
|
||||||
|
(defn make-date
|
||||||
|
"Reads date from the 5th column (Transaction Details)
|
||||||
|
The 2nd element split by space is DD/MM of the actal transaction
|
||||||
|
Add 2022 to that and use it as the transaction date"
|
||||||
|
[trans statement]
|
||||||
|
; TODO this doesn't work for various transactions and other years
|
||||||
|
; Parse real date -> check that it is in fact a date
|
||||||
|
; Use normal date to determine year
|
||||||
|
; If it's not a date, use regular date
|
||||||
|
(let [real-date (-> trans
|
||||||
|
transaction-details
|
||||||
|
(clojure.string/split #" ")
|
||||||
|
second)
|
||||||
|
trans-date-year (-> (trans-date trans)
|
||||||
|
(clojure.string/split #" ")
|
||||||
|
(last)
|
||||||
|
(->> (str "20")))
|
||||||
|
with-year (-> real-date
|
||||||
|
(str "/" trans-date-year)
|
||||||
|
(clojure.string/split #"/")
|
||||||
|
reverse)
|
||||||
|
parse-year (-> (trans-date trans)
|
||||||
|
(jt/parse (pt/of-pattern "dd MMM uu"))
|
||||||
|
(jt/format (pt/of-pattern "YYYY-MM-dd")))]
|
||||||
|
(if (and (not (nil? real-date)) (re-matches #"../.." real-date))
|
||||||
|
(conj statement (clojure.string/join "-" with-year))
|
||||||
|
(conj statement parse-year))))
|
||||||
|
|
||||||
|
(defn add-transaction-flag
|
||||||
|
"Bean count requires transaction type, we use * which means txn"
|
||||||
|
[statement]
|
||||||
|
(conj statement "*"))
|
||||||
|
|
||||||
|
(defn add-narration
|
||||||
|
"Add some comments about the transaction from the 5th column (Transaction Details)"
|
||||||
|
[trans statement]
|
||||||
|
(let [narration (-> trans
|
||||||
|
transaction-details
|
||||||
|
(clojure.string/split #" ")
|
||||||
|
; first 2 are payment method and date
|
||||||
|
rest
|
||||||
|
rest
|
||||||
|
(conj "\"")
|
||||||
|
(cons "\""))]
|
||||||
|
(conj statement narration)))
|
||||||
|
|
||||||
|
(defn sanitize
|
||||||
|
[string]
|
||||||
|
(-> string
|
||||||
|
(clojure.string/capitalize)
|
||||||
|
(clojure.string/replace #" " "")
|
||||||
|
(clojure.string/replace #"'" "")
|
||||||
|
(clojure.string/replace #"\." "")
|
||||||
|
(clojure.string/replace #"\(.+\)" "")
|
||||||
|
(clojure.string/replace #"&" "-")))
|
||||||
|
|
||||||
|
; These should all be the same method tbh
|
||||||
|
; Or they should be a map?
|
||||||
|
; check out how-to-rules for more
|
||||||
|
|
||||||
|
(defn add-account-from-and-amount
|
||||||
|
"We're only handling checking account for now
|
||||||
|
All amounts are from it's perspsective"
|
||||||
|
[trans statement]
|
||||||
|
(let [amount (amount trans)
|
||||||
|
account "Assets:Checking"
|
||||||
|
string (str " " account " " amount " AUD")]
|
||||||
|
(conj statement string)))
|
||||||
|
|
||||||
|
(defn add-account-from-and-amount-savings
|
||||||
|
[trans statement]
|
||||||
|
(let [amount (amount trans)
|
||||||
|
account "Assets:Savings"
|
||||||
|
string (str " " account " " amount " AUD")]
|
||||||
|
(conj statement string)))
|
||||||
|
|
||||||
|
(defn debit-account-to
|
||||||
|
[trans statement]
|
||||||
|
(let [category (sanitize (category trans))
|
||||||
|
merchant-name (sanitize (merchant-name trans))
|
||||||
|
; Uncategorized strings need to be reconciled
|
||||||
|
; TODO we'd probably want rules based on the transaction details
|
||||||
|
; to automatically categorize further ones
|
||||||
|
account-to (if (= category "Uncategorised")
|
||||||
|
(if (> (count merchant-name) 0)
|
||||||
|
(str " ! Expenses:" category ":" merchant-name)
|
||||||
|
(str " ! Expenses:" category))
|
||||||
|
(if (> (count merchant-name) 0)
|
||||||
|
(str " Expenses:" category ":" merchant-name)
|
||||||
|
(str " Expenses:" category)))]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn fees-account-to
|
||||||
|
[trans statement]
|
||||||
|
(let [category (sanitize (category trans))
|
||||||
|
merchant-name (sanitize (merchant-name trans))
|
||||||
|
account-to (str " Expenses:Fees")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn atm-debit-account-to
|
||||||
|
[trans statement]
|
||||||
|
(let [category (sanitize (category trans))
|
||||||
|
merchant-name (sanitize (merchant-name trans))
|
||||||
|
account-to (str " Assets:Cash")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn transfer-credit-account-to
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " Assets:Savings")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn bank-credit-account-to
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " ! Income:ThreatDefence")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn dividend-savings
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " Income:Dividends")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn salary-savings
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " Income:Cubiko")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn interest-account-to
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " Expenses:NAB")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn interest-savings
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " Income:NAB")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn automatic-drawing-account-to
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " Assets:Spaceship")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn misc-credit-account-to
|
||||||
|
[_ statement]
|
||||||
|
(let [account-to (str " Income:Beem")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn deposit-account-to
|
||||||
|
"This function is going to change a lot
|
||||||
|
It currently only works with Expenses
|
||||||
|
but in the future will need to handle determining account to"
|
||||||
|
[trans statement]
|
||||||
|
(let [account-to (str " ! Expenses:OverCharged")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
(defn transfer-debit-account-to
|
||||||
|
"This function is going to change a lot
|
||||||
|
It currently only works with Expenses
|
||||||
|
but in the future will need to handle determining account to"
|
||||||
|
[trans statement]
|
||||||
|
(let [rent (str " Expenses:Rent")
|
||||||
|
trans-type (transaction-type trans)
|
||||||
|
amt (amount trans)
|
||||||
|
else-account (str " Expenses:" (-> trans-type
|
||||||
|
(clojure.string/split #" ")
|
||||||
|
(nth 1)))]
|
||||||
|
(if (= amt 170)
|
||||||
|
(conj statement rent)
|
||||||
|
(conj statement else-account))))
|
||||||
|
|
||||||
|
(defn transfer-debit-savings
|
||||||
|
"This function is going to change a lot
|
||||||
|
It currently only works with Expenses
|
||||||
|
but in the future will need to handle determining account to"
|
||||||
|
[trans statement]
|
||||||
|
(let [account-to (str " Assets:Checking")]
|
||||||
|
(conj statement account-to)))
|
||||||
|
|
||||||
|
;; RULES
|
||||||
|
; {:applies-to (fn [{trans-type :transaction-type
|
||||||
|
; amount :amount
|
||||||
|
; category :category
|
||||||
|
; merchant-name :merchant-name)))
|
||||||
|
|
||||||
|
(defn determine-category
|
||||||
|
"We probably want some sort of rule engine
|
||||||
|
that takes rules and applies the first match or something (see docs)
|
||||||
|
but this will do for now
|
||||||
|
|
||||||
|
Writes out the account-to to the statement"
|
||||||
|
[trans statement]
|
||||||
|
(let [trans-type (transaction-type trans)]
|
||||||
|
(case trans-type
|
||||||
|
"EFTPOS DEBIT" (debit-account-to trans statement)
|
||||||
|
"TRANSFER CREDIT" (transfer-credit-account-to trans statement)
|
||||||
|
"MISCELLANEOUS DEBIT" (debit-account-to trans statement)
|
||||||
|
"TRANSFER DEBIT" (transfer-debit-account-to trans statement)
|
||||||
|
"INTEREST PAID" (interest-account-to trans statement)
|
||||||
|
"AUTOMATIC DRAWING" (automatic-drawing-account-to trans statement)
|
||||||
|
"FEES" (fees-account-to trans statement)
|
||||||
|
"REVERSAL CREDIT" (automatic-drawing-account-to trans statement)
|
||||||
|
"ATM DEBIT" (atm-debit-account-to trans statement)
|
||||||
|
"DEPOSIT" (deposit-account-to trans statement)
|
||||||
|
"INTER-BANK CREDIT" (bank-credit-account-to trans statement)
|
||||||
|
"MISCELLANEOUS CREDIT" (misc-credit-account-to trans statement))))
|
||||||
|
|
||||||
|
(defn determine-category
|
||||||
|
"We probably want some sort of rule engine
|
||||||
|
that takes rules and applies the first match or something (see docs)
|
||||||
|
but this will do for now
|
||||||
|
|
||||||
|
Writes out the account-to to the statement"
|
||||||
|
[trans statement]
|
||||||
|
(let [trans-type (transaction-type trans)]
|
||||||
|
(case trans-type
|
||||||
|
"TRANSFER DEBIT" (transfer-debit-savings trans statement)
|
||||||
|
"INTEREST PAID" (interest-savings trans statement)
|
||||||
|
"INTER-BANK CREDIT" (bank-credit-account-to trans statement)
|
||||||
|
"DIVIDEND" (dividend-savings trans statement)
|
||||||
|
"SALARY" (salary-savings trans statement))))
|
||||||
|
|
||||||
|
(defn write-statement
|
||||||
|
[statement]
|
||||||
|
(spit beancount-file (clojure.string/join "\n" (conj statement "\n")) :append true))
|
||||||
|
|
||||||
|
(defn tap
|
||||||
|
[s]
|
||||||
|
(println s)
|
||||||
|
s)
|
||||||
|
|
||||||
|
(defn organize-correctly
|
||||||
|
[statement]
|
||||||
|
(-> statement
|
||||||
|
(flatten)
|
||||||
|
(into [])
|
||||||
|
(->> (clojure.string/join " "))
|
||||||
|
(->> (conj []))))
|
||||||
|
|
||||||
|
(def accounts (atom #{}))
|
||||||
|
(def new-accounts (atom #{}))
|
||||||
|
|
||||||
|
(defn create-account
|
||||||
|
[transaction]
|
||||||
|
(let [to-account (second (clojure.string/split (second transaction) #" "))
|
||||||
|
from-account (second (clojure.string/split (nth transaction 2) #" "))
|
||||||
|
marshalled-from-account (clojure.string/replace from-account #"! " "")]
|
||||||
|
(if (not (contains? (deref accounts) to-account))
|
||||||
|
(swap! new-accounts conj to-account))
|
||||||
|
(if (not (contains? (deref accounts) marshalled-from-account))
|
||||||
|
(swap! new-accounts conj marshalled-from-account))
|
||||||
|
transaction))
|
||||||
|
|
||||||
|
(defn write-transaction-to-file
|
||||||
|
[transaction]
|
||||||
|
(->> []
|
||||||
|
(make-date transaction)
|
||||||
|
(add-transaction-flag)
|
||||||
|
(add-narration transaction)
|
||||||
|
(organize-correctly)
|
||||||
|
(add-account-from-and-amount transaction)
|
||||||
|
(determine-category transaction)
|
||||||
|
#_(tap)
|
||||||
|
(create-account)
|
||||||
|
(write-statement)))
|
||||||
|
|
||||||
|
(defn write-transaction-to-file-savings
|
||||||
|
[transaction]
|
||||||
|
(->> []
|
||||||
|
(make-date transaction)
|
||||||
|
(add-transaction-flag)
|
||||||
|
(add-narration transaction)
|
||||||
|
(organize-correctly)
|
||||||
|
(add-account-from-and-amount-savings transaction)
|
||||||
|
(determine-category transaction)
|
||||||
|
#_(tap)
|
||||||
|
(create-account)
|
||||||
|
(write-statement)))
|
||||||
|
|
||||||
|
(defn index-of [x coll]
|
||||||
|
(let [idx? (fn [i a] (when (= x a) i))]
|
||||||
|
(first (keep-indexed idx? coll))))
|
||||||
|
|
||||||
|
(defn get-account
|
||||||
|
"Returns the account from statements where the account is opened"
|
||||||
|
[account-info]
|
||||||
|
(let [split (into [] (clojure.string/split account-info #" "))
|
||||||
|
account (last split)]
|
||||||
|
account))
|
||||||
|
|
||||||
|
(defn read-accounts
|
||||||
|
[ledger]
|
||||||
|
(let [file-contents (slurp ledger)
|
||||||
|
split (into [] (rest (clojure.string/split file-contents #"\n")))
|
||||||
|
empty-index (index-of "" split)
|
||||||
|
only-accounts (if (not (nil? empty-index))
|
||||||
|
(subvec split 0 empty-index)
|
||||||
|
split)]
|
||||||
|
(->> (mapv get-account only-accounts)
|
||||||
|
(set)
|
||||||
|
(reset! accounts))))
|
||||||
|
|
||||||
|
(defn open-account-string
|
||||||
|
[account]
|
||||||
|
(str "2016-01-01 open " account))
|
||||||
|
|
||||||
|
(defn write-accounts
|
||||||
|
[ledger]
|
||||||
|
(let [file-contents (slurp ledger)
|
||||||
|
split (clojure.string/split file-contents #"\n")
|
||||||
|
empty-index (index-of "" split)
|
||||||
|
[before after] (split-at empty-index split)
|
||||||
|
middle (mapv open-account-string (into [] (deref new-accounts)))
|
||||||
|
full-file (vec (concat before middle after "\n"))]
|
||||||
|
(spit ledger (clojure.string/join "\n" full-file))))
|
||||||
|
|
||||||
|
(defn main
|
||||||
|
[csv checking? & _args]
|
||||||
|
(read-accounts beancount-file)
|
||||||
|
(if checking?
|
||||||
|
(mapv write-transaction-to-file (drop-last (take-csv csv)))
|
||||||
|
(mapv write-transaction-to-file-savings (drop-last (take-csv csv))))
|
||||||
|
(write-accounts beancount-file))
|
12
src/muneem/main.clj
Normal file
12
src/muneem/main.clj
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
(ns muneem.main
|
||||||
|
(:require [muneem.import :as import]
|
||||||
|
[muneem.etaoin :as download]))
|
||||||
|
|
||||||
|
(defn -main
|
||||||
|
[& _args]
|
||||||
|
(download/download-csv)
|
||||||
|
(download/move-download)
|
||||||
|
(import/main "./documents/checking.csv" true)
|
||||||
|
(import/main "./documents/savings.csv" true))
|
||||||
|
|
||||||
|
(-main)
|
Loading…
Reference in a new issue