This page documents the processing of invoice line items during conversion between GOBL and UBL formats. Line items represent the individual goods or services being invoiced, including quantities, prices, descriptions, tax categories, and line-level charges or discounts.
For document-level charges and discounts, see Charges and Discounts. For tax calculation and aggregation logic, see Financial Totals and Taxes.
The line items subsystem handles bidirectional conversion of the core transactional data in an invoice. Each line item contains:
The conversion process differs significantly between directions due to UBL's BaseQuantity feature for bulk pricing, which requires special precision handling during parsing.
Line Item Processing Flow
Sources: lines.go27-197 lines_parse.go18-124
The InvoiceLine struct represents a single line item in UBL format. It supports both Invoice and CreditNote documents through conditional field usage.
InvoiceLine XML/Struct Mapping
Sources: lines.go12-25
| UBL Field | GOBL Source | GOBL Target | Description |
|---|---|---|---|
ID | l.Index | - | Line number as string (1-indexed) |
InvoicedQuantity | l.Quantity | l.Quantity | Quantity for standard invoices (with unitCode) |
CreditedQuantity | l.Quantity | l.Quantity | Quantity for credit notes (with unitCode) |
LineExtensionAmount | l.Total | - | Line total before taxes |
AccountingCost | Note with key "buyer-accounting-ref" | l.Cost | Buyer's internal accounting reference |
OrderLineReference.LineID | l.Order | l.Order | Reference to order line number |
AllowanceCharge[] | l.Charges[], l.Discounts[] | l.Charges[], l.Discounts[] | Line-level charges and discounts |
Item | l.Item | l.Item | Item details (see below) |
Price.PriceAmount | l.Item.Price | l.Item.Price | Unit price (before BaseQuantity division) |
Price.BaseQuantity | - | Divides price | Number of units to which price applies |
Sources: lines.go39-196 lines_parse.go42-124
The addLines() function orchestrates the conversion of GOBL line items to UBL format. It handles the structural differences between the two formats and ensures all mandatory UBL fields are populated.
Line Conversion Process
Sources: lines.go26-196
Quantity is a mandatory field in UBL line items. The system distinguishes between standard invoices and credit notes:
InvoicedQuantity lines.go57CreditedQuantity lines.go55Both quantity types include the unitCode attribute, which is derived from the GOBL item's unit using UNECE code mapping:
iq := &Quantity{
Value: l.Quantity.String(),
}
if l.Item != nil && l.Item.Unit != "" {
iq.UnitCode = string(l.Item.Unit.UNECE())
}
The quantity field is always set, even when the value is zero, ensuring UBL schema compliance lines.go48-58
Sources: lines.go48-58 lines_test.go55-69
GOBL line notes are processed with special handling for the buyer-accounting-ref key:
"buyer-accounting-ref" are mapped to AccountingCost lines.go63-64Note[] array lines.go66-67This separation allows UBL-compliant placement of buyer accounting references in a dedicated field rather than free-text notes.
Sources: lines.go60-72
The Item element contains detailed information about the product or service being invoiced. The conversion logic handles multiple aspects of item data.
Item Conversion Logic
Sources: lines.go84-187
Tax category information on line items is crucial for UBL compliance. The conversion extracts tax data from the first tax entry in the GOBL line:
untdid-tax-category extension (e.g., "S" for standard rate) lines.go117-120This logic ensures that UBL tax categories are correctly populated while respecting the EN16931 rules for tax-exempt and out-of-scope transactions.
Sources: lines.go110-136
Item identities in GOBL can map to multiple UBL fields depending on the presence of scheme identifiers:
Identity Mapping Strategy
Mapping Rules:
BuyersItemIdentification lines.go145-153StandardItemIdentification lines.go156-165Example: An item with identities [{code: "ABC123"}, {code: "1234567890128", ext: {"iso-scheme-id": "0088"}}] maps to:
BuyersItemIdentification.ID.Value = "ABC123" (no scheme)StandardItemIdentification.ID.Value = "1234567890128" with SchemeID = "0088" (GTIN scheme)Sources: lines.go138-167 lines_test.go44-52
The GOBL Item.Ref field maps directly to SellersItemIdentification lines.go180-186:
if l.Item.Ref != "" {
invLine.Item.SellersItemIdentification = &ItemIdentification{
ID: &IDType{
Value: l.Item.Ref.String(),
},
}
}
This allows the seller to specify their internal product/service reference code.
Sources: lines.go180-186 lines_test.go42
The makeLineCharges() function converts GOBL line charges and discounts into UBL AllowanceCharge elements. For document-level charges, see Charges and Discounts.
Charge/Discount Conversion
Charges (additional fees) are converted with ChargeIndicator = true lines.go200-220:
| GOBL Field | UBL Field | Notes |
|---|---|---|
ch.Amount | Amount.Value | Charge amount in line currency |
ch.Ext[untdid-charge] | AllowanceChargeReasonCode | UNTDID 5189 code (e.g., "AA" for advertising) |
ch.Reason | AllowanceChargeReason | Human-readable reason text |
ch.Percent | MultiplierFactorNumeric | Percentage as base value (e.g., 0.10 for 10%) |
Discounts are converted with ChargeIndicator = false lines.go221-241:
| GOBL Field | UBL Field | Notes |
|---|---|---|
d.Amount | Amount.Value | Discount amount in line currency |
d.Ext[untdid-allowance] | AllowanceChargeReasonCode | UNTDID 5189 code (e.g., "95" for discount) |
d.Reason | AllowanceChargeReason | Human-readable reason text |
d.Percent | MultiplierFactorNumeric | Percentage as base value (e.g., 0.10 for 10%) |
The percentage value is converted using .Base().String() to ensure it's represented as a decimal (0.10) rather than a percentage string (10%) lines.go216-238
Sources: lines.go199-258 lines_test.go23-28
The parsing direction handles conversion from UBL InvoiceLines or CreditNoteLines to GOBL bill.Line structures. This direction is more complex due to UBL's BaseQuantity feature and the need to reconstruct tax information from multiple sources.
UBL Parsing Functions
Sources: lines_parse.go18-40 lines_parse.go42-124
UBL's Price.BaseQuantity field specifies the number of item units to which the price applies. For example, a price of €200 for 2 units means the unit price is €100. When dividing the price by the base quantity, precision must be carefully managed to avoid rounding errors.
The calculateRequiredPrecision() function dynamically determines the required decimal precision:
Precision Calculation Formula: price_decimals + ceil(log10(base_quantity))
Examples:
Precision Calculation Logic
The calculated precision is then used to rescale the price before division lines_parse.go52-64:
precision := calculateRequiredPrecision(price, baseQuantity)
price = price.RescaleUp(precision).Divide(baseQuantity)
Sources: lines_parse.go52-64 lines_parse.go126-144 lines_parse_test.go126-160
During parsing, line items may reference tax categories that don't include the exemption reason code. The system builds a tax category map from the document-level TaxTotal section and uses it to populate missing exemption codes lines_parse.go27-36:
Tax Category Map Building
When parsing line taxes, if the TaxExemptionReasonCode is not present in ClassifiedTaxCategory, the parser looks it up from the map lines_parse.go192-199:
if ctc.TaxExemptionReasonCode != nil {
line.Taxes[0].Ext[cef.ExtKeyVATEX] = cbc.Code(*ctc.TaxExemptionReasonCode)
} else {
// Try to get exemption code from TaxTotal
key := buildTaxCategoryKey(ctc.TaxScheme.ID, *ctc.ID)
if info, ok := taxCategoryMap[key]; ok && info.exemptionReasonCode != "" {
line.Taxes[0].Ext[cef.ExtKeyVATEX] = cbc.Code(info.exemptionReasonCode)
}
}
Sources: lines_parse.go26-36 lines_parse.go175-221
Special logic prevents GOBL from normalizing tax rates to "zero" for exempt or reverse-charge cases. If the tax percent is 0% but the category is not "Z" (zero-rated), the percent is not set, allowing GOBL to infer the correct rate from the category extension lines_parse.go209-213:
// Skip setting percent if it's 0% and tax category is not "Z" (zero-rated)
// This prevents GOBL from normalizing to "zero" tax rate for exempt/reverse-charge cases
if percent.IsZero() && ctc.ID != nil && *ctc.ID != "Z" {
return
}
Sources: lines_parse.go202-220
The parsing direction extracts item identities from multiple UBL fields and maps them to GOBL's unified org.Identity structure:
| UBL Field | GOBL Mapping | Notes |
|---|---|---|
BuyersItemIdentification.ID | org.Identity without label | First identity without scheme |
StandardItemIdentification.ID | org.Identity with iso-scheme-id extension | Contains SchemeID attribute |
CommodityClassification[].ItemClassificationCode | org.Identity array | Multiple classifications |
The goblIdentity() function extracts label information from the first non-nil field among SchemeID, ListID, ListVersionID, SchemeName, or Name lines_parse.go260-274:
for _, field := range []*string{id.SchemeID, id.ListID, id.ListVersionID, id.SchemeName, id.Name} {
if field != nil {
identity.Label = *field
break
}
}
Sources: lines_parse.go223-258 lines_parse.go260-274
The goblLineCharges() function converts UBL AllowanceCharge elements back to GOBL line charges and discounts based on the ChargeIndicator field lines_parse.go276-299:
ChargeIndicator = true → bill.LineChargeChargeIndicator = false → bill.LineDiscountEach allowance/charge is processed by goblLineCharge() or goblLineDiscount() functions, which extract:
Sources: lines_parse.go276-299
When a GOBL line item includes an order reference, it is mapped to the OrderLineReference element lines.go75-79:
if l.Order != "" {
invLine.OrderLineReference = &OrderLineReference{
LineID: l.Order.String(),
}
}
During parsing, the OrderLineReference.LineID is extracted back to the GOBL line.Order field lines_parse.go109-111:
if docLine.OrderLineReference != nil && docLine.OrderLineReference.LineID != "" {
line.Order = cbc.Code(docLine.OrderLineReference.LineID)
}
This bidirectional mapping allows invoice lines to reference specific purchase order line numbers for automated matching.
Sources: lines.go75-79 lines_parse.go109-111 lines_test.go40-41 lines_parse_test.go105-123
UBL requires the quantity field to always be present, even when the quantity is zero. The conversion ensures this by always setting either InvoicedQuantity or CreditedQuantity lines.go48-59:
// Always set quantity (mandatory field)
iq := &Quantity{
Value: l.Quantity.String(),
}
if l.Item != nil && l.Item.Unit != "" {
iq.UnitCode = string(l.Item.Unit.UNECE())
}
This prevents validation errors when processing invoices with zero-quantity lines (e.g., cancelled items or placeholder lines).
Sources: lines.go48-59 lines_test.go55-69 test/data/convert/invoice-zero-quantity.json69-90
Line items can have their own currency that differs from the document currency. The conversion logic prioritizes item-level currency:
ccy := l.Item.Currency.String()
if ccy == "" {
ccy = inv.Currency.String()
}
This currency is then applied to LineExtensionAmount, Price.PriceAmount, and AllowanceCharge.Amount within the line lines.go34-44
Sources: lines.go34-44
The system automatically selects the correct line array based on document type lines.go191-195:
ui.CreditNoteLinesui.InvoiceLinesThis ensures the generated XML uses the correct element names (<cac:CreditNoteLine> vs <cac:InvoiceLine>).
Sources: lines.go191-195
Complete Line Item Conversion Pipeline
Sources: lines.go26-196 lines_test.go11-31 test/data/convert/invoice-zero-quantity.json1-138 test/data/convert/out/invoice-zero-quantity.xml92-109
Refresh this wiki