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.

Mine is written in TXR Lisp. It is not released to the public.

All input is recorded via function call expressions, which are of course S-exps. There is only one single macro in the whole thing: def-date-var 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.

> 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 also have transactions that sum to zero. There can be three or more account deltas in a transaction, not only two.

This is not how accounting is taught in North America, though; I came up with it myself.

Inferring one amount is a bad idea. My system throws an exception if the transaction amounts do not add to zero.

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:

(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.

From the looks of the journal format of hledger, it's too generic. I wouldn't want to use anything of the sort.

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.

I bind a new variable when recording an invoice:

(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%))))

There is no custom DSL here at all; just the new macro of the object system.

This is then recorded using a plain function call to the convenience function record-invoice:

(record-invoice %inv-0042% "Invoice 0042 issued to Foo, Inc.")

This function will digest the invoice and post the right amounts into various accounts. The GST tax has to be charged, and such,

When the invoice is paid:

(record-paid-invoice %inv-0042% "Invoice 0042 paid by Foo, Inc."
(date 2017 7 31))

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.

To generate a nice HTML invoice, at the REPL, I just call the .(html ...) method of the invoice object, which takes a stream argument.

1> (with-stream (s (open-file "invoice.html" "w"))
%inv-0042%.(html s))