Skip to main content

SKU and variant

A "SKU" is a product line item (the master concept). A "variant" is a specific sized/colored version of that SKU, against which inventory is tracked. Many SKUs have only one variant; bikes and apparel typically have many.

Drafted from planning · v0.1

Why two tables

If we only had SKUs, we couldn't track sizes per item without piling fields onto the master. If we only had variants, the human "this is a Specialized Sirrus 4.0" concept would be scattered. The split: SKU is the brand/model concept; variant is the inventory unit.

Table: inventory_skus

ColumnTypeNotes
idINTEGER PK
sku_codeTEXT UNIQUEInternal SKU code (often from vendor)
nameTEXT"Specialized Sirrus 4.0 2024"
brandTEXT"Specialized"
modelTEXT"Sirrus 4.0"
category_idINTEGER FK → inventory_categories"bike", "component", "apparel", etc.
descriptionTEXTLong description
default_price_centsINTEGERDefault sell price; can be overridden per variant
vendor_idINTEGER FK → vendorsPrimary supplier
tax_categoryTEXT'bike_new', 'component', 'service_labour', etc.; drives BC tax rules
image_urlTEXTR2 URL for product photo
is_activeINTEGER (0/1)Soft-delete flag
created_at, updated_atTEXT

Table: inventory_variants

ColumnTypeNotes
idINTEGER PK
sku_idINTEGER FK → inventory_skus
variant_codeTEXT UNIQUEPer-variant SKU (e.g., includes size)
sizeTEXT"S", "M", "L", "XL", "54cm", "27.5"
colorTEXT"Red", "Blue/Black"
qty_on_handINTEGER NOT NULL DEFAULT 0Current count
qty_committedINTEGER NOT NULL DEFAULT 0Reserved (e.g., bike on hold)
qty_on_orderINTEGER NOT NULL DEFAULT 0In open POs
reorder_thresholdINTEGERWhen qty drops below this, low-stock alert
reorder_qtyINTEGERSuggested order quantity
price_centsINTEGEROverride of SKU default if needed
cost_centsINTEGERCost basis (for margin calc)
barcodeTEXTScannable barcode
created_at, updated_atTEXT
  • inventory_categories — Bikes, Components, Apparel, Accessories, Parts, Service Labor
  • inventory_adjustments — manual stock corrections (theft, damage, count fix)
  • vendors — Specialized, Trek, QBP, Shimano
  • purchase_orders + purchase_order_lines — incoming stock

Behaviors

GET /api/inventory/search?q=...:

  • Matches SKU name, sku_code, variant_code, barcode
  • Returns variant-level results (one row per variant matching)

Stock adjustment

POST /api/inventory/adjustments:

  • Body: { variant_id, delta, reason, notes? }
  • Updates inventory_variants.qty_on_hand atomically
  • Writes inventory_adjustments row
  • Audit-logged

Reorder alerts

Daily cron checks each variant where qty_on_hand + qty_on_order < reorder_threshold and writes to a daily low-stock report. Surfaces on the Today dashboard.

Tax category

The tax_category column on inventory_skus drives PST application:

  • bike_new — GST only, no PST
  • bike_used — GST only
  • component — GST + PST normally; if bundled with bike at sale, PST exempted
  • apparel — GST + PST
  • accessory — GST + PST
  • service_labour — GST + PST

See ADR-0020.

In-situ editing surface

On the Inventory screen, in edit mode:

  • Column visibility/order editable
  • Click any price to edit inline
  • Click photo to upload new
    • Add product opens full add form
  • Tax category dropdown appears on each row

On a product detail screen, in edit mode:

  • All fields editable
  • Variant table: drag handles, ×, +
  • Vendor + tax category in dropdowns

Migrated from AIM

For Swicked: 9,337 products, 9,338 variants, 234 manufacturers, 473 categories, 165 subcategories, and 2,544 barcodes from AIM's pim (product image / product master) + related tables:

  • pim.pim_pkinventory_skus.sku_code
  • pim.pim_descinventory_skus.name
  • Category mapping by pim.pim_catinventory_categories (cat_pk=448 is BIKES, special handling)
  • Variants from AIM's pimsize / pimcolor joins → inventory_variants
  • qty_on_hand from AIM's real-time stock counts

See also