Friday, January 30, 2026

Time to Start Generating the iXBRLs

Given that we now have something that works, it's time to start trying to generate "that" (or something like that) from the actual accounts data that we've downloaded and calculated from Google Sheets.

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 properties
type 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 {
    ixhidden := xml.ElementWithNesting("ix:hidden")
    for _, ixp := range i.hidden {
        ixhidden.AddChild(ixp.AsEtree())
    }
And then the ix:references section should include the schema link:
    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:
  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
So, yes, we have referencede the context CY (for current year) but we haven't defined it. Let's do that.

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

Conclusion

We have done the outline of the submission. I think most of the next chunk of work is fairly boilerplate, so I'm going to do it off camera (although I'll still check in publicly). I'll probably return with the next episode when it's time to put the actual accounts and computations into the iXBRL.

No comments:

Post a Comment