This is exactly how I do my accounting: record transactions in a plain text file, which is processed by the system, resulting in various ledger and account type objects being in a certain state, which can then be subject to queries and reports.<p>Mine is written in TXR Lisp. It is not released to the public.<p>All input is recorded via function call expressions, which are of course S-exps. There is only one single macro in the whole thing: <i>def-date-var</i> defines a variable that exhibits different values depending on a effective date context (established by a dynamically scoped date variable). With this we can do things like different tax rates for different periods, without having to introduce such things as parameters appearing in every input item.<p>> <i>The amounts within a transaction must always sum up to zero. As a convenience, one amount may be left blank; it will be inferred so as to balance the transaction.</i><p>I also have transactions that sum to zero. There can be three or more account deltas in a transaction, not only two.<p>This is not how accounting is taught in North America, though; I came up with it myself.<p>Inferring one amount is a bad idea. My system throws an exception if the transaction amounts do not add to zero.<p>However, the entry of items is done via various convenience functions, most of which generate the necessary transactions against all the right accounts. For instance, recording a new asset:<p><pre><code> (record-asset 1 "Discombobulator" :cca-50 ;; ID, description, capital-cost-allowance class
(date 2017 1 23) ($ 79.99) ($ 4.00) ($ 5.60)) ;; base price, GST, PST taxes.
</code></pre>
From the looks of the journal format of hledger, it's too generic. I wouldn't want to use anything of the sort.<p>Different items need their own syntax. I don't want to record a simple purchase of a recurring expense in the same way that I record an invoice.<p>I bind a new variable when recording an invoice:<p><pre><code> (defparml %inv-0042%
(new invoice
number 42
provider %my-addr%
client %foo-client-addr%
remit-to "Kazinator"
remit-days 25
date (date 2017 6 30)
items (list (new time-unit
start (date 2017 6 1)
end (date 2017 6 2)
hours 16
rate %rate-foo-client-2017%)
(new time-unit
start (date 2017 6 5)
end (date 2017 6 9)
hours 40
rate %rate-foo-client-2017%)
(new time-unit
start (date 2017 6 12)
end (date 2017 6 16)
hours 40
rate %rate-foo-client-2017%)
(new time-unit
start (date 2017 6 19)
end (date 2017 6 23)
hours 40
rate %rate-foo-client-2017%)
(new time-unit
start (date 2017 6 26)
end (date 2017 6 30)
hours 40
rate %rate-foo-client-2017%))))
</code></pre>
There is no custom DSL here at all; just the <i>new</i> macro of the object system.<p>This is then recorded using a plain function call to the convenience function <i>record-invoice</i>:<p><pre><code> (record-invoice %inv-0042% "Invoice 0042 issued to Foo, Inc.")
</code></pre>
This function will digest the invoice and post the right amounts into various accounts. The GST tax has to be charged, and such,<p>When the invoice is paid:<p><pre><code> (record-paid-invoice %inv-0042% "Invoice 0042 paid by Foo, Inc."
(date 2017 7 31))
</code></pre>
this function will also update multiple accounts, including the automatic withholding of income tax, based on the withholding rate configured for the date into which the invoice lands.<p>To generate a nice HTML invoice, at the REPL, I just call the .(html ...) method of the invoice object, which takes a stream argument.<p><pre><code> 1> (with-stream (s (open-file "invoice.html" "w"))
%inv-0042%.(html s))</code></pre>