Generate UBL Invoices in Python using Poetry and lxml

Photo by FIN on Unsplash

Generate UBL Invoices in Python using Poetry and lxml

For now with some fake data

Have you ever wanted to generate UBL (Universal Business Language) invoice files with random data for testing purposes? In this tutorial, we'll walk you through creating a Python script that generates UBL invoice files with random data using the lxml and Faker libraries. We'll also use Poetry for dependency management and packaging, as well as write tests with the unittest library.

Project Setup

First, make sure you have Poetry installed. If not, follow the installation instructions here.

Create a new directory for your project and navigate to it:

mkdir ubl_invoice_generator
cd ubl_invoice_generator

Initialize the project using Poetry:

poetry init

Dependencies

We'll use lxml to create XML files, faker to generate random data, and unittest for testing. Add these packages to your pyproject.toml file:

[tool.poetry.dependencies]
python = "^3.9"
lxml = "^4.6.3"
faker = "^8.10.0"

[tool.poetry.dev-dependencies]
unittest = "*"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Install the dependencies using Poetry:

poetry install

Creating the Invoice Generator

Create a file named invoice_generator.py in the project folder. We'll implement the following functions:

  1. create_ubl_invoice(invoice_data): Generates an invoice XML element with the given data.

  2. save_invoice_to_file(invoice, file_path): Saves the invoice XML element to a file.

  3. create_random_invoice_data(): Generates random invoice data using the Faker library.

import os
from lxml import etree
from faker import Faker

fake = Faker()

# Function 1: Generates an invoice XML element
def create_ubl_invoice(invoice_data):
    # ...

# Function 2: Saves the invoice XML element to a file
def save_invoice_to_file(invoice, file_path):
    # ...

# Function 3: Generates random invoice data
def create_random_invoice_data():
    # ...

In the create_ubl_invoice() function, we create an XML element representing the invoice using the lxml library. We add the necessary invoice elements such as "ID" and "IssueDate" as child elements and some other elements so it looks like a real invoice with products and vat.

def create_ubl_invoice(invoice_data):
    root = etree.Element("Invoice", nsmap={None: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"})
    etree.SubElement(root, "ID").text = invoice_data["id"]
    etree.SubElement(root, "IssueDate").text = invoice_data["issue_date"]

    # Add party name and address details
    party = etree.SubElement(root, "AccountingCustomerParty")
    party_name = etree.SubElement(party, "PartyName")
    etree.SubElement(party_name, "Name").text = invoice_data["name"]
    address = etree.SubElement(party, "PostalAddress")
    etree.SubElement(address, "StreetName").text = invoice_data["street"]
    etree.SubElement(address, "CityName").text = invoice_data["city"]
    etree.SubElement(address, "CountrySubentity").text = invoice_data["state"]
    etree.SubElement(address, "PostalZone").text = invoice_data["zipcode"]
    etree.SubElement(address, "Country").text = invoice_data["country"]

    # Add invoice lines with products
    for product in invoice_data["products"]:
        invoice_line = etree.SubElement(root, "InvoiceLine")
        etree.SubElement(invoice_line, "ID").text = product["id"]
        etree.SubElement(invoice_line, "InvoicedQuantity").text = str(product["quantity"])
        etree.SubElement(invoice_line, "LineExtensionAmount").text = str(product["price"])

    # Add VAT and total amount
    etree.SubElement(root, "TaxTotal").text = str(invoice_data["vat"])
    etree.SubElement(root, "LegalMonetaryTotal").text = str(invoice_data["total_amount"])

    return root

The save_invoice_to_file() function takes an invoice XML element and a file path as arguments, then writes the XML element to the file using lxml's ElementTree.write() method.

def save_invoice_to_file(invoice, file_path):
    tree = etree.ElementTree(invoice)
    tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")

The create_random_invoice_data() function generates random invoice data using the Faker library. It returns a dictionary containing random invoice data like ID and IssueDate.

def create_random_invoice_data():
    products = [generate_random_product() for _ in range(fake.random_int(min=1, max=5))]
    subtotal = sum(product["price"] for product in products)
    vat = round(subtotal * 0.2, 2)
    total_amount = subtotal + vat

    return {
        "id": str(fake.random_int(min=1000, max=9999)),
        "issue_date": str(fake.date_between(start_date='-30d', end_date='today')),
        "name": fake.name(),
        "street": fake.street_address(),
        "city": fake.city(),
        "state": fake.state_abbr(),
        "zipcode": fake.zipcode(),
        "country": fake.country_code(),
        "products": products,
        "vat": vat,
        "total_amount": total_amount
    }

For creating random products I use this simple random product generate function.

def generate_random_product():
    return {
        "id": str(fake.random_int(min=1000, max=9999)),
        "name": fake.bs(),
        "quantity": fake.random_int(min=1, max=10),
        "price": round(fake.random.uniform(1, 100), 2)
    }

Finally, in the main function, we generate 5 invoices with random data and save them to the invoices folder:

if __name__ == "__main__":
    os.makedirs("invoices", exist_ok=True)

    for i in range(5):
        invoice_data = create_random_invoice_data()
        invoice = create_ubl_invoice(invoice_data)
        save_invoice_to_file(invoice, f"invoices/invoice_{invoice_data['id']}.xml")

Writing Tests with unittest

Create a file named test_invoice_generator.py in the project folder. We'll write tests for our three main functions using the unittest library:

import unittest
import invoice_generator
from lxml import etree

class TestInvoiceGenerator(unittest.TestCase):

    def test_create_ubl_invoice(self):
        invoice_data = invoice_generator.create_random_invoice_data()
        invoice = invoice_generator.create_ubl_invoice(invoice_data)
        self.assertIsInstance(invoice, etree._Element)
        self.assertEqual(invoice.tag, "Invoice")
        self.assertEqual(invoice.find("ID").text, invoice_data["id"])
        self.assertEqual(invoice.find("IssueDate").text, invoice_data["issue_date"])

        # Test party name and address details
        party = invoice.find("AccountingCustomerParty")
        self.assertEqual(party.find("PartyName/Name").text, invoice_data["name"])
        address = party.find("PostalAddress")
        self.assertEqual(address.find("StreetName").text, invoice_data["street"])
        self.assertEqual(address.find("CityName").text, invoice_data["city"])
        self.assertEqual(address.find("CountrySubentity").text, invoice_data["state"])
        self.assertEqual(address.find("PostalZone").text, invoice_data["zipcode"])
        self.assertEqual(address.find("Country").text, invoice_data["country"])

        # Test invoice lines with products
        invoice_lines = invoice.findall("InvoiceLine")
        for index, product in enumerate(invoice_data["products"]):
            self.assertEqual(invoice_lines[index].find("ID").text, product["id"])
            self.assertEqual(invoice_lines[index].find("InvoicedQuantity").text, str(product["quantity"]))
            self.assertEqual(invoice_lines[index].find("LineExtensionAmount").text, str(product["price"]))

        # Test VAT and total amount
        self.assertEqual(float(invoice.find("TaxTotal").text), invoice_data["vat"])
        self.assertEqual(float(invoice.find("LegalMonetaryTotal").text), invoice_data["total_amount"])

    def test_generate_random_product(self):
        product = invoice_generator.generate_random_product()
        self.assertIn("id", product)
        self.assertIn("name", product)
        self.assertIn("quantity", product)
        self.assertIn("price", product)

if __name__ == "__main__":
    unittest.main()

In our test suite, we have created three test cases for the main functions of our invoice generator:

  1. test_create_ubl_invoice(): Tests if the create_ubl_invoice() function returns a valid XML element with the correct structure and data.

  2. test_save_invoice_to_file(): Tests if the save_invoice_to_file() function saves the invoice XML element to a file with the correct content.

  3. test_create_random_invoice_data(): Tests if the create_random_invoice_data() function generates a dictionary containing the required keys.

Run the tests using Poetry:

poetry run python -m unittest

This tutorial has demonstrated how to create a Python script that generates UBL invoice files with random data using the lxml and Faker libraries. I've also used Poetry for dependency management and packaging, and written tests using the unittest library. You can now apply these techniques to generate UBL invoices or other XML documents for your specific needs.

Did you find this article valuable?

Support Theo van der Sluijs by becoming a sponsor. Any amount is appreciated!