Automation · International trade

Import document checker

Before a shipment is released, someone has to check that the invoice, packing list, shipping list and certificates of analysis tell the same story. I wrote a Python script that does this check and delivers a ready report. Here the same check runs in your browser.

JavaScript · SheetJS · pdf.jsPython · pandasTesseract OCRFictional data

Try it now

I put together a fictional shipment with the five documents a supplier sends. There are a few errors hidden in them. Click check and see what the checker finds.

Download the kit (.zip)

Demo rules: at least 18 months of shelf life at arrival (invoice ETA), manufactured up to 6 months before the invoice, and a fictional authorization for SKU-2214, valid since 2025-06-01.

Why it matters

International trade is a chain of deadlines. A quantity that does not match, a repeated batch or a short expiry found too late turns into cargo stuck at the terminal, storage costs and a risk of running out of product at the end of the line.

The manual check cross-references four or five documents line by line. It is slow, tiring and depends on who is checking that day.

How it works

Checker flow The shipment documents come in, are read as text or with OCR, go through 12 checks and produce an Excel report with failures, warnings and an email draft. Documents Invoice (Excel) Packing list (Excel) Shipping list (Excel) Certificates of analysis (PDF) Signed invoice and packing list (PDF) Reading Excel: pandas Digital PDF: text directly (PyMuPDF) Scanned PDF: OpenCV cleans the image and Tesseract reads it Cache per page 12 checks Header and tax ID Quantities Values and weights Cartons Batches and serials Expiry Certificates Import approval Report Excel with Fail, Warning and OK Support tabs Email draft to the supplier
From document to report. An unreadable certificate does not stop the run: the problem page becomes a warning.

The version on this page follows the same path in JavaScript. SheetJS reads the Excel files, pdf.js reads the text in the PDFs and the 12 checks run in your browser. Only OCR for scanned PDFs was left out, because it would make the page too heavy.

Three result levels

The 12 checks

CodeWhat it checks
AConsistent header across documents: exporter, invoice and order numbers, bill to, ship to, contact.
A2Importer's tax ID (CNPJ) present, with a valid check digit and the same on every document.
BQuantity per SKU: invoice vs. shipping list.
CQuantity per family: shipping list vs. packing list, and invoice grand total vs. packing list.
DInvoice values: quantity x unit price = line amount.
EPacking list weights: sum of lines vs. declared totals.
FCartons: shipping list vs. packing list, including by carton size.
GBatches: quantity equal to the number of serials, no duplicated serial, no batch under two product codes.
HMinimum shelf life on arrival (18 months by default) and recent manufacturing.
ICertificate of analysis batch by batch: batch present, code, serials and expiry matching the shipping list.
JFinal signed documents (PDF) vs. spreadsheets: invoice number, quantities, cartons and weights.
KRegulatory import approval: a batch made before the product's approval date is blocked.

Pieces of the logic

Simplified versions of two parts of the script, written for this page.

Reading a page: direct text or OCR

def page_text(page):
    text = page.get_text()
    if len(text.strip()) > 50:          # digital PDF: the text is already there
        return text
    pix = page.get_pixmap(dpi=300)        # scanned PDF: turn it into an image
    img = np.frombuffer(pix.samples, np.uint8).reshape(pix.h, pix.w, pix.n)
    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return pytesseract.image_to_string(bw)

Minimum shelf life and recent manufacturing

def check_expiry(batch, arrival_date, min_months=18, max_mfg_months=6):
    remaining = months_between(arrival_date, batch.expiry)
    if remaining < min_months:
        return "FAIL", f"Batch {batch.number} arrives with {remaining} months (minimum {min_months})"
    if months_between(batch.manufactured, arrival_date) > max_mfg_months:
        return "WARNING", f"Batch {batch.number} made more than {max_mfg_months} months ago"
    return "OK", f"Batch {batch.number} within limits"

What I learned

About the code: the original script works with real supplier documents and is not public. The version on this page was rewritten in JavaScript and the sample kit was built from scratch: companies, products, lots and numbers are made up.