Check Irrigation Spare-Parts Records with a Local CSV Validator

By IrriNex Store

A spare-parts spreadsheet needs clear records before it can support a useful stock check. Two rows can accidentally share an ID. A quantity can lose its unit. A printed fraction can be mistaken for a measurement. These mistakes are easier to spot with a few repeatable checks than by reading every cell after each edit.

This tutorial provides a small, complete Python tool for checking an irrigation spare-parts CSV on your own computer. Copy the template, checker and optional test file below into one folder. The checker reads the file, reports record issues and leaves the original unchanged. It has no network calls, account requirement or third-party package dependencies.

Scope and disclosure: this is an original, AI-assisted educational example prepared for a China-based irrigation retailer. All sample rows and test measurements are fictional. Passing the checks does not establish product compatibility, safe condition, correct stock counts or suitability for an installation.

What the checker looks for

  • The ten required column names and any duplicate headers
  • Rows with too many or too few cells
  • Empty item IDs, duplicate IDs and missing descriptions
  • Partly completed measurements, unsupported measurement units and nonpositive measurement values
  • Negative, missing or non-finite stock quantities; fractional counts of pieces or packs
  • Invalid calendar dates and unsupported review-status labels

The tool does not infer missing information. It reports a problem so a person can inspect the original record, packaging or stock and decide what to correct.

1. Copy the CSV template

Save the following as example_inventory.csv in a plain-text editor using UTF-8. Keep the header on one physical line. The two example rows show a bag count and a measured stock length; they are not actual inventory.

item_id,description,marking_text,measurement_value,measurement_unit,measurement_basis,stock_quantity,stock_unit,checked_on,review_status
EXAMPLE-001,Illustrative bag of fittings,16 mm,,,,2,packs,2026-10-07,unreviewed
EXAMPLE-002,Illustrative tubing offcut,,,,,3.5,m,2026-10-07,unreviewed

The fields have deliberately separate jobs:

  • item_id identifies the row. The duplicate check ignores letter case and surrounding whitespace, so EXAMPLE-001 and example-001 conflict. It does not rewrite either value.
  • description says what is being counted and can explain missing evidence. marking_text preserves a label such as 16 mm or 1/2 in as text. A blank marking is allowed when none can be read.
  • measurement_value, measurement_unit and measurement_basis are a separate observation. Leave all three blank, or supply all three. This example accepts mm or in, with outside_diameter, inside_diameter or length as the basis.
  • stock_quantity and stock_unit state the amount. Units are pieces, packs, m and ft. Zero is a valid recorded quantity. A blank quantity is flagged. Pieces and packs must be whole numbers.
  • checked_on uses YYYY-MM-DD. review_status is unreviewed, identified or quarantined. These are recordkeeping labels; “identified” is not a safety certification.

Counting two packs does not tell the script how many fittings are in them. Likewise, a 3.5-metre offcut belongs in the stock-length fields; it does not establish the tube diameter. Keep those meanings separate when replacing the fictional rows.

If you use a spreadsheet, import item_id and marking_text as text columns and inspect the exported file afterward. CSV quoting handles commas and quotation marks inside a field, but spreadsheet software may still apply its own type conversions.

2. Save the complete checker

Copy this block into check_spares.py. Preserve indentation and plain quotation marks. It uses Python’s standard CSV reader to keep parsed fields as strings, and Decimal for the numeric checks. It converts only the fields whose meaning is numeric.

"""Check the shape of a spare-parts CSV without changing the file.

This checks record consistency, not product suitability or compatibility.
Uses only the Python standard library and makes no network requests.
"""
import csv
import re
import sys
from datetime import date
from decimal import Decimal, InvalidOperation

FIELDS = (
    "item_id", "description", "marking_text", "measurement_value",
    "measurement_unit", "measurement_basis", "stock_quantity",
    "stock_unit", "checked_on", "review_status",
)


def number(value):
    try:
        result = Decimal(value)
    except (InvalidOperation, ValueError):
        return None
    return result if result.is_finite() else None


def validate(stream):
    errors = []
    reader = csv.DictReader(stream, strict=True)
    headers = reader.fieldnames or []
    if len(headers) != len(set(headers)):
        return ["header: duplicate column names"]
    missing = set(FIELDS) - set(headers)
    if missing:
        return ["header: missing " + ", ".join(sorted(missing))]
    seen = set()
    rows_read = 0
    for row in reader:
        rows_read += 1
        where = f"line {reader.line_num}"
        if None in row or any(value is None for value in row.values()):
            errors.append(f"{where}: cell count does not match the header")
            continue
        # Whitespace is ignored for checks; original values are never rewritten.
        values = {key: row[key].strip() for key in FIELDS}
        item_id = values["item_id"]
        if not item_id:
            errors.append(f"{where}: item_id is empty")
        elif item_id.casefold() in seen:
            errors.append(f"{where}: duplicate item_id {item_id!r}")
        else:
            seen.add(item_id.casefold())
        if not values["description"]:
            errors.append(f"{where}: description is empty")
        measurement = [values[key] for key in (
            "measurement_value", "measurement_unit", "measurement_basis")]
        if any(measurement):
            if not all(measurement):
                errors.append(f"{where}: measurement needs value, unit and basis")
            else:
                measured = number(measurement[0])
                if measured is None or measured <= 0:
                    errors.append(f"{where}: measurement_value must be finite and positive")
                if measurement[1] not in {"mm", "in"}:
                    errors.append(f"{where}: measurement_unit must be mm or in")
                if measurement[2] not in {"outside_diameter", "inside_diameter", "length"}:
                    errors.append(f"{where}: unsupported measurement_basis")
        quantity = number(values["stock_quantity"])
        unit = values["stock_unit"]
        if quantity is None or quantity < 0:
            errors.append(f"{where}: stock_quantity must be finite and nonnegative")
        elif unit in {"pieces", "packs"} and quantity != quantity.to_integral_value():
            errors.append(f"{where}: pieces and packs require whole quantities")
        if unit not in {"pieces", "packs", "m", "ft"}:
            errors.append(f"{where}: stock_unit must be pieces, packs, m or ft")
        checked = values["checked_on"]
        try:
            if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", checked):
                raise ValueError
            date.fromisoformat(checked)
        except ValueError:
            errors.append(f"{where}: checked_on must be a real YYYY-MM-DD date")
        if values["review_status"] not in {"unreviewed", "identified", "quarantined"}:
            errors.append(f"{where}: unsupported review_status")
    if not rows_read:
        errors.append("file: no inventory rows")
    return errors


def main():
    if len(sys.argv) != 2:
        print("Usage: python check_spares.py your_inventory.csv", file=sys.stderr)
        return 2
    try:
        with open(sys.argv[1], encoding="utf-8-sig", newline="") as source:
            errors = validate(source)
    except (OSError, UnicodeError, csv.Error) as exc:
        print(f"Cannot read CSV: {exc}", file=sys.stderr)
        return 2
    for error in errors:
        print(error)
    if errors:
        print(f"{len(errors)} record issue(s). The file was not changed.")
        return 1
    print("Record checks passed. This is not a compatibility or safety approval.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

3. Run it against a copy of your register

The code and nine tests below were run with Python 3.12.14 on 7 October 2026. From a terminal in the folder containing the files, run:

python check_spares.py example_inventory.csv

If your Python 3 command is python3, use that command name instead. The supplied example produces:

Record checks passed. This is not a compatibility or safety approval.

To see a failure without changing an operational register, duplicate the first example row at the end of a separate test copy. Running the checker on that copy reports the repeated ID, including the row’s line number. Correct the record only after checking which item the row was meant to describe.

Exit status 0 means the implemented record checks passed. Status 1 means record issues were found. Status 2 means the file could not be read, its CSV syntax could not be parsed, or the command was used incorrectly. The printed line number is the physical line where a CSV record ends, which matters if a quoted description spans several lines.

4. Reproduce the nine tests

Save the next block as test_checks.py in the same folder. The tests exercise printed fractions, quoted text, incomplete measurements, separate diameter and stock-length fields, duplicate IDs, dates, quantities, supported units, duplicate headers, extra cells, an empty template and broken quoting. They are small example-based software checks, not a full audit or a field trial.

import csv
import io
import unittest
from check_spares import FIELDS, validate


def sample(**changes):
    row = dict.fromkeys(FIELDS, "")
    row.update(item_id="EXAMPLE-001", description="Illustrative fitting",
               marking_text="1/2 in", stock_quantity="2", stock_unit="pieces",
               checked_on="2026-10-07", review_status="unreviewed")
    row.update(changes)
    return row


def run_rows(rows):
    data = io.StringIO()
    writer = csv.DictWriter(data, fieldnames=FIELDS)
    writer.writeheader()
    writer.writerows(rows)
    data.seek(0)
    return validate(data)


class Checks(unittest.TestCase):
    def test_printed_size_is_not_treated_as_measurement(self):
        self.assertEqual(run_rows([sample()]), [])

    def test_quoted_comma_and_fraction_survive(self):
        self.assertEqual(run_rows([sample(marking_text='1/2 in, part "A"')]), [])

    def test_measurement_without_basis_is_flagged(self):
        self.assertTrue(any("needs value" in e for e in run_rows([
            sample(measurement_value="16", measurement_unit="mm")])))

    def test_measured_diameter_and_stock_length_are_separate(self):
        self.assertEqual(run_rows([sample(measurement_value="16.2",
            measurement_unit="mm", measurement_basis="outside_diameter",
            stock_quantity="3.5", stock_unit="m")]), [])

    def test_case_and_whitespace_duplicate_ids(self):
        self.assertTrue(any("duplicate item_id" in e for e in run_rows([
            sample(), sample(item_id=" example-001 ")])))

    def test_invalid_dates_and_nonfinite_quantities(self):
        for value in ["NaN", "Infinity", "-1", "abc"]:
            self.assertTrue(run_rows([sample(stock_quantity=value)]))
        for value in ["2026-02-30", "20261007", "07/10/2026", ""]:
            self.assertTrue(run_rows([sample(checked_on=value)]))

    def test_unit_and_fractional_pack_checks(self):
        self.assertTrue(run_rows([sample(stock_quantity="1.5", stock_unit="packs")]))
        self.assertTrue(run_rows([sample(stock_unit="rolls")]))

    def test_duplicate_headers_and_extra_cells(self):
        self.assertTrue(validate(io.StringIO("item_id,item_id\na,b\n")))
        self.assertTrue(validate(io.StringIO(",".join(FIELDS) + "\n" + ",".join(["x"] * 11))))

    def test_empty_template_and_broken_quoting(self):
        self.assertTrue(validate(io.StringIO(",".join(FIELDS) + "\n")))
        with self.assertRaises(csv.Error):
            validate(io.StringIO(",".join(FIELDS) + '\n"unclosed'))


if __name__ == "__main__":
    unittest.main()

Run the tests with:

python -m unittest -v test_checks

The verification run completed all nine test methods successfully. The test runner prints Ran 9 tests followed by OK; elapsed time will vary. The Python unittest documentation explains the runner and its output.

Keep the limits visible beside the result

A valid date is not proof that an inspection happened. This script does not reject a future date or decide when another inspection is due. It allows additional named columns but checks only the ten required fields. Blank lines are skipped by the CSV parser. It expects comma-separated UTF-8 data, with an optional UTF-8 byte-order mark, and does not automatically detect other delimiter formats.

The checker does not total mixed units, convert measurements, calculate reorder quantities, count stock or identify a thread standard. It also does not evaluate formula-like text or sanitize a file for spreadsheet use. Review externally supplied data before opening it in a spreadsheet.

Keep packaging, supplier specifications and the physical stock available for a person to resolve flagged records. For documenting an installed joint and its two mating components, use the separate irrigation connection record. This CSV tool addresses the consistency of spare-parts data and does not replace equipment instructions or that installation record.

Publisher and sources

Prepared with AI assistance for IrriNex Store, operated by Wuhan Zhengchuang Vision Technology Co., Ltd. The publisher sells irrigation supplies, and the linked store contains commercial product information. This example can be used without buying anything. The Python documentation linked above explains the software interfaces; it does not endorse the retailer or certify this tool.

Comments

Popular posts from this blog

Check Seed-Row Moisture Before Changing Drip Irrigation

An irrigation connection record that makes repairs easier