Back when I was younger, there was a computer game called "Company Director" or something like that, and I was fascinated by reports such as Profit and Loss and Balance Sheet. More so, in fact, than actually running my company and selling refrigerators (I have to admit most of the details escape me now).
As I have grown up, these things have become more natural, but I still have a sense of wonder about all sorts of results in number theory, and the balancing of a balance sheet still seems somehow magical for the same reason that 12x12 = 9x16 seems magical.
So, as much as I will tell you that I am doing this so that I can see what I think the results "should be" before I start trying to use someone else's tool, as much as anything, I want to do this for my own amusement.
Setting the Stage
The data as we have stored it in the
Gnucash structure is perfect for rendering XML, but not ideal for any other purpose. To repurpose it, we need to choose between interpreting it as it stands and saving it in an alternative format.
In doing this, it seems to me easy enough to find the transactions in the
AccountBook, and we already have a pointer to that in
Gnucash, so I will do that, but the actual transaction "splits" are so messed up that there is almost no hope, so I will add specific entries to the transaction to hold those:
type Transaction struct {
XMLName xml.Name `xml:"gnc:transaction"`
Version string `xml:"version,attr"`
accountGuids map[string]string
srcAcct *TransactionItem
destAcct *TransactionItem
credit *AccountCredit
debit *AccountDebit
Elements
}
GNUCASH_REPORT_1:accounts/internal/gnucash/writer/gnucash.go
type AccountAction struct {
When DateInfo
Acct string
Amount Money
}
type AccountCredit struct {
AccountAction
}
type AccountDebit struct {
AccountAction
}
GNUCASH_REPORT_1:accounts/internal/gnucash/writer/gnucash.go
(Each transaction consists of ONE credit and ONE debit, which are a "matching pair".)
And, during the processing of the transaction data, I will just store the relevant information here:
func (g *Gnucash) Transact(date DateInfo, description string, src string, dest string, amount Money) *Transaction {
tx := &Transaction{Version: "2.0.0", accountGuids: g.accountGuids}
guid := newGuid()
...
g.book.Elements = append(g.book.Elements, tx)
// for regurgitation
tx.credit = &AccountCredit{AccountAction: AccountAction{When: date, Acct: dest, Amount: amount}}
tx.debit = &AccountDebit{AccountAction: AccountAction{When: date, Acct: src, Amount: amount}}
return tx
}
GNUCASH_REPORT_1:accounts/internal/gnucash/writer/gnucash.go
And then it is possible to ask for all these credits and debits back:
func (g *Gnucash) Regurgitate(rcvr TxReceiver) {
for _, x := range g.book.Elements {
tx, ok := x.(*Transaction)
if ok {
rcvr.Credit(*tx.credit)
rcvr.Debit(*tx.debit)
}
}
}
GNUCASH_REPORT_1:accounts/internal/gnucash/writer/gnucash.go
The loop iterates over all the book elements (counts, accounts, transactions, etc), and finds the ones that are transactions. For each transaction, it then alerts the receiver about the credit and debit portions of the transaction.
A receiver can be any struct implementing
TxReceiver:
type TxReceiver interface {
Credit(c AccountCredit)
Debit(c AccountDebit)
}
GNUCASH_REPORT_1:accounts/internal/gnucash/writer/deliver.go
Capturing the Data
Having made sure we have a usable source of account data, we can now create a data collector for our reports. Basically, for asset and liability accounts, we want to look at all transactions from the beginning of time until the end of the reporting period; for income and expense accounts, we want to look at transactions within the reporting period.
At the top level, we create the data collector, telling it what the reporting period is, and ask the writer to regurgitate all the transactions and deliver them to the data collector. We can do this for each of the years we are interested in.
func AccountsPipeline(conf *config.Configuration) {
w := writer.MakeWriter(conf)
accts := accounts.MakeAccounts(conf, w)
sheets.ReadSpreadsheet(conf, accts)
for yr := 2019; yr <= 2019; yr++ {
r := reporter.NewReporter(yr)
accts.Regurgitate(r)
}
}
GNUCASH_REPORT_1:accounts/internal/gnucash/pipeline/accounts.go
I want to be clear that all of this is for only my own benefit. I mean, while it's quite likely that a company has the calendar year as its fiscal year, how likely is it that you want to report from 2019 onwards? Let's be clear: this is a blog about doing interesting and different things and experimenting; I am not talking about production code. If I were, we would be knee-deep in unit tests right now.
And we can implement a first cut of the reporter as follows:
package reporter
import (
"fmt"
"github.com/gmmapowell/ignorance/accounts/internal/gnucash/writer"
)
type reporter struct {
Year int
}
func (r *reporter) Credit(c writer.AccountCredit) {
fmt.Printf("%s: Credit %s with %s\n", c.When.JustDate(), c.Acct, c.Amount.String())
}
func (r *reporter) Debit(c writer.AccountDebit) {
fmt.Printf("%s: Debit %s by %s\n", c.When.JustDate(), c.Acct, c.Amount.String())
}
func NewReporter(yr int) writer.TxReceiver {
return &reporter{Year: yr}
}
GNUCASH_REPORT_1:accounts/internal/gnucash/reporter/reporter.go
So far, so good. I can run this and all the transactions come out with a credit and a debit for each one.
Now we need to start summarizing things. The problem here is that all the different accounts behave differently. For Income and Expense accounts, we only want to collect data for a single year; for the others we want to collect data from the beginning of time. And for Bank and Expense accounts we want to treat credits as positive; for Income, Equity and Liability accounts we want to count debits as positive. This is really hard to explain and understand, but when you apply the rules, everything comes out looking right (again, just like number theory).
So we need to know about what accounts exist and what their types are. In other words, we need the configuration. We can pass the configuration to
NewReporter and get that to configure the
reporter:
func NewReporter(conf *config.Configuration, yr int) writer.TxReceiver {
ret := reporter{Year: yr, Accounts: make(map[string]account)}
ret.Configure(conf.Accounts)
return &ret
}
GNUCASH_REPORT_2:accounts/internal/gnucash/reporter/reporter.go
And then provide the
Configure function along with the data slots to store the configuration:
type reporter struct {
Accounts map[string]account
Year int
}
func (r *reporter) Configure(accts []config.Account) {
for _, acc := range accts {
log.Printf("Configuring %s of %s\n", acc.Name, acc.Type)
r.Accounts[acc.Name] = makeAccount(r.Year, acc.Type)
r.Configure(acc.Accounts)
}
}
GNUCASH_REPORT_2:accounts/internal/gnucash/reporter/reporter.go
account is an interface, backed by the poorly named
actor struct (I will rename this if I can think of a good name, but this is just basically
AccountImpl), which is in a new file.
package reporter
import (
"fmt"
"github.com/gmmapowell/ignorance/accounts/internal/gnucash/writer"
)
type account interface {
Credit(date writer.DateInfo, amount writer.Money)
Debit(date writer.DateInfo, amount writer.Money)
}
type actor struct {
debitEffect, creditEffect int
justYear bool
year int
balance writer.Money
}
func (a *actor) Credit(date writer.DateInfo, amount writer.Money) {
if a.justYear && date.Year != a.year {
return
}
a.balance.Incorporate(a.creditEffect, amount)
fmt.Printf("Collect credit %s => %s\n", amount, a.balance)
}
func (a *actor) Debit(date writer.DateInfo, amount writer.Money) {
if a.justYear && date.Year != a.year {
return
}
a.balance.Incorporate(a.debitEffect, amount)
fmt.Printf("Collect debit %s => %s\n", amount, a.balance)
}
func makeAccount(yr int, ty string) account {
switch ty {
case "ASSET":
fallthrough
case "BANK":
return &actor{debitEffect: -1, creditEffect: 1, justYear: false, year: yr}
case "EXPENSE":
return &actor{debitEffect: -1, creditEffect: 1, justYear: true, year: yr}
case "INCOME":
return &actor{debitEffect: 1, creditEffect: -1, justYear: true, year: yr}
case "EQUITY":
return &actor{debitEffect: 1, creditEffect: -1, justYear: false, year: yr}
case "LIABILITY":
return &actor{debitEffect: 1, creditEffect: -1, justYear: false, year: yr}
default:
panic("no such account type: " + ty)
}
}
What I'm doing here is defining exactly one "type" of account, to avoid code duplication, but then configuring it with properties to indicate how credits and debits should affect it, along with the "final" year it should collect and whether it should
just collect that year. I think I would rather have all these things be polymorphic functions, but this is only an experiment (and, in any case, I'm not sure I know how to polymorphic functions well in Go).
The code for
Credit and
Debit is very similar and should probably be combined, but is so simple that it didn't seem worth it. Each of them checks if the transaction was for "this year" if the account has been configured this way (note: there is a bug here which I spotted and fixed later in that even the other accounts should only accept transactions
prior to this year ending). It then asks the balance to incorporate the money with the appropriate "effect".
That is provided in the
Money class:
func (m *Money) Incorporate(effect int, other Money) {
m.Units += effect * other.Units
m.Subunits += effect * other.Subunits
m.Normalize()
}
func (m *Money) Normalize() {
for m.Subunits < 0 {
m.Subunits += 100
m.Units -= 1
}
for m.Subunits >= 100 {
m.Subunits -= 100
m.Units += 1
}
}
func (m Money) GCCredit() string {
return fmt.Sprintf("%d/100", 100*m.Units+m.Subunits)
}
func (m Money) GCDebit() string {
return "-" + m.GCCredit()
}
func (m Money) String() string {
return fmt.Sprintf("£%d.%02d", m.Units, m.Subunits)
}
GNUCASH_REPORT_2:accounts/internal/gnucash/writer/money.go
This is mainly boring complexity - the sort of thing that should be wrapped in half-a-dozen unit tests, but ...
Generating Reports
Then it's just a question of asking the data collector to spit out the reports when all the data for a given reporting period has been collected.
func AccountsPipeline(conf *config.Configuration) {
w := writer.MakeWriter(conf)
accts := accounts.MakeAccounts(conf, w)
sheets.ReadSpreadsheet(conf, accts)
for yr := 2018; yr <= 2025; yr++ {
r := reporter.NewReporter(conf, yr)
accts.Regurgitate(r)
r.ProfitLoss(yr)
r.BalanceSheet(yr)
}
}
GNUCASH_REPORT_3:accounts/internal/gnucash/pipeline/accounts.go
These reports are generated inside the
reporter (obviously):
func (r *reporter) ProfitLoss(yr int) {
fmt.Printf("Profit and Loss for %d\n", yr)
var total writer.Money
for name, acc := range r.Accounts {
if acc.IsPL() && acc.HasBalance() {
fmt.Printf(" %s: %s\n", name, acc.Balance())
total.Incorporate(acc.PLEffect(), acc.Balance())
}
}
fmt.Printf("Net Profit/Loss: %s\n", total)
}
func (r *reporter) BalanceSheet(yr int) {
fmt.Printf("Balance Sheet at %d-12-31\n", yr)
for name, acc := range r.Accounts {
if !acc.IsPL() && acc.HasBalance() {
fmt.Printf(" %s: %s\n", name, acc.Balance())
}
}
}
GNUCASH_REPORT_3:accounts/internal/gnucash/reporter/reporter.go
And depend, in turn, on additional methods to recover the fields of the
accounts.
func (a *actor) HasBalance() bool {
return a.balance.IsNonZero()
}
func (a *actor) IsPL() bool {
return a.justYear
}
func (a *actor) Balance() writer.Money {
return a.balance
}
func (a *actor) PLEffect() int {
return -a.creditEffect
}
GNUCASH_REPORT_3:accounts/internal/gnucash/reporter/accounts.go
While I'm here, I fixed the bug with not checking for transactions past the end of the reporting period:
func (a *actor) Credit(date writer.DateInfo, amount writer.Money) {
if a.justYear && date.Year != a.year {
return
}
if date.Year > a.year {
return
}
a.balance.Incorporate(a.creditEffect, amount)
// fmt.Printf("Collect credit %s => %s\n", amount, a.balance)
}
func (a *actor) Debit(date writer.DateInfo, amount writer.Money) {
if a.justYear && date.Year != a.year {
return
}
if date.Year > a.year {
return
}
a.balance.Incorporate(a.debitEffect, amount)
// fmt.Printf("Collect debit %s => %s\n", amount, a.balance)
}
GNUCASH_REPORT_3:accounts/internal/gnucash/reporter/accounts.go
Conclusion
OK, so I have gone on a little detour and figured out that I can calculate all the Profit & Loss and Balance Sheet data for myself without interacting with GnuCash. But I still need to actually submit accounts, and it's my belief that for that, I need iXBRL and the
ixbrl-reporter.