transform_to_ubl
Convert JSON invoice data to UBL 2.1 XML for separate validation or direct submission to a platform.
Instructions
Convert a structured JSON invoice payload to UBL 2.1 XML.
Unlike generate_invoice_be, this tool does not run validation after
transformation. Intended as a conversion step when the caller will validate
separately or submit directly to a platform that performs its own validation.
Returns a dict with:
xml: the generated UBL 2.1 XML stringwarnings: list of non-fatal issues detected during transformation
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| data | Yes | Source invoice data matching the BEInvoice schema |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- The main handler function for the transform_to_ubl tool. Takes structured invoice data, validates it via BEInvoice model, generates warnings for missing VAT/IBAN, and calls render_ubl_invoice to produce UBL 2.1 XML.
async def transform_to_ubl( data: Annotated[ dict[str, Any], "Source invoice data matching the BEInvoice schema", ], ) -> dict[str, object]: """Convert a structured JSON invoice payload to UBL 2.1 XML. Unlike ``generate_invoice_be``, this tool does not run validation after transformation. Intended as a conversion step when the caller will validate separately or submit directly to a platform that performs its own validation. Returns a dict with: - ``xml``: the generated UBL 2.1 XML string - ``warnings``: list of non-fatal issues detected during transformation """ invoice = BEInvoice.model_validate(data) warnings: list[str] = [] if not getattr(invoice.buyer, "tax_id", None): warnings.append( "Customer VAT number is absent — acceptable for B2C but required for most B2B profiles." ) terms = invoice.payment if invoice.payment_means_code == "30": iban = getattr(terms, "iban", None) if terms else None if not iban: warnings.append("Payment means is credit transfer (code 30) but no IBAN was provided.") elif not validate_iban(iban): warnings.append(f"IBAN '{iban}' does not appear to be valid.") xml_string = render_ubl_invoice( invoice=invoice, customization_id=CUSTOMIZATION_IDS["peppol-bis-3"], profile_id=PROFILE_IDS["peppol-bis-3"], namespaces=UBL_NAMESPACES, ) return {"xml": xml_string, "warnings": warnings} - src/mcp_einvoicing_be/server.py:20-28 (registration)Registration of the transform_to_ubl handler via mcp.tool()(transform_to_ubl) inside _register_be_tools, which is invoked by mcp.register_plugin.
def _register_be_tools(mcp: Any) -> None: """Register all Belgian e-invoicing tools onto the shared FastMCP instance.""" mcp.tool()(_validator.validate_invoice_be) mcp.tool()(_validator.validate_pint_be) mcp.tool()(_generator.generate_invoice_be) mcp.tool()(transform_to_ubl) mcp.tool()(lookup_vat_be) mcp.tool()(check_peppol_participant_be) mcp.tool()(get_invoice_types_be) - BEInvoice Pydantic model used as the input schema for transform_to_ubl. Extends InvoiceDocument with Belgian-specific fields (profile, document_type, seller, buyer, lines, payment, etc.).
class BEInvoice(InvoiceDocument): # type: ignore[misc] """Belgian e-invoice. Extends ``InvoiceDocument`` with Belgium-specific fields: PINT-BE profile selection, Belgian party types, and Belgian payment terms. """ document_type: Literal["380", "381", "383"] = Field( default="380", description="UNTDID 1001 code: 380=Invoice, 381=Credit note, 383=Debit note", ) profile: Literal["peppol-bis-3", "pint-be"] = Field( default="peppol-bis-3", description="Belgian Peppol profile to apply", ) seller: Supplier buyer: Customer lines: list[BEInvoiceLine] = Field(..., min_length=1) payment: BEPaymentTerms | None = Field(default=None) order_reference: str | None = Field( default=None, description="Purchase order reference (BT-13)" ) # noqa: E501 contract_reference: str | None = Field(default=None, description="Contract reference (BT-12)") payment_means_code: str = Field( default="30", description="UNTDID 4461 payment means code (30=credit transfer)", ) @field_validator("currency", check_fields=False) @classmethod def uppercase_currency(cls, v: str) -> str: return v.upper() # Re-export the core validation result — no Belgium-specific fields needed. ValidationResult = DocumentValidationResult - render_ubl_invoice — the core UBL XML rendering function called by transform_to_ubl. Serializes a BEInvoice to UBL 2.1 XML string.
def render_ubl_invoice( invoice: BEInvoice, customization_id: str, profile_id: str, namespaces: dict[str, str], ) -> str: """Serialize a ``BEInvoice`` to a UBL 2.1 Invoice XML string.""" root = Element(_q(_UBL_INVOICE_NS, "Invoice")) _el(root, _q(_CBC, "CustomizationID"), customization_id) _el(root, _q(_CBC, "ProfileID"), profile_id) _el(root, _q(_CBC, "ID"), invoice.number) _el(root, _q(_CBC, "IssueDate"), invoice.date) _el(root, _q(_CBC, "InvoiceTypeCode"), invoice.document_type) _el(root, _q(_CBC, "DocumentCurrencyCode"), invoice.currency) _el_opt(root, _q(_CBC, "Note"), getattr(invoice, "note", None)) if invoice.order_reference: order_ref = SubElement(root, _q(_CAC, "OrderReference")) _el(order_ref, _q(_CBC, "ID"), invoice.order_reference) if invoice.contract_reference: contract_ref = SubElement(root, _q(_CAC, "ContractDocumentReference")) _el(contract_ref, _q(_CBC, "ID"), invoice.contract_reference) _render_party(root, "AccountingSupplierParty", invoice.seller) _render_party(root, "AccountingCustomerParty", invoice.buyer) _render_payment_means(root, invoice) if invoice.payment and invoice.payment.due_date: pt = SubElement(root, _q(_CAC, "PaymentTerms")) _el(pt, _q(_CBC, "Note"), f"Due: {invoice.payment.due_date}") _render_tax_total(root, invoice) _render_legal_monetary_total(root, invoice) for idx, line in enumerate(invoice.lines, start=1): line_el = SubElement(root, _q(_CAC, "InvoiceLine")) _el(line_el, _q(_CBC, "ID"), str(idx)) qty_el = _el(line_el, _q(_CBC, "InvoicedQuantity"), format_quantity(line.quantity or 0)) qty_el.set("unitCode", line.unit_code) ext_el = _el( line_el, _q(_CBC, "LineExtensionAmount"), format_amount((line.quantity or 0) * line.unit_price), ) ext_el.set("currencyID", invoice.currency) item_el = SubElement(line_el, _q(_CAC, "Item")) _el(item_el, _q(_CBC, "Description"), line.description) _el_opt( # noqa: E501 item_el, _q(_CBC, "SellersItemIdentification"), getattr(line, "buyer_item_id", None) ) cls_tax = SubElement(item_el, _q(_CAC, "ClassifiedTaxCategory")) _el(cls_tax, _q(_CBC, "ID"), line.vat_category.value) _el(cls_tax, _q(_CBC, "Percent"), str(line.vat_rate)) ts = SubElement(cls_tax, _q(_CAC, "TaxScheme")) _el(ts, _q(_CBC, "ID"), "VAT") price_el = SubElement(line_el, _q(_CAC, "Price")) price_amt = _el(price_el, _q(_CBC, "PriceAmount"), format_amount(line.unit_price)) price_amt.set("currencyID", invoice.currency) return tostring(root, encoding="unicode", xml_declaration=False) - CUSTOMIZATION_IDS and PROFILE_IDS constants used by transform_to_ubl to set the UBL customization and profile identifiers.
"""Peppol BIS Billing 3.0 constants for Belgium.""" # UBL customizationID values (BT-24) CUSTOMIZATION_IDS: dict[str, str] = { "peppol-bis-3": ("urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"), "pint-be": ( "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0" "#conformant#urn:fdc:www.nbb.be:2020:pintbe" ), } # UBL profileID values (BT-23) PROFILE_IDS: dict[str, str] = { "peppol-bis-3": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0", "pint-be": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0", }