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.
- XLSInvoice6 items, prices and totalsDownload
- XLSPacking listPieces, cartons and weights per familyDownload
- XLSShipping listCarton by carton: lots, serials and expiry datesDownload
- PDFCertificate of analysisQuality release lot by lotViewDownload
- PDFSigned invoice and packing listThe final documents, signed and stampedViewDownload
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
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
- FAIL a mismatch that must go back to the supplier before approval.
- WARNING something to check by eye, for example a doubtful OCR reading.
- OK the check passed.
The 12 checks
| Code | What it checks |
|---|---|
| A | Consistent header across documents: exporter, invoice and order numbers, bill to, ship to, contact. |
| A2 | Importer's tax ID (CNPJ) present, with a valid check digit and the same on every document. |
| B | Quantity per SKU: invoice vs. shipping list. |
| C | Quantity per family: shipping list vs. packing list, and invoice grand total vs. packing list. |
| D | Invoice values: quantity x unit price = line amount. |
| E | Packing list weights: sum of lines vs. declared totals. |
| F | Cartons: shipping list vs. packing list, including by carton size. |
| G | Batches: quantity equal to the number of serials, no duplicated serial, no batch under two product codes. |
| H | Minimum shelf life on arrival (18 months by default) and recent manufacturing. |
| I | Certificate of analysis batch by batch: batch present, code, serials and expiry matching the shipping list. |
| J | Final signed documents (PDF) vs. spreadsheets: invoice number, quantities, cartons and weights. |
| K | Regulatory 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
- OCR fails in predictable ways. The letter O becomes zero, I becomes 1, a colored stamp becomes noise. That is why a doubtful reading becomes a WARNING, not a FAIL.
- A cache per page makes a difference. Running again after fixing a document takes seconds, because only new pages go through OCR.
- The invoice date is not always the shipping date. The real departure date is on the transport document, and that changes the shelf life math.
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.