Fork me on GitHub
#clojure-sweden
<
2024-05-03
>
jherrlin19:05:33

God afton, Skulle vilja leka med min bokföringsdata i Clojure. Är det någon som har kod för att läsa / skriva SIE4 filer?

vlaaad08:05:45

Heyy, I did the thing for my accounting

vlaaad08:05:33

Though I can't share it as is since it's a hodge podge of various imports of transactions from stripe to produce my own sie4 file..

vlaaad08:05:03

But it was fun to do, and there is a format documentation, so it's quite approachable

vlaaad08:05:43

Basically there is some metadata, a list of transactions, and some data like year results which can be entirely derived from the list of transactions

jherrlin08:05:42

Nice! Yeah I think it looks kind of fun to go for it myself. I wonder if a grammar and instaparse is a good approach for reading.

vlaaad11:05:14

Yeah, that's what I did, an instaparse grammar

jherrlin11:05:00

Are you willing to share the grammar?

vlaaad11:05:30

The format is pretty regular, every line is a tag and a sequence of possibly escaped strings

vlaaad11:05:43

Yeah, I can share the grammar I wrote, hold on...

❤️ 1
vlaaad11:05:19

(def parser (insta/parser "
SIE = STMT (<NL> STMT)* <NL> <STMT> = flag | format | sie-type | program | generated-at | organization-name | program-organization-id | organization-id | address | financial-year | tax-year | currency | chart-of-accounts-type | account | account-type | account-sru | dimension | closing-balance | opening-balance | verification flag = <'#FLAGGA'> <SPACE> INT format = <'#FORMAT'> <SPACE> 'PC8' sie-type = <'#SIETYP'> <SPACE> INT program = <'#PROGRAM'> <SPACE> STR <SPACE> STR organization-name = <'#FNAMN'> <SPACE> STR organization-id = <'#ORGNR'> <SPACE> STR address = <'#ADRESS'> <SPACE> STR <SPACE> STR <SPACE> STR <SPACE> STR financial-year = <'#RAR'> <SPACE> INT <SPACE> DATE <SPACE> DATE program-organization-id = <'#FNR'> <SPACE> STR generated-at = <'#GEN'> <SPACE> DATE tax-year = <'#TAXAR'> <SPACE> INT currency = <'#VALUTA'> <SPACE> STR chart-of-accounts-type = <'#KPTYP'> <SPACE> STR account = <'#KONTO'> <SPACE> INT <SPACE> STR account-type = <'#KTYP'> <SPACE> INT <SPACE> ('T'|'S'|'K'|'I') account-sru = <'#SRU'> <SPACE> INT <SPACE> INT dimension = <'#DIM'> <SPACE> INT <SPACE> STR closing-balance = (<'#UB'>|<'#RES'>) <SPACE> INT <SPACE> INT <SPACE> QTY opening-balance = <'#IB'> <SPACE> INT <SPACE> INT <SPACE> QTY verification = <'#VER'> <SPACE> STR <SPACE> INT_STR <SPACE> DATE <SPACE> STR <SPACE> DATE <NL> <'{'> (<NL> <SPACE>? TRANS)* <NL> <'}'> TRANS = <'#TRANS'> <SPACE> INT <SPACE> <STR> <SPACE> QTY QTY = #'-?\\d+(\\.\\d+)?' SPACE = #'\\s+' INT = #'-?\\d+' <STR> = UNESCAPED_STRING | ESCAPED_STRING DATE = STR INT_STR = STR <UNESCAPED_STRING> = #'[^\\s^\"]+' ESCAPED_STRING = #'\"[^\"]*\"' NL = #'\\s*\\r?\\n'")) (defn- convert [ret] (reduce (fn [acc [k & args :as v]] (case k (:flag :format :chart-of-accounts-type :sie-type :generated-at :program-organization-id :tax-year :currency) (apply assoc acc v) :program (assoc acc k (zipmap [:name :version] args)) :address (assoc acc k (zipmap [:contact :distribution :postal :phone] args)) :organization-id (assoc-in acc [:organization :id] (first args)) :organization-name (assoc-in acc [:organization :name] (first args)) :financial-year (let [[id started-at ended-at] args] (assoc-in acc [:financial-year id] {:started-at started-at :ended-at ended-at})) :account (let [[id label] args] (assoc-in acc [:account id :label] label)) :account-type (let [[id type] args] (assoc-in acc [:account id :type] type)) :account-sru (let [[id sru] args] (assoc-in acc [:account id :sru] sru)) :opening-balance (let [[year-id account-id value] args] (assoc-in acc [:balance year-id account-id :opening] value)) :closing-balance (let [[year-id account-id value] args] (assoc-in acc [:balance year-id account-id :closing] value)) :verification (let [[series num ver-date label reg-date & txs] args] (update acc :verifications (fnil conj []) {:series series :number num :created-at ver-date :label label :registered-at reg-date :transactions (mapv (fn [[_ account quantity]] [account quantity]) txs)})) :dimension (apply update acc k assoc args) (do (tap> {:acc acc :val v}) (throw (Exception. (str k)))))) {} (rest (insta/transform {:INT #(Integer/parseInt ^String %) :INT_STR #(Integer/parseInt ^String %) :QTY #(BigDecimal. ^String %) :DATE parse-date :ESCAPED_STRING #(str/replace (subs % 1 (dec (count %))) #"\\(\"|\\)" "$1")} ret)))) (defn parse-sie [sie-str] (let [ret (insta/parse parser sie-str)] (if (insta/failure? ret) (throw (ex-info (pr-str ret) {:failure ret})) (convert ret))))

vlaaad11:05:53

Sorry for wrecked formatting, I'm on mobile

jherrlin11:05:08

That’s very kind of you! Thank you 🙏

😄 1
vlaaad11:05:45

Don't forget :encoding "IBM437" when slurping your sie file

❤️ 1
awesome 1
jherrlin14:05:36

Got it working with the file I exported from Fortnox 😄

👍 1
🎉 1