How to Match Security Tag Counts to Sales Using Python

How to Match Security Tag Counts to Sales Using Python EduTechBuddy.com © edutechbuddy.com
How to Match Security Tag Counts to Sales Using Python © edutechbuddy.com
Learn how to reconcile hard security tag removals with tagged item sales using a concise Python script. This guide helps retail teams quickly identify mismatches and improve loss prevention processes.

Retail managers run into a persistent issue: making sure the number of hard security tags removed at checkout lines up with the sales of tagged items. Even with a solid point-of-sale (POS) system, mismatches can go unnoticed if no one compares physical tag counts to sales records. This guide shows how to automate that check using a short Python script and three CSV files.

Why Tag Reconciliation Matters

Apparel and accessory stores often rely on reusable hard tags to prevent theft. When a tagged item sells, the cashier removes the tag and drops it in a bin. At closing, staff count the tags in each bin. If the tag count in the bin doesn't match the number of tagged items sold, it could point to a process error, a missed tag removal, or a possible loss. Spotting these mismatches early lets managers address problems before they grow.

The National Retail Federation (NRF) points to the value of operational controls and customer service in retail, but there is no industry-wide rule or regulation that requires matching hard security tag removals to POS sales. The method here is a practical, store-level control that can be adjusted to fit a retailer's own needs and routines. The NRF has stressed the importance of recording operational data and reviewing trends over time to boost efficiency and service quality, which fits with the goals of this reconciliation process (Five to Thrive: Customer Service).

Required Data Files

To automate this check, you need three CSV files:

  • tagged_skus.csv: Lists which SKUs require a hard tag.
  • sales.csv: Contains POS sales transactions, including date, register, receipt, SKU, and quantity.
  • tag_counts.csv: Records the number of tags counted in each bin at closing, by date and register.

Each file uses a straightforward structure, making it easy to export from most POS systems and count sheets. In practice, reconciliation setups may need to handle store-specific rules like returns and exchanges, voided transactions, damaged goods, tag reuse, or items with more than one tag. These details aren't standardized across the industry and should be tailored to each retailer's workflow.

Python Script for Reconciliation

The script below uses only Python's standard library. It reads the three CSV files, figures out the expected number of tags per register and date, compares them to the actual counts, and creates a report that flags any mismatches.

import csv
from collections import defaultdict

def load_tagged_skus(path):
    with open(path, newline="") as f:
        return {row["sku"]: row["tag_type"] for row in csv.DictReader(f)}

def expected_tags(sales_path, tagged):
    expected = defaultdict(int)
    with open(sales_path, newline="") as f:
        for row in csv.DictReader(f):
            if row["sku"] in tagged:
                key = (row["date"], row["register"])
                expected[key] += int(row["qty"])
    return expected

def counted_tags(counts_path):
    counted = {}
    with open(counts_path, newline="") as f:
        for row in csv.DictReader(f):
            counted[(row["date"], row["register"])] = int(row["tags_in_bin"])
    return counted

def reconcile(expected, counted, tolerance=0):
    keys = sorted(set(expected) | set(counted))
    report = []
    for key in keys:
        exp = expected.get(key, 0)
        got = counted.get(key)
        if got is None:
            status, diff = "NO COUNT", None
        else:
            diff = got - exp
            if diff < -tolerance:
                status = "SHORT"
            elif diff > tolerance:
                status = "OVER"
            else:
                status = "OK"
        report.append((*key, exp, got, diff, status))
    return report

if __name__ == "__main__":
    tagged = load_tagged_skus("tagged_skus.csv")
    report = reconcile(expected_tags("sales.csv", tagged), counted_tags("tag_counts.csv"))
    print(f"{'date':<11}{'reg':<5}{'expect':>7}{'counted':>8}{'diff':>6} status")
    for date, reg, exp, got, diff, status in report:
        got_s = "-" if got is None else str(got)
        diff_s = "-" if diff is None else f"{diff:+d}"
        print(f"{date:<11}{reg:<5}{exp:>7}{got_s:>8}{diff_s:>6} {status}")

    with open("tag_reconcile_report.csv", "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["date", "register", "expected", "counted", "diff", "status"])
        w.writerows(report)

Put the script and the three CSV files in the same folder. Run the script with:

python3 tag_reconcile.py

The script prints a table for each date and register, showing the expected tag count, the actual count, the difference, and a status. It also writes a tag_reconcile_report.csv file for further review in a spreadsheet.

Understanding the Report

The report uses four status labels:

  • OK: The tag count matches tagged sales for that register and date.
  • SHORT: Fewer tags than expected. This could mean a tag left the store attached to a purchase, a miscount, or a dropped tag.
  • OVER: More tags than expected. This can happen if a cashier scanned an untagged SKU or if tags from returns or fitting rooms were mixed in.
  • NO COUNT: No tag count was recorded for that register and date. This points to a process gap that needs attention.

Retailers are putting more resources into in-store technology and operational efficiency, as the National Retail Federation notes, to help store associates serve customers and automate manual checks. Still, there is no regulatory deadline, enforcement, or required technical spec for reusable hard tags, tag bins, or POS-to-tag reconciliation (NRF podcast). This script works best as an optional internal audit tool that can be adapted to a retailer's own POS exports, register IDs, SKU formats, and closing routines.

Design Choices and Extensions

This script keeps things straightforward by using (date, register) as the key, making it easy to trace issues to a specific shift. The tolerance parameter lets you allow for small differences, such as on busy days. Missing counts are reported as such, not as zero, to avoid false alarms. No database is needed—just CSV files and a scheduled script.

Possible extensions include handling products with more than one tag, grouping shortages by SKU over time, emailing the report, or adjusting for returns. For more on operational best practices, the Qova Security blog offers further guidance: Qova Security.

This approach lets retail teams quickly spot and investigate mismatches, improving both loss prevention and process reliability. While the National Retail Federation has discussed how inaccurate inventory records can affect downstream merchandising and catalog management, such as the risk of "phantom inventory count" hurting product discoverability in AI-driven systems, the reconciliation of security tag removals with sales remains an internal process, not an industry-wide mandate. Retailers should keep these internal controls separate from legally required recordkeeping, accounting, or consumer-protection rules, which may differ by jurisdiction.

Camila Ferreira Editor EduTechBuddy.com
Co-Founder

Camila Ferreira

Camila Ferreira is the Co-Founder of EduTechBuddy, where her editorial focus is programming education and practical software development skills. Her primary subjects include Python programming, Git version control, and the fundamental concepts beginners need to understand when working with code.