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:
create_ubl_invoice(invoice_data)
: Generates an invoice XML element with the given data.save_invoice_to_file(invoice, file_path)
: Saves the invoice XML element to a file.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:
test_create_ubl_invoice()
: Tests if thecreate_ubl_invoice()
function returns a valid XML element with the correct structure and data.test_save_invoice_to_file()
: Tests if thesave_invoice_to_file()
function saves the invoice XML element to a file with the correct content.test_create_random_invoice_data()
: Tests if thecreate_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.