Apparel and footwear break a standard ERP at the item record. A single style is not one product — in eight colours, twelve sizes and two widths it is 192 separately sellable, separately counted, separately replenished units. Every decision downstream depends on modelling that as variant dimensions rather than as 192 items.

The variant matrix

DimensionTypical cardinalityWhy it cannot be flattened into the item number
Colour4–20 per styleBuying, merchandising and markdown all operate at colour level.
Size8–18 per colourSize curves differ by store and drive replenishment independently.
Width (footwear)1–4A width is a different fit, not a different size — substitution is wrong.

Flattening these into item numbers seems simpler for a season and then becomes permanent. Reporting by colour across sizes, transferring one size between stores, marking down a colourway — each becomes a manual exercise against a naming convention instead of a query.

Replenishing at the right level

A store with forty units of a style and none in the three sizes that sell is out of stock, and the style-level report will not say so. Replenishment has to run against the size curve each store actually sells.

Codeunit 50180 Retail Replenishment Engine.al
codeunit 50180 "Retail Replenishment Engine"
{
    Access = Public;
    Subtype = Normal;

    procedure GenerateStoreTransferOrders(StoreLocationCode: Code[10]; TargetSafetyDays: Integer)
    var
        Item: Record Item;
        ItemVariant: Record "Item Variant";
        SKU: Record "Stockkeeping Unit";
        TransferHeader: Record "Transfer Header";
        TransferLine: Record "Transfer Line";
        SalesVelocity: Decimal;
        RequiredQty: Decimal;
        LineNo: Integer;
    begin
        SKU.SetRange("Location Code", StoreLocationCode);
        if not SKU.FindSet() then
            exit;

        repeat
            // 1. Calculate Daily Sales Velocity (Past 14 Days)
            SalesVelocity := CalculateDailyVelocity(SKU."Item No.", SKU."Variant Code", StoreLocationCode, 14);

            // 2. Determine Net Reorder Target
            RequiredQty := (SalesVelocity * TargetSafetyDays) - SKU.Inventory;

            if RequiredQty > 0 then begin
                if TransferHeader."No." = '' then
                    CreateTransferHeader(TransferHeader, 'CENTRAL_WH', StoreLocationCode);

                LineNo += 10000;
                TransferLine.Init();
                TransferLine."Document No." := TransferHeader."No.";
                TransferLine."Line No." := LineNo;
                TransferLine."Item No." := SKU."Item No.";
                TransferLine."Variant Code" := SKU."Variant Code";
                TransferLine.Quantity := RequiredQty;
                TransferLine.Insert(true);
            end;
        until SKU.Next() = 0;
    end;

    local procedure CalculateDailyVelocity(ItemNo: Code[20]; VariantCode: Code[10]; LocationCode: Code[10]; DaysWindow: Integer): Decimal
    var
        ValueEntry: Record "Value Entry";
        TotalSold: Decimal;
    begin
        ValueEntry.SetRange("Item No.", ItemNo);
        ValueEntry.SetRange("Variant Code", VariantCode);
        ValueEntry.SetRange("Location Code", LocationCode);
        ValueEntry.SetRange("Item Ledger Entry Type", ValueEntry."Item Ledger Entry Type"::Sale);
        ValueEntry.SetRange("Posting Date", CalcDate('<-' + Format(DaysWindow) + 'D>', Today), Today);

        ValueEntry.CalcSums("Invoiced Quantity");
        TotalSold := Abs(ValueEntry."Invoiced Quantity");

        if DaysWindow > 0 then
            exit(TotalSold / DaysWindow);
        exit(0);
    end;

    local procedure CreateTransferHeader(var TransferHeader: Record "Transfer Header"; FromLoc: Code[10]; ToLoc: Code[10])
    begin
        TransferHeader.Init();
        TransferHeader."No." := '';
        TransferHeader.Insert(true);
        TransferHeader.Validate("Transfer-from Code", FromLoc);
        TransferHeader.Validate("Transfer-to Code", ToLoc);
        TransferHeader.Modify(true);
    end;
}

Where POS integrations fail

The recurring failure is identity. The POS records a sale against its own product identifier; the ERP holds the variant. If the mapping between them is anything other than exact, stock drifts quietly and the difference is found at a stock count months later.

  1. Map variant to POS identifier explicitly — never derive it from a string pattern.
  2. Reject a POS transaction that references an unknown variant. Never post it to a catch-all.
  3. Reconcile daily on units, not value. Value masks offsetting errors.

Landed cost

Imported apparel arrives with freight, duty, insurance and clearance charges that are frequently 15 to 30 per cent of invoice value. If those sit in an expense account and not in the item cost, gross margin by style is wrong — and every buying decision made from it is wrong in the same direction.

Allocate landed cost across the container by the basis that reflects what actually drove it. Value works for duty; volume or weight works for freight. Using one basis for everything is convenient and quietly distorts the margin on bulky low-value lines.

One thing that is specific to this industry

Apparel inventory has an expiry date that nothing in the system knows about. A style that has not sold by the end of its season is not slow-moving stock; it is a markdown that has not happened yet.

Systems built for continuous replenishment report it as healthy inventory until somebody looks. Put the season end date on the style and report against it, or the markdown decision arrives after the window in which it would have worked.