DEV Community

Cover image for How I Made a Free Automated PDF Invoice Maker for Batch Billing All My Contractors
Anton Tyshchenko for GDOC

Posted on

How I Made a Free Automated PDF Invoice Maker for Batch Billing All My Contractors

Why I needed this in the first place

I run a platform for distributing game graphics called Craftpix. Everyone we work with is a contractor. Incredibly creative people who, to put it mildly, have no interest in paperwork. I outsourced bookkeeping and auditing to a team in Singapore. They have their own dashboard and their accountants are meticulous about collecting everything the audit requires. But the invoices for every single transaction are on me.

In theory that means collecting invoices from each contractor. In practice it would be a nightmare. What saved me is that Hong Kong allows self-billing: I can issue invoices on behalf of my contractors for transactions that have already been completed, without any involvement on their end.

But even with all the details and Google Docs templates on hand, every invoice still needs updated dates and figures. A pointless waste of time. It makes no difference whether I do it or someone else does. It simply should not exist.

I am genuinely glad that e-invoicing is picking up in more countries. That is the future. But while we still live in a world where PDF invoices are a legal requirement, you work with what you have.

Client and contractor data: who is responsible for what

When you receive someone's personal data, you become their data controller. That means you are responsible for tracking where that data goes next.

If you send invoice details to a third-party server-side generator, you are now obligated to read through the Data Processing Agreement of that service. And nobody guarantees things will not go wrong. SaaS services leak data regularly: here is a recent example, and here is another one. Your clients' and contractors' personal data could easily end up on that list.

When we built our invoice generator, we made a clear decision from the start: user data is none of our business. Nothing goes to the server. Generation happens entirely in the browser using plain JS, and all data stays there. You can open the Network tab and verify for yourself that no personal data leaves your machine.

The generator is still a form though: one document at a time. So I wrote a small addition that takes an array of invoice data and generates all of them in one go. I keep the array saved, run it once a month, update the numbers, done. All the details stay in place.

Setup: running the generator

Open Free PDF Invoice Maker and the browser console. I use Chrome: on Mac it is Cmd + Option + J, on Windows Ctrl + Shift + J. Go to the Console tab, paste the code below and hit Enter.

(() => {
    // grab the invoice API exposed by gdoc.io on the window object
    const { store, invoice } = window.invoiceAPI;

    // expose batchInvoice globally so you can call it from the console
    window.batchInvoice = async (invoices) => {
        for (const inv of invoices) {

            // reset the form before filling in the next invoice
            invoice.new();

            // fill in all invoice fields via the store
            store.set({
                'inv.number': inv.number,
                'currency': inv.currency,
                'date.value': inv.date,
                'dueDate.value': inv.dueDate,
                'paymentTerms.value': inv.paymentTerms,
                'poNumber.value': inv.poNumber,
                'from.value': inv.from,
                'billTo.value': inv.billTo,
                'notes.value': inv.notes,
                'terms.value': inv.terms,

                // tax, discount and shipping are optional:
                // if you pass a value, the field gets enabled automatically
                'tax.enabled': inv.tax != null ? '1' : '0',
                'tax.value': inv.tax || 0,
                'discount.enabled': inv.discount != null ? '1' : '0',
                'discount.value': inv.discount || 0,
                'discount.mode': inv.discountMode || 'abs', // 'abs' for fixed amount, 'pct' for percent
                'shipping.enabled': inv.shipping != null ? '1' : '0',
                'shipping.value': inv.shipping || 0,
                'amountPaid.value': inv.amountPaid || 0,
            });

            // each line item needs a unique key β€” built from timestamp + random suffix
            inv.items.forEach(item => {
                const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
                store.set({
                    [`items.${id}.name`]: item.name,
                    [`items.${id}.qty`]: item.qty,
                    [`items.${id}.price`]: item.price,
                });
            });

            // save state, then trigger PDF download
            invoice.save();
            await invoice.pdf();
        }
        console.log(`βœ… ${invoices.length} invoices generated`);
    };
    return 'πŸŒ‹ gdoc.io Invoice Volcano activated! Feed batchInvoice([...]) and watch it erupt';
})();
Enter fullscreen mode Exit fullscreen mode

The generator is ready. Now pass it your data array and hit Enter again.

Each invoice object supports the following fields:

Field Required Description
number yes Invoice number
currency yes Currency symbol, e.g. $, €, Β£
date yes Invoice date in YYYY-MM-DD format
dueDate no Payment due date in YYYY-MM-DD format
paymentTerms no e.g. Net 30, Net 60
poNumber no Purchase order number
from yes Sender details. Use \n for line breaks
billTo yes Recipient details. Use \n for line breaks
items yes Array of line items, each with name, qty, and price
discount no Discount value. Requires discountMode
discountMode no pct for percentage, abs for fixed amount
tax no Tax rate in percent
shipping no Shipping cost
amountPaid no Amount already paid, shown as a deduction
notes no Payment instructions or any additional notes
terms no Terms and conditions
batchInvoice([
    {
        number: '001',            // invoice number
        currency: '$',            // currency symbol shown on the invoice
        date: '2026-04-11',       // invoice date (YYYY-MM-DD)
        dueDate: '2026-05-11',    // payment due date
        paymentTerms: 'Net 30',   // payment terms label
        poNumber: 'PO-2026-001',  // purchase order number (optional)
        from: 'Acme Corp\n100 Main St\nNew York, NY 10001',    // sender details, use \n for line breaks
        billTo: 'TechStart Inc\n456 Oak Ave\nBoston, MA 02101', // recipient details
        items: [
            // each item: service/product name, quantity, unit price
            { name: 'Frontend Development', qty: 40, price: 120 },
            { name: 'Backend API',          qty: 30, price: 150 },
        ],
        discount: 15,           // discount value
        discountMode: 'pct',    // 'pct' = percentage, 'abs' = fixed amount
        tax: 10,                // tax rate in percent
        amountPaid: 5000,       // amount already paid (shown as a deduction)
        notes: 'Wire: Bank of America\nAcc: 1234567890', // payment instructions or any notes
        terms: 'Late payments subject to 1.5% monthly interest', // terms and conditions
    },
    {
        number: '002',
        currency: '€',
        date: '2026-04-11',
        dueDate: '2026-06-11',
        paymentTerms: 'Net 60',
        from: 'Digital Studio GmbH\nBerlinstr. 42\nBerlin, 10115',
        billTo: 'MegaShop EU\n8 Rue de Rivoli\nParis, 75001',
        items: [
            { name: 'UI/UX Design',     qty: 20, price: 95 },
            { name: 'Brand Guidelines', qty: 1,  price: 2500 },
        ],
        discount: 400,          // fixed discount of €400
        discountMode: 'abs',
        tax: 19,
        shipping: 50,           // shipping cost (optional)
        notes: 'IBAN: DE89 3704 0044 0532 0130 00',
        terms: 'Payment within 60 days',
    },
]);
Enter fullscreen mode Exit fullscreen mode

Generation starts automatically. Depending on your OS and browser settings, files will either land straight in your downloads folder or the browser will ask for permission on each one.

Used to be an hour of repetitive work every month. Now I open the array file, update the dates and numbers, run it β€” the whole thing takes a couple of minutes.

Top comments (0)