Python code: convert amounts to words (for invoices and receipts)

Python code: convert amounts to words (for invoices and receipts)
An invoice, a receipt or a promissory note carries the same amount twice: in figures and in words. When the two versions disagree, the document goes back, the payment waits and somebody has to explain the gap. That is why the words line gets read twice, and why it should not be typed by a tired person at the end of a long day.
This program writes that line for you. It reads a list of amounts from a text file, turns each one into words with its cents and saves the result to a second file, ready to be copied straight into the document. It works the same for a small business owner who still invoices by hand, for the bookkeeper who prepares receipts in batches and for the warehouse clerk who hands over goods with a signed slip.
⬇ Download the code (ZIP)The ZIP holds the commented program, the sample data and the exact output you should get when you run it. It runs on Python 3.11 or newer and asks you to install nothing else: everything it uses ships with the language.
What is inside the download
| File | What it is for |
|---|---|
| numero_a_letras.py | The program, commented line by line |
| datos/importes.csv | The ten sample amounts, each one with its currency |
| salida_ejemplo.txt | The output you should get when you run it |
| README.md | The guide of the package, with the steps and the usage notice |
What the program does
The program always follows the same steps and does not need you to explain anything about your business:
- It reads the file
datos/importes.csv, which carries four columns: the document, the amount, the currency in singular and the currency in plural. - It splits each amount into a whole part and cents. The whole part is converted with the tables of the language; the cents stay as the fraction over one hundred that gets printed on paper.
- It walks the number in groups of three digits and builds the full text: units, tens, hundreds, thousands and millions.
- It applies the short form to numbers ending in one, so the reading comes out the way people say it: "one peso" instead of a longer form, and "twenty-one thousand" with the hyphen the language asks for.
- It puts the currency in singular for a single unit and in plural for several, and finishes with the cents.
- It prints the table on screen and saves the output CSV so you can paste it wherever you need it.
Nothing here depends on the system date or on an internet service: the same input file always produces the same output, and that is exactly what you want when a document is reviewed months later and somebody has to know how it was produced.
How to run it
Install Python 3.11 or newer from the official site if you do not have it yet. Then unzip the package into any folder and open the terminal right there: the files land loose in that same folder, and there is no subfolder to create or remember.
python numero_a_letras.py
There are no packages to install, no virtual environment to build and no system variables to touch. The program uses the standard library only: file reading, decimal numbers and paths. It runs the same on Windows, Linux and Mac because the paths are built from the folder of the program itself and not from an address written by hand.
If the terminal answers that it does not recognise the command, on Windows try py instead of python, and check that Python was added to the PATH during the installation. That is the number one stumble for everybody, and it is fixed once.
The code, explained
The whole program runs to about one hundred and fifty lines and reads from top to bottom. These are the parts that matter, copied exactly as they travel inside the English ZIP.
# ==========================================================================
# AMOUNTS IN WORDS (FOR INVOICES AND RECEIPTS)
# Didactic Python code for accounting · Kardex Tauro · kardex-tauro.muisca.co
# What it does: reads the amounts in datos/importes.csv, writes each one in words with
# its cents and saves it to salida/importes_en_letras.csv
# Tested with Python 3.11. Standard library only: nothing to install.
The header says what the file does, which version it was tested with and what it writes. The base folder is computed from the location of the program itself, and the input path and the output path come from there. Move the whole folder somewhere else and the program still finds its files, with no hard-coded path that breaks when you change computer or lift the package onto a server.
UNIDADES = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
ESPECIALES = {10: "ten", 11: "eleven", 12: "twelve", 13: "thirteen", 14: "fourteen", 15: "fifteen", 16: "sixteen", 17: "seventeen", 18: "eighteen", 19: "nineteen", 20: "twenty", 21: "twenty-one", 22: "twenty-two", 23: "twenty-three", 24: "twenty-four", 25: "twenty-five", 26: "twenty-six", 27: "twenty-seven", 28: "twenty-eight", 29: "twenty-nine"}
DECENAS = {30: "thirty", 40: "forty", 50: "fifty", 60: "sixty", 70: "seventy", 80: "eighty", 90: "ninety"}
CENTENAS = {1: "one hundred", 2: "two hundred", 3: "three hundred", 4: "four hundred", 5: "five hundred", 6: "six hundred", 7: "seven hundred", 8: "eight hundred", 9: "nine hundred"}
ESCALAS = {1: {"uno": "one thousand", "muchos": "thousand"}, 2: {"uno": "one million", "muchos": "millions"}}
CIEN = "one hundred"
CERO = "zero"
UNIR_DECENA = "-"
UNIR_CENTENA = " and "
APOCOPE = {1: "one", 21: "twenty-one"}
ETIQUETA = "AMOUNT IN WORDS"
UNIR_CENTAVOS = "AND"
Here is the answer to the question everybody asks: where do the words come from? From tables. The units, the exact words from ten to twenty-nine, the tens, the hundreds and the names of the scales all live in separate structures, and the algorithm that walks them is the same in all three languages. The English word tables in this program are not the tables used by the Spanish program or by the Portuguese one: each language brings its own, and that is why the same engine serves three languages without duplicating the logic. The separators and the label of the line live here too, so changing how the sentence is printed means changing one word in this part and nothing else.
| Language | Tens are joined with | Needs the short form |
|---|---|---|
| Spanish | the word "y" | Yes: it says un peso and veintiún mil |
| English | a hyphen | No: its words are already built that way |
| Portuguese | the word "e" | Yes: it says um real and vinte e um |
The table above sums the point up: the engine is identical, the words change. If you ever need another language, you add its tables and you do not touch a single line of the arithmetic.
def hasta_999(numero: int) -> str:
"""Writes in words from zero to nine hundred and ninety-nine."""
if numero == 0:
return ""
if numero < 10:
return UNIDADES[numero]
if numero <= 29:
return ESPECIALES[numero]
if numero < 100:
decena, unidad = (numero // 10) * 10, numero % 10
return DECENAS[decena] + (UNIR_DECENA + UNIDADES[unidad] if unidad else "")
if numero == 100:
return CIEN
centena, resto = numero // 100, numero % 100
return CENTENAS[centena] + (UNIR_CENTENA + hasta_999(resto) if resto else "")
This is the function that converts from zero to nine hundred and ninety-nine. It solves three cases: the single unit, the stretch from ten to twenty-nine -which in English are words of their own and cannot be assembled from parts- and the rest, which is built from the tens word, the joiner and the unit. A round one hundred is what it is, and zero returns empty text because the function that builds the whole sentence decides whether that text is needed at all.
def apocopar(texto: str, numero: int) -> str:
"""Applies the short form of a number ending in one (kept for the languages that need it)."""
if numero % 10 != 1 or numero % 100 == 11:
return texto
corta = APOCOPE.get(numero % 100, APOCOPE.get(1))
partes = texto.split(" ")
partes[-1] = corta
return " ".join(partes)
This function takes care of the details that give away a program written in a hurry. When a number ends in one, the last word is shortened or swapped for its short form: that is why the result says "one peso" and not "uno peso", and "twenty-one thousand" instead of a clumsier reading. The program does not guess from the text: it looks at the number and decides.
def entero_a_letras(numero: int) -> str:
"""Walks the number in groups of three digits and builds the whole text."""
if numero == 0:
return CERO
if numero > 999999999:
raise ValueError("This program goes up to nine hundred and ninety-nine million")
partes = []
escala = 0
resto = numero
while resto > 0:
grupo = resto % 1000
resto //= 1000
if grupo == 0:
escala += 1
continue
if escala == 0:
texto = hasta_999(grupo)
elif grupo == 1:
texto = ESCALAS[escala]["uno"]
else:
texto = apocopar(hasta_999(grupo), grupo) + " " + ESCALAS[escala]["muchos"]
partes.insert(0, texto)
escala += 1
return " ".join(partes)
The heart of the matter. It walks the number from right to left in groups of three digits, decides whether each group carries the name of a scale -thousand, millions- and keeps the pieces in order so it can return them as a single sentence. The limit of the program lives here too: past nine hundred and ninety-nine million it raises an error instead of writing nonsense. A program that warns you when it cannot do the job is worth more than one that writes it wrong and nobody notices.
def convertir(valor: Decimal, moneda: str, moneda_plural: str) -> str:
"""Converts an amount with cents: whole part, currency and cents over one hundred."""
entero = int(valor)
centavos = int((valor - entero).quantize(CENTAVO, rounding=ROUND_HALF_UP) * 100)
palabra = moneda if entero == 1 else moneda_plural
# # The short form also applies before the currency: one peso, thirty-one pesos
letras = apocopar(entero_a_letras(entero), entero) + " " + palabra + " " + UNIR_CENTAVOS
return f"{letras} {centavos:02d}/100".upper()
This is the function that looks at the document the way an accountant does: it separates the whole part from the cents, picks the currency in singular or plural, builds the sentence and upper-cases it, which is how this line is printed. The cents are computed with decimal numbers and commercial rounding, so an amount like 1,234,567.89 never loses a cent to the binary arithmetic of computers.
What you will see on screen
When you run it, this is what appears. The first column is the document, the second one the amount and the third one the line you are going to paste:
AMOUNTS IN WORDS Didactic Python code · Kardex Tauro · kardex-tauro.muisca.co Document Amount In words ------------------------------------------------------------------------------------------------ RECEIPT 001 0.75 AMOUNT IN WORDS: ZERO PESOS AND 75/100 RECEIPT 002 1.00 AMOUNT IN WORDS: ONE PESO AND 00/100 INVOICE 001 15.50 AMOUNT IN WORDS: FIFTEEN PESOS AND 50/100 INVOICE 002 21.00 AMOUNT IN WORDS: TWENTY-ONE PESOS AND 00/100 INVOICE 003 100.00 AMOUNT IN WORDS: ONE HUNDRED PESOS AND 00/100 INVOICE 004 101.40 AMOUNT IN WORDS: ONE HUNDRED AND ONE PESOS AND 40/100 INVOICE 005 1,000.00 AMOUNT IN WORDS: ONE THOUSAND PESOS AND 00/100 INVOICE 006 1,001.00 AMOUNT IN WORDS: ONE THOUSAND ONE PESOS AND 00/100 INVOICE 007 21,000.00 AMOUNT IN WORDS: TWENTY-ONE THOUSAND PESOS AND 00/100 INVOICE 008 1,234,567.89 AMOUNT IN WORDS: ONE MILLION TWO HUNDRED AND THIRTY-FOUR THOUSAND FIVE HUNDRED AND SIXTY-SEVEN PESOS AND 89/100 ------------------------------------------------------------------------------------------------ Documents converted: 10 File written: salida/importes_en_letras.csv
The line you will read most often is the last one: an amount of 1,234,567.89 comes out as ONE MILLION TWO HUNDRED AND THIRTY-FOUR THOUSAND FIVE HUNDRED AND SIXTY-SEVEN PESOS AND 89/100. Notice three details that are worth the download. The program writes "ONE THOUSAND" and not a looser reading. It writes "TWENTY-ONE THOUSAND" with the hyphen the language asks for. And it closes with the cents over one hundred, exactly the way they are written on paper.
At the end the table tells you how many documents it converted and where the result was saved. Open that file with Excel, with LibreOffice or with any text editor, and copy the words column straight into the document you are preparing.
Common mistakes and tips
- Leave only the number in the amount column: if you paste the currency symbol or the thousands separators, the program stops with a conversion error. That is on purpose, so it never converts a badly written figure.
- The currency is changed in the file, not in the code: the currency and plural columns decide whether the text says "peso" or "pesos".
- The cents are computed with decimals and commercial rounding. If your document demands another rule, you change it in one line and run the program again.
- The ceiling is nine hundred and ninety-nine million. Above that the program warns you with an error instead of inventing words.
- Keep the output file next to the document. When somebody reviews the invoice six months later, you will want the same list the program produced.
- Try the sample data first and compare it against the expected output. If it matches, you know your Python installation is fine and that any problem later sits in your own data.
When this is not enough
A console program solves the conversion and nothing else. It does not carry your stock, it does not issue the invoice, it does not tell you which item is about to run out and it does not record who authorised each issue. If your operation still fits in a spreadsheet and a couple of scripts, this ZIP is what you need and it will serve you for years.
When the business grows, the words line stops being a problem and becomes one detail of a bigger one: invoicing, taking the goods off the stock ledger, controlling purchases and knowing what actually sold. That is where Kardex Tauro makes sense: it is the program that keeps the inventory and the documents of the business in order, and this free code is the previous step, the one you use while the volume still allows it.
⬇ Download the code (ZIP)Download the ZIP, run it with your own amounts and compare the first output against the one you typed by hand. If it works for you, hand the file to your bookkeeper so they can check two or three lines: that is the best way to confirm the conversion is right before it reaches real documents.