UblEx (UblEx v0.8.1)

Copy Markdown View Source

UBL (Universal Business Language) document generation and parsing for Elixir.

This library provides:

  • Peppol BIS Billing 3.0 compliant invoice, credit note, and application response generation
  • SBDH (Standard Business Document Header) support for Peppol network transmission
  • Fast SAX-based XML parsing using Saxy (1.4-4x faster than xmerl)
  • Full round-trip support (parse → generate → parse without data loss)

Quick Start

Generating a UBL Invoice

document_data = %{
  type: :invoice,
  number: "F2024001",
  date: ~D[2024-01-15],
  expires: ~D[2024-02-14],
  supplier: %{
    endpoint_id: "0797948229",
    scheme: "0208",
    name: "Company Name",
    street: "Street 40",
    city: "City",
    zipcode: "2180",
    country: "BE",
    vat: "BE0797948229",
    email: "invoice@company.com"
  },
  customer: %{
    endpoint_id: "0012345625",
    scheme: "0208",
    name: "Customer Name",
    vat: "BE0012345625",
    street: "Customer Street",
    housenumber: "10",
    city: "Brussels",
    zipcode: "1000",
    country: "BE"
  },
  details: [
    %{
      name: "Service",
      quantity: Decimal.new("1.00"),
      price: Decimal.new("100.00"),
      vat: Decimal.new("21.00"),
      discount: Decimal.new("0.00")
    }
  ]
}

xml = UblEx.generate(document_data)

Parsing a UBL Document

{:ok, parsed} = UblEx.parse(xml_content)

# Document type is in the data
case parsed.type do
  :invoice -> handle_invoice(parsed)
  :credit -> handle_credit_note(parsed)
  :application_response -> handle_response(parsed)
end

Tax Categories

Line items support all Peppol BIS 3.0 tax categories via the optional tax_category field:

AtomPeppol CodeUse Case
:standardSStandard rated VAT (default for 6/12/21%)
:zero_ratedZZero rated goods (default for 0% VAT)
:exemptEExempt from tax
:reverse_chargeAEDomestic reverse charge
:intra_communityKEU cross-border B2B (intra-community supply)
:exportGExport outside EU
:outside_scopeOServices outside scope of tax

If tax_category is not specified, it defaults to :standard for non-zero VAT and :zero_rated for 0% VAT.

Tax Exemption Fields

According to Peppol BIS 3.0 validation rules (BR-O-11 through BR-O-14), when using tax categories :exempt, :export, :intra_community, or :reverse_charge, you must provide tax exemption information via these fields:

  • tax_exemption_reason_code - A VATEX code (e.g., "vatex-eu-ic", "vatex-eu-ae")
  • tax_exemption_reason - A human-readable explanation (e.g., "Vrijgestelde intracommunautaire levering - Art. 39bis WBTW")

The tax_exemption_reason_code follows the format vatex-{country}-{code}:

  • vatex-eu-ic - Intra-community supply
  • vatex-eu-ae - Reverse charge / Autoliquidation
  • vatex-eu-g - Export outside EU
  • vatex-be-xxx - Belgian-specific codes

Example with Tax Categories and Exemption Fields

details: [
  %{
    name: "Standard Service",
    quantity: Decimal.new("1.00"),
    price: Decimal.new("100.00"),
    vat: Decimal.new("21.00"),
    discount: Decimal.new("0.00")
    # tax_category defaults to :standard
  },
  %{
    name: "EU Cross-Border Service",
    quantity: Decimal.new("1.00"),
    price: Decimal.new("500.00"),
    vat: Decimal.new("0.00"),
    discount: Decimal.new("0.00"),
    tax_category: :intra_community,
    tax_exemption_reason_code: "vatex-eu-ic",
    tax_exemption_reason: "Vrijgestelde intracommunautaire levering - Art. 39bis WBTW"
  }
]

Usage Pattern

Parse documents and handle them in your own code:

{:ok, parsed} = UblEx.parse(xml)
MyApp.save_invoice(parsed)
MyApp.send_notification(parsed)

Summary

Functions

Generate a Peppol-compliant UBL document based on the type field.

Generate a UBL document wrapped in SBDH (Standard Business Document Header).

Parse UBL XML with automatic schema detection.

Strip the StandardBusinessDocument/StandardBusinessDocumentHeader wrapper from UBL XML.

Functions

generate(document_data)

@spec generate(map()) :: String.t() | {:error, String.t()}

Generate a Peppol-compliant UBL document based on the type field.

Routes to the appropriate generator based on document_data.type:

  • :invoice → Invoice generator
  • :credit → CreditNote generator
  • :application_response → ApplicationResponse generator

Examples

# Parse and regenerate
{:ok, parsed} = UblEx.parse(xml)
regenerated_xml = UblEx.generate(parsed)

# Generate invoice
document_data = %{type: :invoice, number: "F001", ...}
xml = UblEx.generate(document_data)

# Generate application response
response_data = %{type: :application_response, id: "RESP-001", ...}
xml = UblEx.generate(response_data)

generate_with_sbdh(document_data)

@spec generate_with_sbdh(map()) :: String.t() | {:error, String.t()}

Generate a UBL document wrapped in SBDH (Standard Business Document Header).

SBDH is used in Peppol networks for routing and identification. This function generates the standard UBL document and wraps it with the SBDH header.

Example

document_data = %{type: :invoice, number: "F001", ...}
sbdh_xml = UblEx.generate_with_sbdh(document_data)

parse(xml_content)

@spec parse(String.t()) :: {:ok, map()} | {:error, String.t()}

Parse UBL XML with automatic schema detection.

Returns {:ok, parsed_data} or {:error, reason}.

The document type is available in parsed_data.type (:invoice, :credit, or :application_response).

Example

{:ok, parsed} = UblEx.parse(xml_content)
IO.inspect(parsed.type)  # :invoice, :credit, or :application_response

strip_sbdh(xml_content)

@spec strip_sbdh(String.t()) :: String.t()

Strip the StandardBusinessDocument/StandardBusinessDocumentHeader wrapper from UBL XML.

Some accounting software cannot process documents with SBDH wrappers. This function extracts the inner UBL document (Invoice, CreditNote, or ApplicationResponse).

If the XML is not wrapped in an SBDH, it is returned unchanged.

Example

{:ok, sbdh_xml} = File.read("invoice_with_sbdh.xml")
plain_xml = UblEx.strip_sbdh(sbdh_xml)