I am obviously going to do this by using an iXBRL abstraction and then use that to generate the iXBRL (as XML) from the data using an appropriate class. I'll need two such classes: one for the accounts and one for the computations.
To avoid having to deal with too many errors at once, I'm going to deal with the accounts first, and then come back to the computations; so while I do that, I'm going to go back to giving pathetic excuses for not submitting computations.
Starting at the beginning, we have to change submit to use an AccountsGenerator:
func Submit(conf *config.Config) error {
utr := conf.Utr
if utr == "" {
utr = conf.Business.TaxNum
}
ctr := &govtalk.IRenvelope{Business: conf.Business, ReturnType: "new",
Sender: "Company", // the type of business we are, I believe. The schema limits it to a handful of options
UTR: utr,
PeriodStart: "2025-01-01", PeriodEnd: "2025-12-31",
Turnover: 100000.0, TradingProfits: 0, LossesBroughtForward: 0, TradingNetProfits: 0,
CorporationTax: 0,
AccountsGenerator: conf.AccountsGenerator(),
// AccountsIXBRL: "ct600/micro-accounts.xml",
// ComputationIXBRL: "ct600/sample-ctcomp.xhtml",
NoComputationsReason: "Not within charge to CT",
}
CT600_IXBRL_GENERATION:accounts/internal/ct600/submission/submit.go
What is this AccountsGenerator? It's an interface (one of a pair) that I've introduced in gnucash/config (note that because of the code's history, there's also a ct600/config).package config
import "github.com/gmmapowell/ignorance/accounts/internal/ct600/ixbrl"
type AccountsGenerator interface {
Generate() *ixbrl.IXBRL
}
type ComputationsGenerator interface {
}
CT600_IXBRL_GENERATION:accounts/internal/gnucash/config/generators.go
And it's created in the config object:package config...
import "fmt"
type Configuration struct {
APIKey string
OAuth string
Token string
RedirectPort int
Spreadsheet string
Output string
Business Business
Accounts []Account
Verbs []Verb
VerbMap map[string]*Verb
}
func (config *Configuration) AccountsGenerator() AccountsGenerator {
return GnuCashAccountsIXBRLGenerator{config: config}
}
CT600_IXBRL_GENERATION:accounts/internal/gnucash/config/config.go
The actual implementation class is GnuCashAccountsIXBRLGenerator which can be found in config/accountsixbrl.go:package config
import "github.com/gmmapowell/ignorance/accounts/internal/ct600/ixbrl"
type GnuCashAccountsIXBRLGenerator struct {
config *Configuration
}
// Generate implements AccountsGenerator.
func (g GnuCashAccountsIXBRLGenerator) Generate() *ixbrl.IXBRL {
return ixbrl.NewIXBRL()
}
CT600_IXBRL_GENERATION:accounts/internal/gnucash/config/accountsixbrl.go
But I don't imagine it will be long before I move that off into its own package.At the moment, of course, it isn't doing very much; just returning a new IXBRL object, which is itself a new concept:
package ixbrl
import (
"github.com/gmmapowell/ignorance/accounts/internal/ct600/xml"
"github.com/unix-world/smartgoext/xml-utils/etree"
)
type IXBRL struct {
}
func (i *IXBRL) AsEtree() *etree.Element {
return xml.ElementWithNesting("html")
}
func NewIXBRL() *IXBRL {
return &IXBRL{}
}
CT600_IXBRL_GENERATION:accounts/internal/ct600/ixbrl/ixbrl.go
This class is going to (eventually) do a lot of the heavy lifting around iXBRL generation, but for now, it's just returning an empty <html> node.Also, for reasons of project circularity, I had to move the Element* code out of govtalk and into its own package, xml. This makes the checkin quite a bit bigger that would otherwise be necessary.
Not surprisingly, it doesn't work all that well:
2 error(s) reported:
Code Raised By Location Type Message
3001 Department business The submission of this document has failed due to departmental specific business logic in the Body tag.
4065 ChRIS /hd:GovTalkMessage[1]/hd:Body[1]/ct:IRenvelope[1]/ct:CompanyTaxReturn[1]/ct:AttachedFiles[1]/ct:XBRLsubmission[1]/ct:Accounts[1]/ct:Instance[1]/ct:InlineXBRLDocument[1]/ct:html[1] schema Invalid content found at element 'html'
submission failed: 2 error(s) reported
Generating an Outline
Regardless of the content of an iXBRL file, it will have some standard features: a set of schemas in the html element; a head; a body; a hidden ix:header element; and a series of pages. We are going to encode this structure into our IXBRL class.Here's a start:
package ixbrl
import (
"github.com/gmmapowell/ignorance/accounts/internal/ct600/xml"
"github.com/unix-world/smartgoext/xml-utils/etree"
)
type IXBRL struct {
title string
}
func (i *IXBRL) AsEtree() *etree.Element {
metaContent := xml.ElementWithNesting("meta")
metaContent.Attr = append(metaContent.Attr, etree.Attr{Key: "content", Value: "application/xhtml+xml; charset=UTF-8"}, etree.Attr{Key: "http-equiv", Value: "Content-Type"})
title := xml.ElementWithText("title", i.title)
head := xml.ElementWithNesting("head", metaContent, title)
body := xml.ElementWithNesting("body", i.ixHeader())
ret := xml.ElementWithNesting("html", head, body)
ret.Attr = append(ret.Attr, etree.Attr{Key: "xmlns", Value: "http://www.w3.org/1999/xhtml"}, etree.Attr{Space: "xmlns", Key: "xsi", Value: "http://www.w3.org/2001/XMLSchema-instance"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "ix", Value: "http://www.xbrl.org/2013/inlineXBRL"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "ixt", Value: "http://www.xbrl.org/inlineXBRL/transformation/2010-04-20"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "ixt2", Value: "http://www.xbrl.org/inlineXBRL/transformation/2011-07-31"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "xbrli", Value: "http://www.xbrl.org/2003/instance"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "xbrldi", Value: "http://xbrl.org/2006/xbrldi"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "link", Value: "http://www.xbrl.org/2003/linkbase"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "xlink", Value: "http://www.w3.org/1999/xlink"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "iso4217", Value: "http://www.xbrl.org/2003/iso4217"})
return ret
}
func (i *IXBRL) ixHeader() *etree.Element {
ixhidden := xml.ElementWithNesting("ix:hidden")
ixrefs := xml.ElementWithNesting("ix:references")
ixresources := xml.ElementWithNesting("ix:resources")
ixheader := xml.ElementWithNesting("ix:header", ixhidden, ixrefs, ixresources)
ret := xml.ElementWithNesting("div", ixheader)
ret.Attr = append(ret.Attr, etree.Attr{Key: "style", Value: "display: none"})
return ret
}
func NewIXBRL() *IXBRL {
return &IXBRL{}
}
CT600_IXBRL_GENERATE_OUTLINE:accounts/internal/ct600/ixbrl/ixbrl.go
I'm going to say it's an improvement, even though we now have three errors rather than just 2:3 error(s) reported:
Code Raised By Location Type Message
3001 Department business The submission of this document has failed due to departmental specific business logic in the Body tag.
0 ChRIS Accounts xbrl.core.xml.SchemaValidationError.cvc-complex-type24_b cvc-complex-type.2.4.b: The content of element 'ix:hidden' is not complete. One of '{"http://www.xbrl.org/2013/inlineXBRL":footnote, "http://www.xbrl.org/2013/inlineXBRL":fraction, "http://www.xbrl.org/2013/inlineXBRL":nonFraction, "http://www.xbrl.org/2013/inlineXBRL":nonNumeric, "http://www.xbrl.org/2013/inlineXBRL":tuple}' is expected.
0 ChRIS Accounts xbrl.core.xml.SchemaValidationError.cvc-complex-type24_b cvc-complex-type.2.4.b: The content of element 'ix:references' is not complete. One of '{"http://www.xbrl.org/2003/linkbase":schemaRef, "http://www.xbrl.org/2003/linkbase":linkbaseRef}' is expected.
submission failed: 3 error(s) reported
Generating Accounts Structure
To make further progress, we need to start adding more abstractions and providing those from the Accounts Generator. You may have noticed that we already slipped a title member into the IXBRL struct. We'll now add a schema to that (which is the schema to be defined in ix:references) and add lists for custom schema definitions and hidden propertiestype IXBRL struct {
schema string
title string
hidden []*IXProp
schemas []*ixSchema
}
type ixSchema struct {
id, schema string
}
type IXProp struct {
Type int
Context string
Name string
Format string
Decimals int
Unit string
Text string
}
const (
NonNumeric = iota
)
func (i *IXBRL) AddSchema(id, schema string) {
i.schemas = append(i.schemas, &ixSchema{id: id, schema: schema})
}
func (i *IXBRL) AddHidden(h *IXProp) {
i.hidden = append(i.hidden, h)
}
CT600_GENERATE_ACCOUNTS_STRUCTURE:accounts/internal/ct600/ixbrl/ixbrl.go
We can now make sure to include those schemas along with the pre-defined ones:func (i *IXBRL) AsEtree() *etree.Element {
metaContent := xml.ElementWithNesting("meta")
metaContent.Attr = append(metaContent.Attr, etree.Attr{Key: "content", Value: "application/xhtml+xml; charset=UTF-8"}, etree.Attr{Key: "http-equiv", Value: "Content-Type"})
title := xml.ElementWithText("title", i.title)
head := xml.ElementWithNesting("head", metaContent, title)
body := xml.ElementWithNesting("body", i.ixHeader())
ret := xml.ElementWithNesting("html", head, body)
ret.Attr = append(ret.Attr, etree.Attr{Key: "xmlns", Value: "http://www.w3.org/1999/xhtml"}, etree.Attr{Space: "xmlns", Key: "xsi", Value: "http://www.w3.org/2001/XMLSchema-instance"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "ix", Value: "http://www.xbrl.org/2013/inlineXBRL"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "ixt", Value: "http://www.xbrl.org/inlineXBRL/transformation/2010-04-20"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "ixt2", Value: "http://www.xbrl.org/inlineXBRL/transformation/2011-07-31"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "xbrli", Value: "http://www.xbrl.org/2003/instance"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "xbrldi", Value: "http://xbrl.org/2006/xbrldi"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "link", Value: "http://www.xbrl.org/2003/linkbase"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "xlink", Value: "http://www.w3.org/1999/xlink"})
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: "iso4217", Value: "http://www.xbrl.org/2003/iso4217"})
for _, s := range i.schemas {
ret.Attr = append(ret.Attr, etree.Attr{Space: "xmlns", Key: s.id, Value: s.schema})
}
return ret
}
CT600_GENERATE_ACCOUNTS_STRUCTURE:accounts/internal/ct600/ixbrl/ixbrl.go
Then in the ix:header, we can add the hidden elements:func (i *IXBRL) ixHeader() *etree.Element {And then the ix:references section should include the schema link:
ixhidden := xml.ElementWithNesting("ix:hidden")
for _, ixp := range i.hidden {
ixhidden.AddChild(ixp.AsEtree())
}
schemaLink := xml.ElementWithNesting("link:schemaRef")
schemaLink.Attr = append(schemaLink.Attr, etree.Attr{Space: "xlink", Key: "href", Value: i.schema})
schemaLink.Attr = append(schemaLink.Attr, etree.Attr{Space: "xlink", Key: "type", Value: "simple"})
ixrefs := xml.ElementWithNesting("ix:references", schemaLink)
ixresources := xml.ElementWithNesting("ix:resources")
ixheader := xml.ElementWithNesting("ix:header", ixhidden, ixrefs, ixresources)
CT600_GENERATE_ACCOUNTS_STRUCTURE:accounts/internal/ct600/ixbrl/ixbrl.go
Then we need to go to the AccountsGenerator and make it generate the appropriate elements:// Generate implements AccountsGenerator.
func (g *GnuCashAccountsIXBRLGenerator) Generate() *ixbrl.IXBRL {
ret := ixbrl.NewIXBRL(g.config.Business.Name+" - Financial Statements", "https://xbrl.frc.org.uk/FRS-102/2025-01-01/FRS-102-2025-01-01.xsd")
ret.AddSchema("bus", "http://xbrl.frc.org.uk/cd/2025-01-01/business")
ret.AddHidden(&ixbrl.IXProp{Type: ixbrl.NonNumeric, Context: "CY", Name: "bus:NameProductionSoftware", Text: "Ziniki HMRC"})
return ret
}
CT600_GENERATE_ACCOUNTS_STRUCTURE:accounts/internal/gnucash/config/accountsixbrl.go
And then we can try submitting again:2 error(s) reported:So, yes, we have referencede the context CY (for current year) but we haven't defined it. Let's do that.
Code Raised By Location Type Message
3001 Department business The submission of this document has failed due to departmental specific business logic in the Body tag.
0 ChRIS Accounts xbrl.ixbrl.UnknownContext Cannot find context definition with id 'CY' on element '{http://xbrl.frc.org.uk/cd/2025-01-01/business}NameProductionSoftware'.
submission failed: 2 error(s) reported
Defining Contexts
Contexts are by far the most confusing aspects of this whole iXBRL thing (in my opinion, at least). I have gained some insight by reducing the computations iXBRL file to a minimal skeleton. In spite of the fact that the computations only cover one year, I still need four contexts. There appear to be two reasons for this: one is that there are things that want an "instant" (or at least a single date) when a fact is true, while others want a date range (such as a financial year); and the other is that there are multiple "dimensions" which are needed for a given fact, and the context must include those dimensions explicitly.I also notice from the micro-accounts.xml file which I'm currently trying to recreate computationally, that there is an xbrli:entity element with an xbrli:identifier and an optional xbrli:segment with an explicitMember. Hopefully, we'll discover why these exist as we go through the process.
But for now, let's just define the context that we need:
// Generate implements AccountsGenerator.
func (g *GnuCashAccountsIXBRLGenerator) Generate() *ixbrl.IXBRL {
ret := ixbrl.NewIXBRL(g.config.Business.Name+" - Financial Statements", "https://xbrl.frc.org.uk/FRS-102/2025-01-01/FRS-102-2025-01-01.xsd")
ret.AddSchema("bus", "http://xbrl.frc.org.uk/cd/2025-01-01/business")
ret.AddContext(&ixbrl.Context{ID: "CY", IdentifierScheme: "http://www.companieshouse.gov.uk/", Identifier: g.config.Business.ID, FromDate: ixbrl.NewDate(g.config.Ranges["Curr"].Start), ToDate: ixbrl.NewDate(g.config.Ranges["Curr"].End)})
ret.AddHidden(&ixbrl.IXProp{Type: ixbrl.NonNumeric, Context: "CY", Name: "bus:NameProductionSoftware", Text: "Ziniki HMRC"})
return ret
}
CT600_FIRST_CONTEXT:accounts/internal/gnucash/config/accountsixbrl.go
Which obviously depends on the existence of a context object:type IXBRL struct {...
schema string
title string
hidden []*IXProp
schemas []*ixSchema
contexts []*Context
}
type Context struct {...
ID string
IdentifierScheme string
Identifier string
FromDate Date
ToDate Date
}
func (i *IXBRL) AddContext(c *Context) {...
i.contexts = append(i.contexts, c)
}
func (cx *Context) AsEtree() *etree.Element {
ret := xml.ElementWithNesting("xbrli:context")
ret.Attr = append(ret.Attr, etree.Attr{Key: "id", Value: cx.ID})
entity := xml.ElementWithNesting("xbrli:entity")
identifier := xml.ElementWithNesting("xbrli:identifier")
identifier.Attr = append(identifier.Attr, etree.Attr{Key: "scheme", Value: cx.IdentifierScheme})
entity.AddChild(identifier)
ret.AddChild(entity)
period := xml.ElementWithNesting("xbrli:period")
sd := xml.ElementWithText("xbrli:startDate", cx.FromDate.isoDate)
period.AddChild(sd)
ed := xml.ElementWithText("xbrli:endDate", cx.ToDate.isoDate)
period.AddChild(ed)
ret.AddChild(period)
return ret
}
CT600_FIRST_CONTEXT:accounts/internal/ct600/ixbrl/ixbrl.go
Most of the work here is obviously done in AsEtree, which converts the context created above into the appropriate XML.Finally, we have previously hacked in the date ranges, but that is no longer acceptable, so they are now part of the configuration file:
type Configuration struct {...
APIKey string
OAuth string
Token string
RedirectPort int
Spreadsheet string
Output string
Business Business
Ranges map[string]DateRange
Accounts []Account
Verbs []Verb
VerbMap map[string]*Verb
}
type DateRange struct {
Start string
End string
}
CT600_FIRST_CONTEXT:accounts/internal/gnucash/config/config.go
And then we can try submitting again:16 error(s) reported:After months of struggling with errors that make no sense, I finally feel a sense of relief at the appearance of this list of errors. It makes sense! Yes, that is a fairly accurate summary of some (all?) of the things I still have to do.
Code Raised By Location Type Message
3001 Department business The submission of this document has failed due to departmental specific business logic in the Body tag.
3312 ChRIS Accounts business Company Reference Number (bus:UKCompaniesHouseRegisteredNumber) is missing.
3318 ChRIS Accounts business The period to which this Return's Accounts applies does not coincide with the effective from/to dates of the referenced Accounts Taxonomy. Please correct and re-submit.
3312 ChRIS Accounts business Legal form of entity (bus:LegalFormEntity) is missing.
3312 ChRIS Accounts business Accounts type, full or abbreviated (bus:AccountsType) is missing.
3312 ChRIS Accounts business Balance Sheet Date of Approval (core:DateAuthorisationFinancialStatementsForIssue) is missing.
3312 ChRIS Accounts business Description of principal activities (bus:DescriptionPrincipalActivities) is missing.
3312 ChRIS Accounts business Accounts status, audited or unaudited (bus:AccountsStatusAuditedOrUnaudited) is missing.
3312 ChRIS Accounts business Trading/non-trading indicator (bus:EntityTradingStatus) is missing.
3312 ChRIS Accounts business Accounting standards applied (bus:AccountingStandardsApplied) is missing.
3312 ChRIS Accounts business Period Start Date (bus:StartDateForPeriodCoveredByReport) is missing.
3312 ChRIS Accounts business Balance Sheet Date (bus:BalanceSheetDate) is missing.
3312 ChRIS Accounts business Period End Date (bus:EndDateForPeriodCoveredByReport) is missing.
3312 ChRIS Accounts business Company Name (bus:EntityCurrentLegalOrRegisteredName) is missing.
3312 ChRIS Accounts business Dormant/non-dormant indicator (bus:EntityDormantTruefalse) is missing.
3312 ChRIS Accounts business Name of Director Approving Balance Sheet (core:DirectorSigningFinancialStatements) is missing.
submission failed: 16 error(s) reported