This document describes how individual invoice line items are processed during conversion between GOBL JSON and CII XML formats. Line items represent the itemized products or services being invoiced, including their quantities, prices, taxes, and related metadata.
For information about overall invoice structure, see Invoice Structure. For tax processing across the entire document, see Tax Processing and Mapping. For line-level allowances and charges, see Allowances and Charges.
Line items in CII XML are represented by the IncludedSupplyChainTradeLineItem element, which contains several sub-components that map to different aspects of GOBL's bill.Line structure. Each line item includes:
The following table shows the mapping between GOBL and CII structures:
| GOBL Structure | CII Element | Purpose |
|---|---|---|
bill.Line.Index | ram:AssociatedDocumentLineDocument/ram:LineID | Line identification number |
bill.Line.Item | ram:SpecifiedTradeProduct | Product/service details |
bill.Line.Item.Price | ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice | Unit price |
bill.Line.Quantity | ram:SpecifiedLineTradeDelivery/ram:BilledQuantity | Quantity and unit |
bill.Line.Taxes | ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax | Tax categories and rates |
bill.Line.Total | ram:SpecifiedLineTradeSettlement/...MonetarySummation/ram:LineTotalAmount | Line total amount |
Sources: lines.go10-92 lines_parse.go15-134
Diagram: GOBL to CII Line Item Conversion Flow
The conversion from GOBL to CII is initiated by the addLines() function, which iterates through each bill.Line in the GOBL invoice and creates corresponding CII Line structures through the newLine() function.
Sources: lines.go94-103 lines.go105-160 lines.go162-198
The newLine() function at lines.go105-160 performs the core conversion:
Creates LineDoc with the line index as the ID:
LineDoc: &LineDoc{
ID: strconv.Itoa(l.Index),
}
Creates Product with item name and price information
Creates LineAgreement with the net price from the item
Creates LineDelivery with quantity and UNECE unit code
Creates TradeSettlement by calling newTradeSettlement() for taxes and totals
The function handles several optional fields:
Sources: lines.go105-160
Diagram: CII to GOBL Line Item Parsing Flow
The parsing from CII to GOBL is initiated by the goblAddLines() function, which processes the CII Transaction.Lines array and creates GOBL bill.Line structures.
Sources: lines_parse.go15-134 lines_parse.go136-160
The goblAddLines() function at lines_parse.go15-134 performs several key operations:
Extracts price information from Agreement.NetPrice.Amount and converts to num.Amount
Creates base line structure with default quantity of 1 if not specified:
l := &bill.Line{
Quantity: num.MakeAmount(1, 0),
Item: &org.Item{
Name: strings.TrimSpace(it.Product.Name),
Price: &price,
},
Taxes: tax.Set{...},
}
Processes quantity if present in CII data
Maps UNECE unit code to GOBL unit using goblUnitFromUNECE()
Handles product identifiers:
SellerAssignedID → Item.RefBuyerAssignedID → Item.IdentitiesGlobalID → Item.Identities with scheme extensionProcesses optional fields:
Item.Meta)Sources: lines_parse.go15-134
Product information is represented differently in GOBL and CII, requiring careful mapping during conversion.
The system supports three types of product identifiers:
| Identifier Type | GOBL Location | CII Element | Notes |
|---|---|---|---|
| Seller's SKU | Item.Ref | ram:SellerAssignedID | Internal product reference |
| Buyer's reference | Item.Identities | ram:BuyerAssignedID | Buyer's product code |
| Global ID | Item.Identities with iso.ExtKeySchemeID | ram:GlobalID with schemeID | International identifier (GTIN, etc.) |
Global IDs use ISO 6523 ICD scheme identifiers. During GOBL to CII conversion at lines.go148-157:
if len(it.Identities) > 0 {
for _, id := range it.Identities {
if id.Ext.Has(iso.ExtKeySchemeID) {
lineItem.Product.GlobalID = &GlobalID{
SchemeID: id.Ext[iso.ExtKeySchemeID].String(),
Value: id.Code.String(),
}
}
}
}
During CII to GOBL parsing at lines_parse.go64-75:
if it.Product.GlobalID != nil {
l.Item.Identities = append(l.Item.Identities, &org.Identity{
Ext: tax.Extensions{
iso.ExtKeySchemeID: cbc.Code(it.Product.GlobalID.SchemeID),
},
Code: cbc.Code(it.Product.GlobalID.Value),
})
}
Common scheme IDs include:
0088 - EAN/GTIN0160 - GTIN (Global Trade Item Number)Sources: lines.go148-157 lines_parse.go64-75 test/data/convert/en16931/invoice-de-de.json111-117
Product characteristics in CII (ram:ApplicableProductCharacteristic) are mapped to GOBL's Item.Meta field during parsing at lines_parse.go121-127:
if len(it.Product.Characteristics) > 0 {
l.Item.Meta = make(cbc.Meta)
for _, char := range it.Product.Characteristics {
key := formatKey(char.Description)
l.Item.Meta[key] = char.Value
}
}
This allows arbitrary key-value pairs to be preserved during round-trip conversions.
Sources: lines_parse.go121-127 lines.go40-73
Quantities in CII include a unitCode attribute that uses UN/ECE Recommendation 20/21 codes (UNECE codes). GOBL uses its own unit system that must be mapped to and from UNECE codes.
During GOBL to CII conversion at lines.go124-129:
Quantity: &LineDelivery{
Quantity: &Quantity{
Amount: l.Quantity.String(),
UnitCode: string(it.Unit.UNECE()),
},
}
The Unit.UNECE() method converts GOBL's unit system to UNECE codes. Common mappings include:
| GOBL Unit | UNECE Code | Description |
|---|---|---|
h | HUR | Hours |
kg | KGM | Kilograms |
piece | C62 | Unit/piece |
m2 | MTK | Square meters |
During CII to GOBL parsing at lines_parse.go46-49:
if it.Quantity.Quantity.UnitCode != "" {
u := cbc.Code(it.Quantity.Quantity.UnitCode)
l.Item.Unit = goblUnitFromUNECE(u)
}
Sources: lines.go124-129 lines_parse.go46-49
If quantity is not present in the CII document, the parsing logic defaults to a quantity of 1 at lines_parse.go27:
Quantity: num.MakeAmount(1, 0),
If present, the quantity is parsed from the string value at lines_parse.go39-44:
if it.Quantity != nil && it.Quantity.Quantity != nil {
l.Quantity, err = num.AmountFromString(it.Quantity.Quantity.Amount)
if err != nil {
return err
}
}
Sources: lines_parse.go27 lines_parse.go39-44
The net price represents the unit price after any line-level discounts but before quantity multiplication.
In GOBL to CII conversion at lines.go118-122:
Agreement: &LineAgreement{
NetPrice: &NetPrice{
Amount: it.Price.String(),
},
}
In CII to GOBL parsing at lines_parse.go20-23:
price, err := num.AmountFromString(it.Agreement.NetPrice.Amount)
if err != nil {
return err
}
The price is stored in Item.Price for GOBL structures.
Sources: lines.go118-122 lines_parse.go20-23
The line total amount is calculated as quantity × net price, and stored in the TradeSettlement section at lines.go169-174:
stlm := &TradeSettlement{
ApplicableTradeTax: taxes,
Sum: &Summation{
Amount: l.Total.Rescale(2).String(),
},
}
The .Rescale(2) method ensures the amount is formatted with 2 decimal places, as required by most e-invoicing standards.
Sources: lines.go169-174
Each line item must specify the applicable tax category and rate. The tax information is stored in the SpecifiedLineTradeSettlement section.
The TradeSettlement struct at lines.go81-87 includes:
type TradeSettlement struct {
ApplicableTradeTax []*Tax
Period *Period
AllowanceCharge []*AllowanceCharge
Sum *Summation
}
During GOBL to CII conversion at lines.go162-167:
func newTradeSettlement(l *bill.Line) *TradeSettlement {
var taxes []*Tax
for _, tax := range l.Taxes {
t := makeTaxCategory(tax)
taxes = append(taxes, t)
}
// ...
}
The makeTaxCategory() function creates CII tax structures with:
TypeCode - Tax type (typically "VAT")CategoryCode - UNTDID tax category code (e.g., "S" for standard rate)RateApplicablePercent - Tax percentageDuring CII to GOBL parsing at lines_parse.go99-112:
if len(it.TradeSettlement.ApplicableTradeTax) > 0 {
for i, tax := range it.TradeSettlement.ApplicableTradeTax {
if tax.RateApplicablePercent != "" {
if !strings.HasSuffix(tax.RateApplicablePercent, "%") {
tax.RateApplicablePercent += "%"
}
p, err := num.PercentageFromString(tax.RateApplicablePercent)
if err != nil {
return err
}
l.Taxes[i].Percent = &p
}
}
}
The parser ensures the percentage string includes a "%" suffix before parsing.
Sources: lines.go162-167 lines_parse.go99-112 lines.go81-87
Line items can include charges (additional fees) and discounts (reductions) that modify the net price or line total.
The AllowanceCharge elements are part of the TradeSettlement at lines.go85:
AllowanceCharge []*AllowanceCharge
The getLineCharges() function at lines_parse.go136-160 processes line-level allowances and charges:
func getLineCharges(alwcs []*AllowanceCharge, l *bill.Line) (*bill.Line, error) {
for _, ac := range alwcs {
if ac.ChargeIndicator.Value {
c, err := goblNewLineCharge(ac)
if err != nil {
return nil, err
}
if l.Charges == nil {
l.Charges = make([]*bill.LineCharge, 0)
}
l.Charges = append(l.Charges, c)
} else {
d, err := goblNewLineDiscount(ac)
if err != nil {
return nil, err
}
if l.Discounts == nil {
l.Discounts = make([]*bill.LineDiscount, 0)
}
l.Discounts = append(l.Discounts, d)
}
}
return l, nil
}
The ChargeIndicator.Value determines whether the item is a charge (true) or discount (false).
During GOBL to CII conversion at lines.go193-195:
if len(l.Charges) > 0 || len(l.Discounts) > 0 {
stlm.AllowanceCharge = newLineAllowanceCharges(l)
}
Sources: lines_parse.go136-160 lines.go193-195 lines_parse.go114-119
Line items can specify a billing period indicating the timeframe for the service or product delivery.
The period is part of the TradeSettlement at lines.go84:
Period *Period
During GOBL to CII conversion at lines.go176-191:
if l.Period != nil {
stlm.Period = &Period{
Start: &IssueDate{
DateFormat: &Date{
Value: formatIssueDate(l.Period.Start),
Format: issueDateFormat,
},
},
End: &IssueDate{
DateFormat: &Date{
Value: formatIssueDate(l.Period.End),
Format: issueDateFormat,
},
},
}
}
The formatIssueDate() function at utils.go34-40 converts GOBL's cal.Date to the CII date format (YYYYMMDD):
func formatIssueDate(d cal.Date) string {
if d.IsZero() {
return ""
}
t := d.Time()
return t.Format("20060102")
}
The format code "102" indicates CCYYMMDD format as per UN/CEFACT date format codes.
Sources: lines.go176-191 utils.go34-40 utils.go12-13
Line-level notes can provide additional information about the line item. They are stored in the LineDoc structure at lines.go19-23:
type LineDoc struct {
ID string
Note []*Note
}
During GOBL to CII conversion at lines.go137-146:
if len(l.Notes) > 0 {
var notes []*Note
for _, n := range l.Notes {
notes = append(notes, &Note{
SubjectCode: n.Key.String(),
Content: n.Text,
})
}
lineItem.LineDoc.Note = notes
}
During CII to GOBL parsing at lines_parse.go85-97:
if it.LineDoc != nil && len(it.LineDoc.Note) > 0 {
l.Notes = make([]*org.Note, 0, len(it.LineDoc.Note))
for _, note := range it.LineDoc.Note {
n := &org.Note{}
if note.Content != "" {
n.Text = strings.TrimSpace(note.Content)
}
if note.SubjectCode != "" {
n.Key = cbc.Key(note.SubjectCode)
}
l.Notes = append(l.Notes, n)
}
}
The SubjectCode uses codes from the UNTDID 4451 code list for note subject classification.
Sources: lines.go137-146 lines_parse.go85-97 lines.go19-23
The following example from the test data shows a complete line item in both GOBL and CII formats:
GOBL Format (test/data/convert/en16931/invoice-de-de.json101-135):
CII XML Format (test/data/convert/en16931/out/invoice-de-de.xml16-50):
Sources: test/data/convert/en16931/invoice-de-de.json101-135 test/data/convert/en16931/out/invoice-de-de.xml16-50
Refresh this wiki