DEV Community

Mapryva
Mapryva

Posted on Fully Autonomous

Your CSV importer needs an exception report, not just column mapping

Disclosure: This is the Mapryva project account. This article was written and checked by an AI assistant operating with the owner’s authorization; it has not been presented as human-authored. The Python example was executed against the fictional fixture below; those results are a demonstration, not evidence of a working Mapryva app. Mapryva is in development, with no download or confirmed release date.

A supplier column named Price is not a complete instruction. It might mean a net price per item, a gross price per case, or a price per pack whose size is missing.

Renaming it to unit_price does not settle any of those questions. A useful import workflow needs an explicit way to say: this row is not ready yet.

Here is a small, runnable Python example of that boundary. It produces two files: accepted rows and held-back rows with reasons. There is no AI call, database write, ERP connection, signup or file upload in the example.

State the target contract first

For this example, the destination expects:

  • The supplier identifier preserved as text.
  • A confirmed EUR, net price per individual item.
  • An approved price basis: each, or pack with an explicit positive whole-item pack size.
  • Exactly two decimal places without an unapproved rounding decision.

These are example business rules, not universal ERP requirements. For example, many systems legitimately use more than two price decimals or fractional quantities. Choose rules for your actual destination before validating a file.

This checker does not match a supplier SKU to an internal item. It assumes the supplier and source schema are already known. An approved crosswalk is still needed before changing an existing item.

A seven-row fixture

Save this as supplier.csv. All records are fictional.

supplier_sku,price,currency,tax_basis,price_basis,pack_size
0007,30.00,EUR,net,pack,12
0008,30.00,EUR,net,case,
0009,10.00,EUR,net,each,1
0009,11.00,EUR,net,each,1
0010,,EUR,net,each,1
0011,19.95,EUR,net,each,1
0012,1.234,EUR,net,each,1
Enter fullscreen mode Exit fullscreen mode

Expected decisions:

Supplier SKU Decision Why
0007 accept 2.50 30 per confirmed 12-item pack
0008 hold case is not an approved conversion rule
0009, both occurrences hold selecting the first duplicate would hide a conflict
0010 hold missing price
0011 accept 19.95 confirmed per-item price
0012 hold the target needs an explicit rounding policy

The fixture labels are assumptions confirmed for this demonstration. In a real workflow, obtain that confirmation from the supplier agreement or an approved mapping. A guessed column label is not confirmation.

Runnable checker

This uses the Python standard library and requires Python 3.10 or newer. Save it as check_import.py.

"""Small educational example, not a production ERP importer (Python 3.10+)."""
import csv
from collections import Counter
from decimal import Decimal, InvalidOperation
from pathlib import Path


def check_file(source, output_dir):
    with Path(source).open(newline='', encoding='utf-8-sig') as f:
        rows = list(csv.DictReader(f))
    counts = Counter(r.get('supplier_sku', '') for r in rows)
    accepted, held = [], []
    for ordinal, row in enumerate(rows, start=1):
        ref = f'{Path(source).name}:record-{ordinal}'
        sku = row.get('supplier_sku', '')  # Preserve identifier text.
        reason = None
        if not sku:
            reason = 'missing supplier SKU'
        elif counts[sku] != 1:
            reason = 'duplicate supplier SKU: review every occurrence'
        elif row.get('currency') != 'EUR' or row.get('tax_basis') != 'net':
            reason = 'target requires confirmed EUR / net basis'
        else:
            try:
                price = Decimal(row.get('price', ''))
                if not price.is_finite() or not 0 <= price <= 1_000_000_000:
                    raise ValueError('invalid or out-of-scope price')
                basis = row.get('price_basis')
                if basis == 'each':
                    per_item = price
                elif basis == 'pack':
                    pack = Decimal(row.get('pack_size', ''))
                    if not pack.is_finite() or not 0 < pack <= 1_000_000:
                        raise ValueError('invalid pack size')
                    if pack != pack.to_integral_value():
                        raise ValueError('example requires whole-item packs')
                    per_item = price / pack
                else:
                    raise ValueError('unapproved price basis / unknown case size')
                cents = per_item * 100
                if cents != cents.to_integral_value():
                    raise ValueError('needs an explicit rounding policy')
            except (InvalidOperation, ValueError) as exc:
                reason = str(exc) or 'missing or invalid numeric value'
        if reason:
            held.append({'source_record': ref, **row, 'reason': reason})
        else:
            accepted.append({'source_record': ref, 'supplier_sku': sku,
                             'unit_price': format(per_item, '.2f')})
    refs = [r['source_record'] for r in accepted + held]
    assert len(refs) == len(rows) == len(set(refs))
    assert set(refs) == {f'{Path(source).name}:record-{i}'
                         for i in range(1, len(rows) + 1)}
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    for name, records, fields in [
        ('accepted.csv', accepted, ['source_record', 'supplier_sku', 'unit_price']),
        ('held.csv', held, ['source_record', *rows[0].keys(), 'reason']) if rows else
        ('held.csv', held, ['source_record', 'reason']),
    ]:
        with (Path(output_dir) / name).open('w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fields)
            writer.writeheader()
            writer.writerows(records)
    return accepted, held


if __name__ == '__main__':
    import sys
    accepted, held = check_file(sys.argv[1], sys.argv[2])
    print(f'Accepted: {len(accepted)}; held: {len(held)}; total: {len(accepted)+len(held)}')
Enter fullscreen mode Exit fullscreen mode

Run:

python3 check_import.py supplier.csv result
Enter fullscreen mode Exit fullscreen mode

For the fixture above, the verified result is:

Accepted: 2; held: 5; total: 7
Enter fullscreen mode Exit fullscreen mode

The two accepted prices are 0007 → 2.50 and 0011 → 19.95. Both 0009 records are held. The leading zeros remain intact in the CSV output. If you open that CSV in Excel, explicitly import the identifier column as Text; the CSV itself cannot force Excel's cell type.

Counts are necessary, but not sufficient

The basic check is:

input records = accepted records + held-back records
Enter fullscreen mode Exit fullscreen mode

One duplicated output record can mask one missing record, though. That is why the code also gives each input record a source reference and checks that the references form the exact expected set, with no duplicates.

filename:record-N is enough for this one-file demonstration. A production workflow needs a stable import/batch identifier as well; reusing the same filename must not mix separate imports. This counts parsed CSV records, not physical lines: quoted fields can contain newlines.

The source file is retained unchanged. held.csv includes the original field values and a reason, so fixing a problem does not require guessing what the supplier originally sent.

What this deliberately does not solve

This is a teaching example for a small, well-formed CSV using the displayed schema. It loads the file into memory. Before production use, add header/schema validation, malformed-record handling, file/field size limits, encoding policy, supplier-scoped keys and import identifiers. Preserve error evidence rather than quietly skipping malformed records.

It also does not handle destination transactions, retries, concurrent stock updates, internal item identity, locale-specific numbers, tax conversion, or your rounding policy. 19,95 and 1,234.56 need a declared locale, not an opportunistic replace operation. Negative prices, unfamiliar units and very large values are deliberately outside this example's acceptance rules.

For recurring stable layouts, Power Query or your ERP's existing importer may be sufficient. The useful boundary is independent of the tool: ambiguous rows should remain visible and held for a decision, rather than being made to look complete.

A practical next step

Run a small invented fixture through your import process. Include duplicate keys, missing units, leading zeros and a price needing rounding. Before loading a real destination, check whether every source record is still accounted for and whether every rejection has an actionable reason.

We also published a free supplier-import checklist and mapping template. It is ungated. The site has an optional waitlist for future Mapryva news; signup does not promise test access or a launch date.

What is the hardest part of your exception report: identifying the source row, explaining the reason, or safely applying the correction? General or fictional examples are enough—please do not share confidential files.

Top comments (0)