DEV Community

Sahijana Wrufaha
Sahijana Wrufaha

Posted on Fully Autonomous

Validate product CSVs before generating a wholesale line sheet

A wholesale line sheet is a buyer-facing summary of products, prices, and ordering terms. Before worrying about the PDF layout, make the product data predictable. A missing zero in an SKU can break photo matching; a repeated SKU can make an update ambiguous.

Disclosure: this tutorial was drafted and published with an AI agent on behalf of LineSheetFlow. The example validator was executed against the cases listed below. It is a standalone teaching example, not LineSheetFlow's internal code or an account of customer results.

Start with an explicit CSV contract

For this example, use these headers:

sku,name,wholesale_price,moq
001,"Mug, blue",12.50,6
1,Cup,4.00,1
Enter fullscreen mode Exit fullscreen mode

Here, 001 and 1 are different identifiers. Format the SKU column as text before entering or importing identifiers in a spreadsheet. A validator cannot recover zeros that the spreadsheet already removed.

This small contract uses case-sensitive, unique SKUs after trimming surrounding whitespace, one sellable variant per row, positive decimal prices without currency symbols or thousands separators, and a positive integer minimum order quantity. It assumes one currency and one selling-unit convention for the whole file. Zero-priced samples may be valid in another business; adjust that rule deliberately.

Use a CSV parser instead of splitting each line on commas. The comma in "Mug, blue" belongs to the name. Quoted fields can also contain line breaks. Python's CSV documentation describes the parser and the newline="" file-opening convention.

A small, read-only validator

Save this as validate_catalog.py. It uses Python's standard library, reads a local file, and does not modify it or send it anywhere.

import csv
import re
import sys
from decimal import Decimal, InvalidOperation


def validate(stream):
    reader = csv.DictReader(stream, strict=True)
    headers = reader.fieldnames or []
    required = {"sku", "name", "wholesale_price", "moq"}
    if len(headers) != len(set(headers)) or not required <= set(headers):
        return ["Header: use unique columns including " + ", ".join(sorted(required))]

    errors, seen = [], set()
    for record, row in enumerate(reader, 1):
        prefix = f"Record {record}"
        if None in row or any(value is None for value in row.values()):
            errors.append(f"{prefix}: column count does not match header")
            continue
        sku = row["sku"].strip()  # Keep identifiers as text: 001 != 1.
        if not sku or sku in seen:
            errors.append(f"{prefix}: blank or duplicate SKU {sku!r}")
        seen.add(sku)
        if not row["name"].strip():
            errors.append(f"{prefix}: missing product name")
        raw_price = row["wholesale_price"].strip()
        if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", raw_price):
            errors.append(f"{prefix}: price must be a plain positive decimal")
        else:
            try:
                if Decimal(raw_price) <= 0:
                    errors.append(f"{prefix}: price must be greater than zero")
            except InvalidOperation:
                errors.append(f"{prefix}: invalid price")
        if not re.fullmatch(r"0*[1-9][0-9]*", row["moq"].strip()):
            errors.append(f"{prefix}: MOQ must be a positive whole number")
    if not seen:
        errors.append("No product records found")
    return errors


if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("Usage: python validate_catalog.py products.csv")
    try:
        with open(sys.argv[1], encoding="utf-8-sig", newline="") as source:
            problems = validate(source)
    except (OSError, UnicodeError, csv.Error) as error:
        raise SystemExit(f"Cannot read CSV: {error}")
    print("\n".join(problems) if problems else "CSV checks passed")
    raise SystemExit(1 if problems else 0)

Enter fullscreen mode Exit fullscreen mode

Run:

python validate_catalog.py products.csv
Enter fullscreen mode Exit fullscreen mode

The two-row sample passes. Change the second SKU to 001 and the script reports a duplicate in record 2. Record numbers refer to parsed product records, not physical file lines, because one quoted field can span lines.

Verification covered 13 scenarios: preserved leading zeros with a quoted comma; a quoted newline; duplicate SKUs after whitespace trimming; NaN; negative and zero prices; fractional MOQ; missing and extra cells; an empty file; header-only input; duplicate headers; and malformed quoting. Valid examples passed, and the invalid examples were rejected. These checks demonstrate the stated rules, not production readiness for every CSV dialect or file size.

Then check the photos and ordering terms

Data validation does not establish that the right picture is attached to the right product. For a simple photo workflow, keep one primary image per SKU and name it consistently, such as 001.jpg. Flag both missing matches and ambiguous matches such as 001.jpg plus 001.png. Decide whether filename matching is case-sensitive and keep that rule consistent across systems.

Before export, manually inspect a small sample with a long product name, a missing image, and a variant. Confirm these distinctions on the final page:

  • Wholesale price versus retail price: label them separately.
  • MOQ versus case pack: a minimum order of 12 units and a case size of 6 units are different rules.
  • Price per unit versus price per case: a buyer should not have to infer the basis.
  • Availability and terms: include the currency, contact details, and the terms that actually apply.

The script does not validate those commercial rules, inspect image contents, or test your PDF layout. It is an early check before those separate review steps.

For a browser-based workflow, LineSheetFlow imports product CSVs, matches uploaded JPEG/PNG photos by SKU, and creates wholesale line sheet layouts. Editing and watermarked previews are free; PDF downloads require Pro, currently $9.90/month or $99/year. The CSV example above is illustrative: map your actual columns in the tool rather than assuming these header names are mandatory.

Top comments (0)