Part 0 — Orientation

B What an ERP Actually Is

An ERP is one shared database that every part of a company writes to, wrapped in the screens and rules each department needs. This chapter teaches you the business before you write a line of code for it: where the category came from, what every module does, how money and goods actually move through an apparel brand, and the exact vocabulary your users will speak. Everything in Chapters 1 through 10 is an engineering answer to a problem defined here.

In this chapter15 sections · about 109 min
  1. What you need to know first
  2. B.1 The stage before the software: spreadsheet chaos
  3. B.2 What the letters mean, and what the category really is
  4. B.3 The actual history: MRP, then MRP II, then ERP
  5. B.4 The modules, and which ones you actually build first
  6. B.5 Apparel vocabulary, defined precisely
  7. B.6 ATS: the central number, and why it is contested
  8. B.7 Order-to-cash, step by step
  9. B.8 Procure-to-pay, step by step
  10. B.9 The inventory lifecycle
  11. B.10 Returns and RMAs
  12. B.11 The financial close, at altitude
  13. B.12 The canonical entity map
  14. B.13 Who actually uses this, and what their day looks like
  15. B.14 Where ERP projects fail

What you need to know first

This chapter is about the business, so there is nothing to install and no code to run. You do need a handful of computing words, because the chapter uses them casually from here on. Each one is defined below in plain language.

Tables, rows and keys

A database is a program that stores data and answers questions about it. The data lives in tables. A table is a grid: each row is one thing (one customer, one order) and each column is one fact about that thing (name, date, quantity). A schema is the list of tables and columns — the shape of the data.

Each row needs a way to be pointed at, so tables carry a primary key: a column whose value is unique in that table, usually an ID. When one row needs to refer to a row in another table, it stores that other row's key in a foreign key column. An order row storing customer_id is a foreign key pointing at a customer row. That single idea — store the key, not a copy of the name — is most of what "shared master data" means.

Transactions, SQL, grain and debits

A transaction, in database terms, is a group of changes that all succeed or all fail together. If a shipment is supposed to reduce stock and increase what a customer owes, a transaction guarantees you never get one without the other. Confusingly, the apparel business also uses "transaction" to mean "a business event that moved something." Both senses appear below; context makes it clear.

SQL is the language you use to ask a database questions and to define its tables. The create table blocks in this chapter are PostgreSQL sketches meant to show the shape of an idea. They are not the final schema — Chapters 3 to 8 build that properly. Read them as diagrams that happen to be written in code.

Two more words. The grain of a table is the precise thing one row represents, stated as a sentence: "one size of one color of one style," not "a product." Getting the grain wrong is the most expensive early mistake in this kind of system.

And in accounting notation, Dr is short for debit and Cr for credit — the two halves of every bookkeeping entry. Section B.11 explains what they mean; until then, read Dr X / Cr Y as "value moves out of Y and into X."

No accounting background is assumed. Every finance and apparel term is defined at the point it first appears.

B.1 The stage before the software: spreadsheet chaos

Every ERP project starts the same way. A small brand buys business software because the way it was working stopped working.

Picture a two-year-old apparel label. Four people. They sell hoodies and jackets to about sixty boutiques and two mid-size department chains. Their data lives in four places.

Where the data actually lives

The product list is a Google Sheet called FW26 LINE FINAL v4 (USE THIS ONE). It has one row per style and columns for each color. Sizes are stuffed into a single cell as text: "XS-S-M-L-XL". Nobody can filter it. Nobody can sum it.

Orders arrive three ways. The sales rep emails PDFs from trade shows. Boutiques text photos of handwritten order forms. The two department chains send EDI documents — Electronic Data Interchange, meaning structured electronic files sent computer-to-computer, defined properly in section B.5 — that land in a web portal nobody logs into for a week.

Inventory is a second spreadsheet, updated by whoever last visited the warehouse. It is always wrong, and wrong in a specific, expensive way: optimistically wrong, because when a unit gets promised to a customer nobody writes that down until the box ships. The sheet says 40 olive jackets in medium. Three are already promised. The owner sells all 40. One customer gets a phone call.

Money lives in a fourth place. Invoices go out of QuickBooks, typed by hand from the shipping paperwork, and sometimes the typist transposes a quantity. A retailer then pays $8,412.19 against three invoices totaling $9,100.00, attaching a remittance PDF that deducts $687.81 in penalties for shipping cartons with the wrong label. Nobody can tell whether that deduction was legitimate, so nobody disputes it. The brand eats it.

Every one of those is a data integration problem — the same fact living in several systems that never talk to each other. "We have 40 olive mediums" exists in five places and disagrees with itself in all five. Nobody is stupid; the information is simply not in one place, so the company cannot reason about itself. That is the condition an ERP exists to end.

The one-sentence definition

An ERP is a single shared transactional database, with a single agreed vocabulary, that every business function reads from and writes to, so that "how many can I sell?" and "how much do we owe?" have exactly one answer.

B.2 What the letters mean, and what the category really is

ERP stands for Enterprise Resource Planning. The name is a historical accident that actively misleads beginners. Enterprise means "the whole company," as opposed to one department. Resource originally meant physical things — raw material, machine hours, labor. Planning meant deciding how much of each you need and when. The phrase was coined for factory-scheduling software, and modern ERPs do far more than plan.

A working definition today: an ERP is the system of record for everything a business does that involves a countable thing or a dollar — products, stock, orders, purchases, shipments, invoices, payments, ledger entries. "System of record" means the one copy everyone agrees to trust when copies disagree. Everything else, from email to design files, sits outside the ERP.

Three properties distinguish an ERP from "some apps":

  1. One database, many modules. The sales screen and the accounting screen are views over the same tables. When a shipment posts, the inventory balance and the accounts-receivable balance change in the same database transaction. There is no nightly sync and no CSV export in the middle. This is the whole point.
  2. Transactional integrity. Business events are recorded as immutable facts, in order, with an audit trail. "Immutable" means once written, never changed. You do not edit history; you post a correcting entry alongside it. This idea comes directly from double-entry bookkeeping (section B.11) and it is the single most important design idea in the entire book.
  3. Shared master data. "Master data" is the reference data that documents point at rather than copy: products, customers, warehouses. There is exactly one record for a given product, one for a given customer, one for a given warehouse, and every document stores that record's key instead of re-typing its name.

PIM and WMS, the neighboring categories

Two adjacent terms you will hear. PIM — Product Information Management — is software dedicated to rich product content: descriptions, imagery, marketing copy, translations. WMS — Warehouse Management System — is software dedicated to the inside of a building: bin locations, pick paths, scanner workflows. An ERP knows how many units you have; a WMS knows which shelf they are on. A small apparel brand needs a slice of both embedded in the ERP and neither as a separate product.

B.3 The actual history: MRP, then MRP II, then ERP

You need this history for one reason: the shape of every ERP schema on earth, including the one you are about to build, was set by decisions made in the 1960s. The vocabulary you will use with your users is 60 years old.

1964 — MRP: Material Requirements Planning

Joseph Orlicky formalized Material Requirements Planning in 1964, partly in response to the Toyota production system in Japan. Black & Decker implemented it first, in 1964, with Dick Alban as project leader. Rolls-Royce and General Electric had computerized versions in the early 1950s, but never commercialized them.

MRP answers one question: given what I have promised to build, what do I need to buy, and when? It takes three inputs — a bill of materials (the recipe: what parts go into the product), a master production schedule (what we intend to build and when), and inventory records (what we already have), and produces purchase and production orders with dates.

MRP, by hand, for one week:

  Master schedule:      build 500 jackets in week 12
  Bill of materials:    1 jacket = 2.4 yd shell fabric
                                 = 1 zipper
                                 = 2 labels
  Gross requirement:    500 x 2.4 = 1,200 yd shell fabric
  On hand:                            300 yd
  Already on order:                   200 yd  (arrives week 10)
  ------------------------------------------------
  NET requirement:                    700 yd
  Supplier lead time:                 6 weeks
  => Place a purchase order for 700 yd no later than WEEK 6.

Read that carefully, because it is the seed of everything. Line by line: the schedule says build 500 jackets in week 12. The recipe says each jacket eats 2.4 yards of shell fabric, so the total need is 500 × 2.4 = 1,200 yards.

You already own 300 yards and have 200 more already ordered and arriving in week 10, so 500 of the 1,200 are covered and the true shortfall is 700 yards. The supplier takes 6 weeks to deliver, so counting backwards from week 12, the order has to be placed by week 6.

MRP does three things:

  • it explodes finished-goods demand into component demand using the recipe;
  • it nets that gross demand against stock you already own and stock already inbound;
  • and it offsets the result backward in time by the lead time to get an action date.

Explode, net, offset. Your ERP does a simplified version of exactly this when it tells the owner "place the factory order by March 7 or you miss the ship window."

MRP spread fast: 700 companies had implemented it by 1975, and roughly 8,000 by 1981.

1983 — MRP II: Manufacturing Resource Planning

Oliver Wight extended MRP into Manufacturing Resource Planning — confusingly abbreviated MRP II — in 1983, adding master scheduling, capacity planning (do we have enough machine hours and people, not just enough fabric?) and, critically, a financial view. For the first time one plan could be expressed in units for the factory and in dollars for finance. By 1989, MRP II software made up about one-third of the software industry sold to American industry, worth roughly $1.2 billion a year.

That financial linkage is the direct ancestor of the rule you will implement in Chapter 8: every physical inventory movement produces a matching accounting entry, automatically, in the same transaction.

The 1990s — Gartner names it "ERP"

The research firm Gartner is credited with first using the acronym ERP in the 1990s. The insight was that machinery built for factories — netting supply against demand, posting transactions, tying operations to finance — was equally useful for functions with no factory at all: HR, sales, purchasing, projects. Strip out the manufacturing assumption and you have a general integrated business system.

SAP was already there. Five former IBM engineers — Dietmar Hopp, Klaus Tschira, Hans-Werner Hector, Hasso Plattner and Claus Wellenreuther — founded it in Weinheim, Germany in June 1972, having worked together at IBM in nearby Mannheim. The name stands for Systems, Applications and Products in Data Processing.

SAP shipped its financial accounting system in 1973, R/2 in 1979 (adding materials management and production planning) and R/3 in 1992. R/3 made SAP the default for large enterprises worldwide. SAP reported revenue of €36.8 billion for 2025 and says 98 of the world's 100 largest companies are its customers.

In 2000 Gartner published ERP Is Dead — Long Live ERP II, predicting that the next generation would be web-based and open to partners outside the company's walls. That prediction describes the software you are about to build.

1998 onward — cloud ERP

NetSuite was founded in 1998 by Evan Goldberg, backed by Larry Ellison, originally as NetLedger — web-hosted accounting software. It renamed to NetSuite in September 2003, is generally recognized as the first cloud computing software company (pre-dating Salesforce by roughly a month), and was bought by Oracle for $9.3 billion in a deal announced in July 2016 and completed that November.

Cloud ERP changed two things that matter to you directly. First, multi-tenancy: one running copy of the application serves many separate customer companies at once, with each company's data kept apart by a tenant identifier rather than by giving each customer its own installation. Second, continuous delivery: the vendor upgrades everyone on the same schedule, which only works if customers have not forked the source code into private versions. Both constraints reappear later as concrete engineering rules — row-level security in Chapter 3, configuration-over-customization in Chapter 9.

B.4 The modules, and which ones you actually build first

"Module" is ERP jargon for a functional area. In a monolith like SAP, modules were literally licensed separately. In the system you are building, a module is a set of tables, screens and rules that hang together. Here is what each one owns:

  • Product / catalog (PIM). The definition of what you sell: styles, colors, sizes, the SKU (Stock Keeping Unit) list, barcodes, costs, prices, season assignments, images, composition, country of origin. Everything downstream points here.
  • Inventory / warehouse (WMS overlap). How many units of each SKU exist, where, and in what state. Owns the transaction log of every movement and the balances calculated from it. The WMS part — bins, pick paths, scanning — is optional depth.
  • Sales orders. Demand: who wants what, at what price, shipping where, inside what date window. Owns quotes, orders, order lines, ship windows, status and cancellations. Purchasing is the supply mirror: purchase orders (POs) to factories, expected dates, costs, and the receiving process that closes them out.
  • Production / manufacturing. For brands that cut and sew rather than buy finished goods: bills of materials, cut tickets (the work order authorizing a quantity of a style to be cut from specific fabric), work-in-progress tracking and raw material consumption.
  • Fulfillment / shipping. Turning orders into boxes: allocation, pick tickets, packing, cartons, carrier labels, bills of lading, and the ASN — Advance Ship Notice, the electronic "here is exactly what is on the truck" message — sent to the retailer.
  • Accounts receivable (AR). Money customers owe you: invoices, credit memos, payments, cash application, aging, credit limits, deductions and disputes. Accounts payable (AP) is the mirror: supplier invoices, the three-way match (checking the purchase order, the goods receipt and the supplier's bill against each other before paying), payment runs. General ledger (GL) is the financial spine — chart of accounts, journal entries, trial balance, financial statements — where every other module ultimately posts.
  • CRM edges. CRM stands for Customer Relationship Management: customer and prospect records, contacts, sales reps, territories, commission. A full CRM is a separate product; an ERP needs the customer master and the rep assignment. Reporting covers sell-through, open orders, aged inventory, margin by style and order book by delivery month — the module owners actually log in for.

Two abbreviations appear in the table below before their full definitions arrive. ATS means available-to-sell: the stock a rep is actually allowed to promise, worked out in section B.6. The numbered codes in the EDI row (850, 855, 856, 810) are standard electronic message types, listed in section B.5.2.

ModuleOwnsDay one?Why
Product / catalog (PIM)Styles, colorways, sizes, SKUs, barcodesDay oneNothing else can exist without a SKU to point at.
Customers & ship-to locationsRetailer master, addresses, termsDay oneAn order needs someone to belong to.
Price listsWholesale price by SKU by listDay oneDifferent customers pay different prices. Hardcoding one price is a rewrite later.
Sales ordersQuotes, orders, lines, ship windowsDay oneThis is the product. Everything else serves it.
Inventory transactions & balancesMovement log, on-hand, allocated, ATSDay oneOverselling is the failure that made them buy the software.
AllocationReserving units to order linesDay oneWithout it, "available" is a lie. See B.6.
Fulfillment / shippingPick, pack, ship, packing listDay oneOrders must be able to leave the building.
Purchasing / POs to factoryFactory orders, expected dates, on-orderDay oneForward ATS depends on knowing what is inbound.
ReceivingReceipts against POsDay oneThe only legitimate way stock enters the system.
Invoicing (AR documents)Invoices generated from shipmentsDay oneShipping without invoicing means not getting paid.
Payments & cash applicationReceipts, applying cash to invoicesDay one (basic)Start with "mark invoice paid." Deductions can wait.
Reporting: open orders, ATS, sell-throughRead-only aggregatesDay one (three reports)Three good reports beat a report builder.
Returns / RMAAuthorized returns, credit memosPhase 2Real but low volume at first; can be handled manually for months.
Accounts payableSupplier invoices, three-way matchPhase 2Small brands run AP out of accounting software initially.
General ledgerChart of accounts, journals, closePhase 2 or neverMost small brands keep QuickBooks/Xero. Integrate, do not rebuild.
EDI trading partners850/855/856/810 message exchangePhase 2Required the day they land a major chain — not before.
Production / cut tickets / BOMManufacturing orders, materialsPhase 3Only if the brand manufactures rather than buys finished goods.
Warehouse (bins, scanners, pick paths)Location-level stock, handheld-scanner workflowsPhase 3Needed past roughly one warehouse and 10k units/month.
CRM (pipeline, activities)Prospects, tasks, commsLater / neverSales reps already use email and a phone.
Read the "Day one" column as a scope contract

Twelve modules is a large day one, and it is still the minimum for a coherent system. Drop any one of them and what you ship is a tool rather than an ERP. Add anything from the Phase 2 or 3 list before those twelve work, and you will ship neither.

B.5 Apparel vocabulary, defined precisely

Apparel has its own language. Your users will not translate for you. Learn it exactly, because several of these words look like ordinary English and mean something specific.

Style, colorway, size: the three levels

The most important structural fact is the style-color-size hierarchy. A style is a design, "the Fielder Jacket", with a style number, a fit, a pattern and a fabric.

A colorway is one coordinated color combination of that style across every component: shell fabric, lining, thread, zipper, buttons, labels. Colorways are specified with Pantone TCX or FHI codes, Pantone's two standard color libraries for textiles and fashion, and confirmed with physical lab dips, small swatches the mill dyes and mails for approval before it dyes the bulk fabric.

A size is a point on a size scale. Only the combination of all three is a physically distinct object you can count and sell, and that combination is the SKU, or Stock Keeping Unit.

ONE STYLE  ->  MANY COLORWAYS  ->  MANY SIZES  =  MANY SKUs

  Style: FLDJKT "Fielder Jacket"
    |
    +-- Colorway OLV "Olive"        +-- Colorway BLK "Black"
    |     |                         |     |
    |     +-- XS  FLDJKT-OLV-XS     |     +-- XS  FLDJKT-BLK-XS
    |     +-- S   FLDJKT-OLV-S      |     +-- S   FLDJKT-BLK-S
    |     +-- M   FLDJKT-OLV-M      |     +-- M   FLDJKT-BLK-M
    |     +-- L   FLDJKT-OLV-L      |     +-- L   FLDJKT-BLK-L
    |     +-- XL  FLDJKT-OLV-XL     |     +-- XL  FLDJKT-BLK-XL
    |                               |
    (5 SKUs)                        (5 SKUs)

  1 style x 2 colors x 5 sizes = 10 SKUs
  1 style x 8 colors x 9 sizes = 72 SKUs
  A 40-style season, 4 colors, 6 sizes  =  960 SKUs

The diagram reads top down. One design, the Fielder Jacket, exists in two colors. Each color exists in five sizes. Multiply and you get ten separately countable items, each with its own code. The three lines at the bottom just repeat that multiplication at bigger numbers: 2 × 5 = 10, 8 × 9 = 72, and a forty-style season in four colors and six sizes is 40 × 4 × 6 = 960 distinct items to track.

This tree is why apparel data models are harder than they look. Nothing is priced, photographed or merchandised at the SKU level — a buyer picks "the Fielder Jacket in Olive," not "FLDJKT-OLV-M." But everything is counted, allocated, picked, barcoded and shipped at the SKU level. Your schema needs both levels as first-class citizens, and your screens must let users move between them constantly. Chapter 4 calls the SKU level a variant, and that naming decision starts here.

The last line of the diagram is the SKU explosion problem: a grid for one style is 4 rows by 6 columns and readable, while a flat list of 960 SKUs is unusable. Design for the grid.

Never let a size be free text

The single most common data disaster in apparel systems is sizes typed by hand. "M", "m", "Med", "Medium", "MED " with a trailing space become five distinct values, and every inventory number breaks. Sizes belong to an ordered, tenant-owned lookup table with a sort position, because "S, M, L, XL" must never sort alphabetically as "L, M, S, XL."

Terms, and what each one does to your schema

TermPrecise meaningWhy it hits your schema
StyleA garment design, identified by a style number shared by all its colors and sizes.Parent table. Carries name, category, fabric, season, images.
ColorwayOne coordinated color combination across shell, lining, trim and labels; specified in Pantone TCX/FHI.Middle level. Usually modeled as an attribute of the variant plus a style-color grouping.
Size runThe complete set of sizes a style is produced in, e.g. XS-S-M-L-XL, or numeric misses 0-2-4-6-8.An ordered set attached to a style. Drives the columns of every grid.
Size curve / size scaleThe ratio of quantities across the run, written like 1:2:2:1. Without sales history, brands default to a bell shape with the middle sizes weighted heaviest.Used to explode a total buy quantity into per-size quantities.
SKUStock Keeping Unit. One style + one color + one size. The atomic countable item. Internal to your company; not standardized between companies.The grain of every inventory and order-line row.
PrepackA carton with a predetermined mix of sizes of one style sold as a single unit. A solid pack is one size; a ratio pack is mixed, e.g. 1-2-2-1.An order line for 10 prepacks must explode into per-SKU allocation. See B.5.1.
UPC / GTINGlobal Trade Item Number, from the standards body GS1. US retail apparel uses the 12-digit GTIN-12, printed as a UPC-A barcode. It contains a GS1 company prefix, an item reference and a check digit that catches mis-scans.GS1's rule is explicit: every size and color variation needs its own GTIN. A one-size tee in 3 colors needs 3 GTINs. So it is a column on the SKU, not on the style.
Season / dropThe selling period a style belongs to: Spring/Summer (SS), Fall/Winter (FW or AW), Resort/Cruise, Pre-Fall. Cadence varies widely by brand — two main seasons is the historic base, with resort and pre-fall stacked on top, and many contemporary brands add smaller drops through the year.A filter on almost every screen and the natural partition for reporting.
DeliveryA specific in-store arrival date within a season. One season can have several deliveries.Order lines carry a delivery, which drives the ship window.
LinesheetThe wholesale sales document sent to buyers: image, style number, colorway, size run, wholesale price, MSRP, minimum order quantity, ship window, terms. Distinct from a lookbook, which is imagery only.A generated PDF/web view. Reads catalog + price list + season.
MSRPManufacturer's Suggested Retail Price. What the consumer is expected to pay.Catalog attribute. Printed on linesheets, never used to bill.
Wholesale priceWhat the retailer pays the brand per unit.Lives on a price list, not on the product.
CostWhat the brand pays the factory per unit, plus landed cost (freight, duty, insurance).Drives margin. Never shown to customers.
Keystone markupRetail price = wholesale price × 2. That is a 100% markup on the wholesale figure and a 50% gross margin on the sale price. Multipliers vary by brand and category; apparel commonly runs about 2.0×–2.5× from cost to wholesale and again about 2.0×–2.5× from wholesale to retail.Explains why MSRP is roughly 2–2.5× your wholesale number, and why both must be stored.
Pre-book / futuresOrders placed months in advance of production, committing demand so the brand knows what to make.These orders exist against inventory that does not exist yet. This is why ATS must be forward-looking.
At-once / immediatesOrders filled from stock on hand and shipped now.Draws down current ATS in real time. Competes with pre-books for the same units.
Market weekA trade-only period when buyers visit a fashion hub to view collections and write orders. Major US shows include Coterie and MAGIC in New York (February and September) and MAGIC Las Vegas (17–19 February and 10–12 August in 2026), plus LA Market Week.Order entry bunches into a handful of days, often on unreliable convention-center wifi. Design for fast keyboard entry that survives a dropped connection.
Start ship dateThe earliest date the brand may ship the order. Usually order date + production lead time.A date column on the order. Ship before it and the retailer can refuse the goods.
Cancel dateThe latest date the buyer will accept the shipment. After it, the buyer may cancel or reject. Two weeks after start ship is the most commonly cited window, though it varies by retailer.Drives urgency queues, auto-cancellation and the most stressful screen in the app.
Cut ticketThe internal production work order authorizing how much fabric to cut, in which sizes and quantities, for one style.The manufacturing analogue of a purchase order. Creates future supply.
EDIElectronic Data Interchange: computer-to-computer exchange of structured business documents, with no human re-typing in the middle. North America uses the ANSI ASC X12 standard; most of the rest of the world uses UN/EDIFACT. Files travel over a VAN (value-added network, a paid message-routing service) or over AS2, the internet transport Walmart adopted in 2002.An integration surface you have to maintain. See the transaction-set list below.
ChargebackA deduction a retailer takes off your invoice for violating its vendor compliance manual or routing guide — wrong labels, late ASN, wrong carrier, short shipment. Rates vary a lot by retailer; apparel suppliers commonly report 2–5% of wholesale invoice value, and across retail generally suppliers report losing 2–10% of revenue to deductions.Payments will not equal invoices. Your cash application must handle partial payment with reason codes.
FactoringSelling your invoices to a factor at a discount for immediate cash. Advance rates commonly 70–90% of invoice value; fees commonly 1–5% of invoice value per 30 days, varying with volume and customer credit quality. Under non-recourse factoring the factor absorbs the loss if the retailer goes bankrupt; under recourse, you do. Factoring has been a mainstay of working capital in textiles and apparel since the nineteenth century.The factor, not you, approves each customer's credit limit. Your credit-check step calls out to their answer.
ConsignmentYou place goods in a retailer's store but keep legal ownership until they sell. You are paid only on sale; unsold goods come back.Inventory you own sits in a location you do not control. Needs its own warehouse-type record.
DropshipThe retailer takes the consumer's order and you ship directly to the consumer on their behalf.Thousands of one-unit orders with consumer addresses, mixed into a wholesale system built for pallets.
Sell-through rate(Units sold ÷ units received) × 100 for a period. Benchmarks vary by category and by how long the season runs; 70–80% across a season is widely described as healthy, and much below 60% is usually read as a markdown signal.The number the owner actually cares about, and it needs retailer point-of-sale data you may not have.
Open-to-buy (OTB)How much more inventory can still be purchased for a period without blowing the plan. OTB = planned sales + planned markdowns + planned end-of-month stock − beginning-of-month stock − on order.A planning report, phase 3 at the earliest.
RMAReturn Merchandise Authorization. The number a seller issues that authorizes a specific return before it is accepted at the dock.Gates reverse logistics. Unauthorized returns must be refusable.

B.5.1 The prepack problem, in full

Prepacks deserve their own section because they break the naive data model, and every apparel system has to solve them.

The buyer's order line says:

    FLDJKT-OLV   "Fielder Jacket, Olive"   PREPACK-A   x 10

The prepack definition (PREPACK-A, ratio 1-2-2-1):

    XS : 1        S : 2        M : 2        L : 1     = 6 units per pack

What the ORDER means to the customer:      10 packs
What the ORDER means to inventory:         10 x each ratio, per SKU

    FLDJKT-OLV-XS   10 x 1  =  10 units
    FLDJKT-OLV-S    10 x 2  =  20 units
    FLDJKT-OLV-M    10 x 2  =  20 units
    FLDJKT-OLV-L    10 x 1  =  10 units
                              --------
                    total     60 units   (10 packs x 6 units)

What the INVOICE shows:   10 packs @ $312.00 = $3,120.00
What the WAREHOUSE picks: 60 individual garments, or 10 sealed cartons

Follow the arithmetic. The pack recipe is one extra-small, two smalls, two mediums and one large, which is 1 + 2 + 2 + 1 = 6 garments in every carton. The buyer ordered ten cartons, so each size multiplies by ten: 10, 20, 20 and 10 units, totaling 60 garments. The customer is billed per carton at $312.00, so the invoice reads 10 × $312.00 = $3,120.00, which works out at $52.00 per garment. One order, three different ways of counting it.

The order is expressed in one unit of measure (packs) and the inventory in another (each). Your system must hold both truths simultaneously and never let them drift.

The clean solution, which Chapter 4 develops, is to store the order line at the SKU grain internally — four rows, one per size, while displaying and pricing it as ten packs. The prepack becomes a template that explodes on entry, plus a display grouping.

If instead you treat a prepack as its own SKU with its own stock, you now have to decide whether an olive medium sitting inside a sealed pack is available to sell on its own, and every answer to that question is wrong.

B.5.2 The EDI transaction sets you will meet

A "transaction set" is one standard message type, identified by a three-digit number. Trading partners agree on which ones they exchange.

ANSI ASC X12 retail transaction sets, in the order they occur:

  850  Purchase Order          R->Y  "here is what we want"
  855  PO Acknowledgment       Y->R  "accepted, with changes"
  860  PO Change Request       R->Y  "make it 40, not 60"
  846  Inventory Inquiry       Y->R  "here is my ATS by SKU"
  856  Advance Ship Notice     Y->R  "shipped: cartons, SKUs, BOL"
  810  Invoice                 Y->R  "you owe us this"
  820  Payment / Remittance    R->Y  "paid X, deducted Y, and why"
  852  Product Activity Data   R->Y  "what sold at store level"
  997  Functional Ack          both  "I received your file"

  R = the retailer,  Y = you, the brand

Read the list as a conversation running down the page, with the arrow showing who sends each message. The retailer opens with an 850 (their purchase order) and you answer with an 855 (accepted, possibly with changes); an 860 is the retailer changing its mind afterwards. The 846 is you publishing your availability.

The 856 announces a shipment before it arrives, the 810 bills for it, and the 820 is the money coming back with an explanation of anything deducted. The 852 reports what actually sold in their stores. The 997 is a plain receipt confirmation for any of the above — the electronic equivalent of "got it."

Notice the symmetry with your own tables. The 850 becomes a sales order. The 856 is generated from a shipment and carries shipment-level data (carrier, bill-of-lading number), order-level data (the retailer's PO number) and item-level data (SKU, quantity, carton IDs). The 810 is an invoice; the 820 is a payment plus the deduction reasons behind any short payment; the 852 is the only way most brands ever see real sell-through.

A missing, late or inaccurate 856 is among the most common chargeback triggers in retail — a business reason your shipment record must be complete before the truck leaves.

B.6 ATS: the central number, and why it is contested

ATS means Available-to-Sell. It is the number a salesperson sees before promising a customer anything, and it is the single most-viewed figure in an apparel ERP.

The apparel definition is narrow and specific: ATS is stock physically on hand that has not already been committed to another order.

SIMPLE ATS  (what "available" means today, in this warehouse)

    on_hand            units physically in the building
  - allocated          units already committed to open sales orders
  - reserved           units held back (samples, quality hold, damaged)
  ------------------
  = ATS

  Worked example, FLDJKT-OLV-M:
    on_hand      40
    allocated   -12   (three open orders waiting to ship)
    reserved     -3   (2 press samples, 1 damaged)
    ---------------
    ATS          25   <-- the only number a rep may sell against

The arithmetic is 40 − 12 − 3 = 25. Forty units are physically in the building. Twelve of them already belong to customers who have ordered and not yet been shipped. Three more cannot be sold at all: two are out with a magazine and one is damaged. Twenty-five are genuinely available. In the spreadsheet world of section B.1, the rep sees 40, sells 40, and 15 customers get bad news. The entire justification for the allocations table you will build in Chapter 6 is the middle line of that calculation.

Forward ATS: availability as of a date

Now the complication. Apparel sells the future. A buyer at market week in September is ordering goods manufactured in December and shipped in February; on-hand today is zero, and selling against "ATS = 0" would mean selling nothing. So apparel systems also compute forward ATS: availability as of a future date, netting inbound supply against dated demand.

FORWARD ATS as of a date D, for one SKU in one warehouse

    on_hand_now
  + sum(purchase_order_lines.qty_open  WHERE expected_date <= D)
  + sum(production/cut_ticket qty_open WHERE expected_date <= D)
  - sum(sales_order_lines.qty_open     WHERE start_ship_date <= D)
  - reserved
  ------------------------------------------------------------
  = ATS as of D

  FLDJKT-OLV-M.  on hand now = 40.  reserved = 3 throughout.
  "cumul. inbound" and "cumul. demand" are running totals of
  everything due on or before that row's date.

  date        event                  cumul.   cumul.     free
                                    inbound   demand  balance
  ----------  --------------------   -------  -------  -------
  today       starting position            0       12       25
  Nov 15      PO-1041 arrives +600       600       12      625
  Dec 01      SO-2210 due     -300       600      312      325
  Jan 10      SO-2288 due     -220       600      532      105
  Feb 01      PO-1077 arrives +400      1000      532      505

  free balance = 40 + cumul. inbound - cumul. demand - 3

  A buyer asks on Dec 15 for 150 units to ship FEB 1:
    from Feb 1 onward the free balance is 505.  YES.

  The same buyer asks for 150 units to ship JAN 5:
    the free balance on Jan 5 is 325, which looks like a yes.
    But SO-2288 needs 220 more on Jan 10, dropping the free
    balance to 105.  Promising 150 on Jan 5 would break that
    order.  NO.

Read the table as a running balance through time. Each row adds that date's event to the running totals, then recomputes the free balance from the formula on the line below the table. Check one row yourself: at Jan 10 the free balance is 40 + 600 − 532 − 3 = 105.

Two lessons come out of it. First, the same SKU is simultaneously short in January and long in February, which is why ATS is a function of a date rather than a stored column, and why a naive quantity_available field on the product table is one of the most damaging shortcuts you can take.

Second, a safe promise for a given ship date is the smallest free balance from that date forward, not the balance on that one day. Checking a single date is how systems cheerfully sell units that a later order already needs.

The four arguments about what ATS includes

ATS is "contested" because reasonable people define the middle terms differently, and the differences are worth real money:

  • Does an unconfirmed quote consume ATS? Sales says yes, to protect the deal. Operations says no, so stock is not held hostage by dead quotes.
  • Does a pre-book order for February consume today's ATS? If yes, at-once business starves. If no, you may sell the same unit twice.
  • Does a purchase order arriving in three weeks count as available? Factories are late; counting on-order stock as available is how brands promise goods that do not exist.
  • Do returns in transit count, and does stock at a third-party warehouse or on consignment in a store count? You own all of it. You may not be able to ship any of it.
ATS is a policy decision, so write the policy down

There is no universally correct ATS formula, and every ERP argues about it. Your job is to make the policy explicit, named, versioned and testable — a single function with a documented set of inclusion rules — rather than scattering slightly different SUM() expressions across nine screens. When the owner asks "why does the rep portal say 25 and the report say 31?", you want one place to look.

Order-to-cash, the spine of the business Order-to-cash, the spine of the business. Read it left to right: a promise becomes reserved units, then physical movement, then money owed, then money received. The row underneath shows what each step writes to the inventory ledger — note that taking an order changes nothing physical, and that only shipment reduces on-hand stock. Confusing "allocated" with "gone" is the single most common modelling mistake. Sales order promise made Allocation units reserved Pick + pack warehouse Shipment stock leaves Invoice money owed Payment cash applied ORDER-TO-CASH Ledger effect at each step none (demand only) allocated +N on_hand unchanged picked +N on_hand −N shipment row A/R +$ A/R −$ cash +$ Nothing above is ever edited in place. Each step writes a NEW immutable row.
Order-to-cash, the spine of the business. Read it left to right: a promise becomes reserved units, then physical movement, then money owed, then money received. The row underneath shows what each step writes to the inventory ledger — note that taking an order changes nothing physical, and that only shipment reduces on-hand stock. Confusing "allocated" with "gone" is the single most common modelling mistake.

B.7 Order-to-cash, step by step

Order-to-cash (O2C) is the process that starts when a customer expresses interest and ends when the money is in your bank and correctly matched. It is the primary process of a wholesale brand. Here it is in full, with the document produced and the state change caused at each step.

ORDER-TO-CASH

 1 QUOTE            doc: quotation           state: nothing reserved
       |                                     ATS impact: none (usually)
       v
 2 SALES ORDER      doc: order confirmation  state: order = open
       |                                     demand now exists
       v
 3 CREDIT CHECK     doc: credit hold/release  state: credit_hold | open
       |                                     factor or controller decides
       v
 4 ALLOCATION       doc: (internal)          state: units allocated
       |                                     ATS DROPS HERE
       v
 5 PICK             doc: pick ticket         state: allocated -> picked
       |                                     stock leaves its bin
       v
 6 PACK             doc: packing list        state: picked -> packed
       |                                     cartons + carton contents exist
       v
 7 SHIP             doc: BOL + ASN (856)     state: on_hand DECREASES
       |                                     order line -> shipped
       |                                     GL: Dr COGS / Cr Inventory
       v
 8 INVOICE          doc: invoice (810)       state: AR balance INCREASES
       |                                     GL: Dr A/R / Cr Revenue
       v
 9 PAYMENT          doc: remittance (820)    state: cash received
       |
       v
10 CASH APPLICATION doc: applied receipt     state: invoice open -> paid
       |                                     GL: Dr Cash / Cr A/R
       v
11 DEDUCTIONS       doc: chargeback/         state: residual balance
   /CHARGEBACKS          credit memo               disputed or written off

Each block in that diagram is one step, reading downward. The first column names the step, the second names the paper or electronic document it produces, and the third says what changed in the data as a result. The GL: lines are the accounting entries; Dr means debit and Cr means credit, and COGS is cost of goods sold. Walk it slowly, because the state changes are the design.

Steps 1 to 4: quote, order, credit check, allocation

1. Quote. A price offer with no obligation, no inventory effect and no accounting effect. It exists so a rep can hand a buyer a number without creating a commitment, and it converts to an order in one click.

2. Sales order. The customer's commitment to buy and yours to supply. It records the customer, the ship-to location, the sales rep, the price list, the requested delivery, and, critically for apparel, the start ship date and cancel date. It creates demand. Nothing has left the building, but every forward-ATS calculation now nets against it.

3. Credit check. Before you spend money making and shipping goods, someone checks whether this retailer will pay. For a factored brand the factor decides, per order. The order sits in a credit_hold state that blocks allocation until released. Skipping this is how a brand ships $80,000 to a chain that files for bankruptcy the following week.

4. Allocation. The system commits specific quantities of specific SKUs in a specific warehouse to this order. Nothing moves physically, but ATS drops immediately and for everyone. This is the moment the software earns its price. Allocation policy — first come first served, by cancel date, by customer priority, or manually by a merchandiser — is a genuine business decision, so Chapter 6 makes it pluggable.

Steps 5 to 8: pick, pack, ship, invoice

5. Pick. A pick ticket goes to paper or a scanner, listing order number, ship-to, SKUs, quantities and bin locations, and deliberately excluding prices. Stock moves from storage to staging; inventory state moves from allocated to picked.

6. Pack. Garments go into cartons. The system records carton contents, weights and dimensions and produces a packing list. Carton-level detail is mandatory if the customer takes an ASN, because the 856 carries carton IDs the retailer scans at its dock.

7. Ship. The truck leaves, and more changes here than at any other step in the process. A bill of lading (BOL) is issued: the legal contract with the carrier, traveling with the goods. An ASN goes to the retailer electronically. And now, for the first time, on-hand inventory actually decreases. The entry is Dr Cost of Goods Sold / Cr Inventory: the cost of those garments stops being an asset on the balance sheet and becomes an expense against this period's sales.

8. Invoice. The bill, generated from the shipment rather than the order, so you bill what you actually sent. The entry is Dr Accounts Receivable / Cr Revenue. Under the US revenue-recognition standard ASC 606, revenue is recognized when control of the goods transfers to the customer — for typical wholesale terms that is at shipment or at delivery, depending on the shipping terms in the contract. Invoice before shipping and the credit goes to deferred revenue, a liability, until you perform.

Steps 9 to 11: payment, cash application, deductions

9–10. Payment and cash application. Money arrives 30 to 60 days later, usually covering several invoices at once, and must be matched to the specific open invoices it pays: Dr Cash / Cr Accounts Receivable. Matching is much harder than it sounds, because of step 11.

11. Deductions and chargebacks. The payment rarely equals the invoice total. A short pay is any partial payment; the difference is a deduction, coded with a reason — short shipment, price discrepancy, early-payment discount, freight dispute, promotional allowance, or a compliance chargeback. Each must be researched, then recovered or written off with a credit memo. Apparel brands lose real percentage points of revenue here, and most cannot say how much, because their systems cannot tie a deduction back to the shipment that caused it.

The order-to-cash tables in SQL

-- The order-to-cash spine, stripped to its essentials.
-- Every table carries tenant_id: multi-tenancy is designed in
-- from the start, not bolted on later.

create table sales_orders (
  id                uuid primary key default gen_random_uuid(),
  tenant_id         uuid not null references tenants(id),
  order_number      text not null,
  customer_id       uuid not null references customers(id),
  ship_to_id        uuid not null references ship_to_locations(id),
  sales_rep_id      uuid          references sales_reps(id),
  price_list_id     uuid not null references price_lists(id),
  warehouse_id      uuid not null references warehouses(id),
  order_type        text not null default 'prebook',  -- prebook | at_once
  customer_po       text,                             -- their PO number
  start_ship_date   date not null,
  cancel_date       date not null,
  status            text not null default 'draft',
  -- draft | credit_hold | open | partially_shipped | shipped | cancelled
  created_at        timestamptz not null default now(),
  unique (tenant_id, order_number),
  check (cancel_date >= start_ship_date)
);

create table sales_order_lines (
  id                uuid primary key default gen_random_uuid(),
  tenant_id         uuid not null references tenants(id),
  sales_order_id    uuid not null references sales_orders(id)
                      on delete cascade,
  variant_id        uuid not null references variants(id),   -- the SKU
  qty_ordered       integer not null check (qty_ordered > 0),
  qty_allocated     integer not null default 0,
  qty_shipped       integer not null default 0,
  qty_cancelled     integer not null default 0,
  unit_price        numeric(12,2) not null,
  prepack_id        uuid references prepacks(id),  -- how it was ENTERED
  line_number       integer not null,
  check (qty_allocated + qty_cancelled <= qty_ordered),
  check (qty_shipped <= qty_allocated)
);

Reading the SQL: create table defines a table; each indented line is one column, written as name, then type, then rules. uuid is a long random identifier, text is a string, numeric(12,2) is a number with two decimal places for money, and timestamptz is a date and time with a time zone.

references customers(id) makes the column a foreign key, so the database refuses to store an order pointing at a customer that does not exist. not null means the column must have a value. unique (tenant_id, order_number) means order numbers must be unique within one brand, though two different brands may both have an order 1001. check (...) is a rule the database enforces on every write.

Four things in that sketch carry the chapter's ideas into code. tenant_id appears on both tables, including the child — it could be looked up through the parent, but repeating it is what lets one row-level security policy (a database rule that filters rows by the logged-in user's tenant) protect every table uniformly, as Chapter 3 explains.

The line stores four quantities rather than one: ordered, allocated, shipped, canceled. A single quantity column cannot express "ordered 60, allocated 40, shipped 40, customer canceled the rest," which happens every day in wholesale. prepack_id records how the line was entered while the line itself stays at SKU grain — the resolution to B.5.1.

And the two check constraints encode business rules the database enforces itself: you cannot allocate more than you ordered, nor ship more than you allocated. Put invariants in the database whenever the database can express them.

B.8 Procure-to-pay, step by step

Procure-to-pay (P2P) is the mirror image: it starts with needing goods and ends with paying the supplier. For an apparel brand the supplier is usually an overseas factory, and the lead times are long enough that this process defines the season.

PROCURE-TO-PAY

 1 DEMAND PLAN      doc: buy plan            state: intent only
       |            (pre-books + forecast + safety stock, by SKU by date)
       v
 2 PURCHASE ORDER   doc: PO to factory       state: qty ON ORDER
       |            (or CUT TICKET if you manufacture)   fwd ATS rises
       v
 3 ACKNOWLEDGMENT   doc: PO ack (855)        state: dates confirmed
       |                                     or revised (expect revisions)
       v
 4 PRODUCTION       doc: WIP updates         state: still on order
       |                                     milestone: the ex-factory date
       v
 5 INBOUND ASN      doc: ASN / packing list  state: on order -> IN TRANSIT
       |            + commercial invoice, bill of lading, customs docs
       v
 6 RECEIPT          doc: goods receipt       state: IN TRANSIT -> ON HAND
       |                                     GL: Dr Inventory / Cr GR-IR
       v
 7 PUT-AWAY         doc: putaway task        state: on hand, now LOCATED
       |                                     ATS rises here, not before
       v
 8 SUPPLIER INVOICE doc: vendor invoice      state: payable created
       |                                     GL: Dr GR-IR / Cr A/P
       v
 9 THREE-WAY MATCH  doc: match report        state: approved | on hold
       |            PO vs RECEIPT vs INVOICE  (qty, price, total)
       v
10 PAYMENT          doc: payment / remittance state: A/P cleared
                                              GL: Dr A/P / Cr Cash

The diagram reads the same way as the order-to-cash one: step, document, resulting state. Three abbreviations appear in it. WIP means work in progress — goods part-way through production. The ex-factory date is the day the goods are due to leave the factory gate. GR-IR is explained at step 6.

Steps 1 to 4: plan, order, acknowledge, produce

1. Demand plan. Someone decides what to buy. Inputs are confirmed pre-book orders, a forecast of at-once business, and a safety stock target — a deliberate buffer of extra units held to absorb late deliveries and demand you did not predict. Output is a quantity per SKU with a required in-stock date. This is MRP from section B.3, run on finished goods instead of components: explode the plan, net against what you already own and what is already inbound, offset backward by lead time.

2. Purchase order. The commitment to the factory: SKUs, quantities, unit costs, ex-factory date, and incoterms — the standard three-letter trade terms (FOB, DDP and the rest) that say exactly where the seller's responsibility ends, who pays the freight, and at what point you legally own the goods. From this moment those units count as on order, and forward ATS for future dates goes up. If the brand manufactures its own goods, the equivalent document is a cut ticket, and the supply comes from a production run rather than a vendor.

3–4. Acknowledgment and production. The factory confirms, usually with changes: quantities rounded to fabric minimums, dates slipped. Every change ripples into ATS and therefore into what sales may promise, so the PO's expected date must be a real, editable, audited field rather than a note. Production then takes weeks or months, and for most small brands it is a black box with occasional emails.

Steps 5 to 7: transit, receipt, put-away

5. Inbound ASN. The shipment leaves the factory. Goods are now in transit — you may already own them, depending on the incoterms, but you cannot ship them to a customer. Ocean freight from Asia to the United States commonly runs about two to three and a half weeks port-to-port to the West Coast and four to six weeks to the East Coast, with door-to-door times longer still (transit times as of 2025, and congestion moves them). That state can outlast a whole selling window, so it deserves its own inventory state.

6. Receipt. The container arrives and is counted. This is the only legitimate way stock enters the system. The entry is Dr Inventory / Cr GR-IR, where GR-IR is the "goods received / invoice received" clearing account — a temporary liability meaning "we have the goods, the bill has not arrived." Receipts are frequently short, over or damaged, and the receipt document records what actually came, not what the PO said.

7. Put-away. Stock moves from the receiving dock to a storage location. With bin-level tracking, ATS should rise at put-away rather than receipt, because goods sitting unsorted on a dock cannot be picked.

Steps 8 to 10: supplier invoice, three-way match, payment

8. Supplier invoice. The factory bills you: Dr GR-IR / Cr Accounts Payable, clearing the temporary account created at receipt.

9–10. Three-way match and payment. The three-way match is the control that prevents paying for goods you never got. The three documents are the purchase order (what was agreed), the goods receipt (what arrived) and the supplier invoice (what is billed). All three must agree on quantity, unit price and total before payment releases; any mismatch holds the invoice for a human. A two-way match, PO against invoice, is used for services where there is nothing to receive. Payment then posts Dr Accounts Payable / Cr Cash and the cycle closes.

The factory's price is only part of what a garment costs you

Landed cost adds freight, duty, customs brokerage and insurance on top of the factory invoice, and it is capitalized into inventory value — added to what the stock is worth on the balance sheet — rather than expensed separately. A $19.00 jacket can easily land at $24.50. If your margin reports use the factory invoice price, every margin you show the owner is overstated by roughly the amount of the profit.

B.9 The inventory lifecycle

Inventory is a set of states, and units move between them one event at a time. Getting these states right is the difference between an ERP and a counting app.

INVENTORY STATE MACHINE  (one SKU, one warehouse)

   ON ORDER          PO placed, factory has not shipped
      |              counts toward FORWARD ATS only
      v
   IN TRANSIT        shipped by factory, not yet received
      |              you may own it; you cannot sell from it today
      v
   RECEIVED  ------> ON HAND         physically here, unallocated
                        |            counts toward ATS in full
                        |
                        +--> RESERVED    held out (samples, QC, damage)
                        |                 NOT in ATS
                        |
                        +--> ALLOCATED   committed to an order
                                |         still on hand, NOT in ATS
                                v
                             PICKED       pulled from the bin, staged
                                |         still on hand
                                v
                             SHIPPED      gone. on_hand decreases.
                                |         COGS recognized.
                                v
                             RETURNED     comes back under an RMA
                                          -> inspect -> restock | scrap

   Sideways transitions that touch no order:
     CYCLE COUNT   ----> adjustment (+/-) when counted <> system
     ADJUSTMENT    ----> damage, theft, sample pulls, found stock
     TRANSFER      ----> warehouse A out, warehouse B in

Read the arrows as "a unit can move from here to there." The main line runs top to bottom: ordered from the factory, in transit, received, then either held back, or committed to a customer, picked, shipped, and occasionally returned. The three at the bottom are movements with no customer order behind them — a count correction, a damage write-off, or a move between warehouses. <> in the cycle-count line means "does not equal."

Note precisely where ATS changes. It rises at receipt (or put-away) and falls at allocation. It does not fall at shipment, because those units were already spoken for. Beginners consistently get this backwards, decrementing availability when the box leaves, which reintroduces the exact overselling bug the system was bought to fix.

Inventory as an append-only ledger

The correct way to store all of this is an append-only ledger, exactly like accounting. "Append-only" means you only ever add new rows; you never edit or delete an old one.

-- Inventory as an immutable event log. Balances are DERIVED.

create table inventory_transactions (
  id             bigserial primary key,
  tenant_id      uuid not null references tenants(id),
  variant_id     uuid not null references variants(id),
  warehouse_id   uuid not null references warehouses(id),
  txn_type       text not null,
  -- receipt | shipment | adjustment | transfer_in | transfer_out
  -- | return | cycle_count | allocation | deallocation
  qty_delta      integer not null,        -- signed: +50 or -12, never zero
  unit_cost      numeric(12,4),           -- landed cost at this event
  reference_type text,                    -- 'purchase_order','shipment'
  reference_id   uuid,                    -- the document that caused it
  reason_code    text,                    -- 'damaged','sample','miscount'
  occurred_at    timestamptz not null default now(),
  created_by     uuid references users(id),
  check (qty_delta <> 0)
);

create index on inventory_transactions
  (tenant_id, variant_id, warehouse_id);

-- The balance is a running total over the log, not a column
-- somebody keeps up to date by hand.
create view inventory_on_hand as
  select tenant_id, variant_id, warehouse_id, sum(qty_delta) as on_hand
  from   inventory_transactions
  where  txn_type in ('receipt','shipment','adjustment','transfer_in',
                      'transfer_out','return','cycle_count')
  group by tenant_id, variant_id, warehouse_id;

In plain English: the first block defines one table where every stock movement gets its own row. qty_delta is the change, positive for stock coming in and negative for stock going out, and the check rule forbids a row that changes nothing. reference_type and reference_id record which document caused the movement, so you can always trace a number back to a receipt or a shipment.

The create index line tells the database to keep a lookup structure so queries filtered by tenant, SKU and warehouse stay fast. The create view block defines a saved query, not a second copy of the data: it adds up all the deltas per SKU per warehouse and calls the result on_hand.

Every physical event appends one signed row. Nothing is updated in place; nothing is deleted. On-hand is the sum of the deltas. Three properties fall out for free:

  • a complete audit trail (for any balance you can list the exact events that produced it, with who and when);
  • time travel (filter by occurred_at for the balance as of any past date, which is what auditors and month-end close require);
  • and correctness when two things happen at once (two simultaneous receipts append two rows instead of racing to overwrite one counter).

The cost is that summing millions of rows on every page load is slow. The standard fix, developed in Chapter 5, is a stored inventory_balances table maintained by a database trigger or a scheduled refresh and treated strictly as a cache that can always be rebuilt from the log. Allocation appears in the type list but is deliberately excluded from the on-hand view, because allocating does not change how many units are in the building, only how many are free.

Store the events, derive the balance

The append-only transaction log is the most important structural idea in this book, and it is borrowed wholesale from double-entry bookkeeping, which was codified more than 500 years ago. Anywhere a quantity or a balance matters — inventory, receivables, payables — store the events and calculate the balance from them. Systems that store the balance and mutate it lose their history the first time something goes wrong, which is precisely when you need it.

Cycle counting and ABC analysis

Cycle counting keeps the log honest. The APICS Dictionary defines it as "an inventory accuracy audit technique where inventory is counted on a cyclic schedule rather than once a year." Most programs use ABC analysis, a Pareto split by value: A items, typically the 10–20% of items that account for 70–80% of inventory value, get counted weekly or monthly, B items quarterly, C items annually.

It beats the annual physical count on every dimension: it spreads labor across the year instead of shutting the warehouse for a weekend, and it catches errors close in time to their cause. Well-run programs sustain 98–99% inventory record accuracy.

Cycle counting is impossible under a periodic inventory system, which only updates the books at period end; it requires the perpetual approach where every movement updates records in real time. Your ERP is perpetual by construction.

B.10 Returns and RMAs

Goods come back. In wholesale they come back in cases, not ones, and often for reasons that are your fault.

  1. Request. The retailer asks to return goods — defective, wrong item shipped, overshipment, or a negotiated end-of-season return.
  2. Authorization. You issue an RMA — Return Merchandise Authorization — a number that identifies exactly which SKUs, in what quantities, from which original invoice, may come back and by when. Unauthorized returns can then be refused at the dock, which is the entire point of the number.
  3. Receipt and inspection. Goods arrive referencing the RMA. You count them and grade their condition.
  4. Disposition. Each unit is restocked as sellable, downgraded to a second, repaired, or scrapped. Restocking appends a positive inventory transaction; scrapping adds nothing back to sellable stock and instead posts a write-off.
  5. Credit memo. A negative invoice that reduces the customer's receivable balance. The entry is Dr Sales Returns and Allowances / Cr Accounts Receivable. Sales Returns and Allowances is a contra-revenue account, an account that offsets revenue rather than reducing it directly, so the original gross sales figure stays visible in the reports. If the goods were restocked, you also reverse the original cost: Dr Inventory / Cr COGS.

Returns are Phase 2 because a small brand can handle a handful a month by hand. They become urgent the moment the brand starts dropshipping to consumers, where roughly a quarter of units come back, online apparel return rates run about 23–26% as of 2025, and manual handling collapses.

B.11 The financial close, at altitude

You will not build a general ledger in version one. You still need to understand the close, because it constrains what your data must be able to prove.

Four definitions first. A journal entry is a dated record of a transaction with equal debits and credits. That equality is double-entry bookkeeping: every transaction is written twice, as a debit to one account and an equal credit to another, so the accounting equation, assets = liabilities + equity, always balances.

The chart of accounts is the structured list of buckets those debits and credits land in. The general ledger is the master record of every entry, organized by account. A subledger is a transaction-level ledger for one area — receivables, payables, inventory, fixed assets — that summarizes into a single control account in the GL. Your ERP is, in this vocabulary, a sophisticated set of subledgers.

The month-end close is the process of making a period's numbers final:

  1. Capture every transaction that belongs to the period. Nothing may be added afterward.
  2. Close and reconcile the subledgers. The AR subledger detail must sum exactly to the AR control account in the GL. Same for AP and inventory. A difference here means something posted to the ledger without a document behind it.
  3. Record accruals: costs already incurred but not yet invoiced, such as payroll earned but unpaid, plus depreciation.
  4. Reconcile accounts — bank statements, intercompany balances.
  5. Produce a trial balance — a list of every account with its ending balance — confirm total debits equal total credits, then generate the balance sheet, income statement and cash flow statement.

APQC benchmark data across more than 2,300 organizations puts the median cycle time from trial balance to consolidated financial statements at about 6.4 calendar days, with top-quartile performers under about 5 days and the bottom quartile taking 10 or more.

The engineering consequence is blunt: once a period is closed, its data must not change. If your app lets a user edit a shipment dated three months ago, you have silently changed a number that appears on financial statements already sent to a bank. That is why you need immutable transaction logs, period-lock flags, and corrections posted as new dated entries rather than edits. The append-only design in B.9 is there because accounting requires it, and a tidy architecture is the side effect.

B.12 The canonical entity map

Here is every core table a small apparel ERP needs. Read the diagram first, then the notes.

                            +-------------------+
                            |      TENANTS      |  one row per brand
                            +---------+---------+
                                      |
        every table below carries tenant_id -> tenants.id
        (the row-level security boundary; see Chapter 3)
                                      |
   +---------+-----------+------------+-----------+-------------+
   |         |           |            |           |             |
+--+----+ +--+-----+ +---+------+ +---+-------+ +-+---------+ +-+--------+
| USERS | | STYLES | |CUSTOMERS | |WAREHOUSES | |PRICE_LISTS| |SUPPLIERS |
+--+----+ +--+-----+ +---+------+ +---+-------+ +-+---------+ +-+--------+
   |       1 |         1 |          1 |           1 |            |
   |       N v         N v          N v           N v            |
+--+------+ +--------+ +-----------+ +---------+ +-------------+ |
|SALES_   | |VARIANTS| |SHIP_TO_   | |LOCATIONS| |PRICE_LIST_  | |
|REPS     | | (SKUs) | |LOCATIONS  | | (bins)  | |ITEMS        | |
+--+------+ +---+----+ +-----+-----+ +----+----+ +------+------+ |
   |            |            |            |             |        |
   |            |            |            |             |        |
   +------------+-----+------+------------+-------------+        |
                      |                                          |
              +-------+--------+                     +-----------+------+
              |  SALES_ORDERS  |                     | PURCHASE_ORDERS  |
              +-------+--------+                     +---------+--------+
                    1 |                                      1 |
                    N v                                      N v
           +--------------------+                  +--------------------+
           | SALES_ORDER_LINES  |                  | PURCHASE_ORDER_    |
           +--+--------------+--+                  | LINES              |
            1 |              | 1                   +---------+----------+
            N v              v N                             | N
      +-------------+  +--------------+                      v 1
      | ALLOCATIONS |  |SHIPMENT_LINES|              +----------------+
      +------+------+  +------+-------+              |    RECEIPTS    |
             |              N|                       +--------+-------+
             |               v 1                              |
             |         +-----------+                          |
             |         | SHIPMENTS |                          |
             |         +-----+-----+                          |
             |               | 1                              |
             |               v 1                              |
             |         +-----------+   1     N  +------------------+
             |         | INVOICES  |---------->| INVOICE_LINES     |
             |         +-----+-----+            +------------------+
             |               | 1
             |               v N
             |     +----------------------+   N        1  +----------+
             |     | PAYMENT_APPLICATIONS |------------->| PAYMENTS |
             |     +----------------------+               +----------+
             |               |
             |               v N
             |        +---------------+
             |        | CREDIT_MEMOS  | <---- RMAS <---- returns
             |        +---------------+
             |
             v  every physical movement appends to:
     +--------------------------+        +------------------------+
     | INVENTORY_TRANSACTIONS   |--fold-->| INVENTORY_BALANCES    |
     | (append-only event log)  |         | (derived cache)       |
     +--------------------------+         +------------------------+
              ^            ^
              |            |
          RECEIPTS     SHIPMENTS, ADJUSTMENTS, TRANSFERS, CYCLE_COUNTS

Each box is a table. A line between two boxes is a foreign key. The little 1 and N labels show how many rows sit on each end: 1 customer has N ship-to locations, 1 sales order has N order lines. "Fold" on the bottom line means adding the event rows up into a running total. Everything hangs off TENANTS at the top because every row in the system belongs to exactly one brand.

What each table is for

Now the same map in prose, table by table:

  • tenants — one row per brand on the platform. Every other table has a tenant_id pointing here and every query filters by it. This is the isolation boundary; getting it wrong leaks one brand's costs to another.
  • users — people who log in, with a role. sales_reps — the commercial identity a user may also have, carrying territory and commission rate. Keep them separate: an outside rep can exist without a login, and an admin has a login without being a rep.
  • styles — the design level: style number, name, category, season, fabric, MSRP, images. variants — the SKU level, one row per style + color + size, carrying the SKU code, the GTIN/UPC, the size sort position and the standard cost. Everything countable points at variants; everything merchandised points at styles.
  • price_lists and price_list_items — a named set of prices (US Wholesale, EU Wholesale, Outlet) and the per-variant price within it. A customer is assigned a price list; an order copies the price onto the line at entry, so later price changes never rewrite history.
  • customers — retailers who buy from you: payment terms, credit limit, factor, default price list, tax status. ship_to_locations — the addresses a customer receives at. One retailer can have one showroom and four hundred stores, so orders reference both the customer (who pays) and the ship-to (where it goes).
  • warehouses — places you hold stock, including third-party logistics providers and consignment locations. locations — bins inside a warehouse; optional until you outgrow one building.
  • sales_orders and sales_order_lines — demand, as sketched in B.7. allocations — the join between an order line and committed stock in a warehouse, with a quantity. Allocations need a table of their own rather than a column on the order line, because one line can be allocated across multiple warehouses and in multiple steps.
  • shipments and shipment_lines — what physically left, with carrier, tracking, BOL number and carton detail. Kept separate from orders because one order can ship in three parts and one shipment can consolidate two orders.
  • invoices and invoice_lines — the bill, generated from shipments. payments — money received. payment_applications — the many-to-many join saying which payment paid how much of which invoice. It exists because one check pays five invoices and one invoice gets paid by two checks; a paid true/false column cannot express that.
  • suppliers, purchase_orders, purchase_order_lines and receipts mirror all of the above on the supply side. inventory_transactions is the append-only ledger from B.9 and the source of truth for every quantity; inventory_balances is the derived, rebuildable cache that makes screens fast. rmas and credit_memos handle the return path.

One pattern runs through that list. Beginners model the nouns — products, orders, customers — then bolt on relationships as foreign keys. In an ERP the interesting tables are the joins: allocations, payment_applications, price_list_items, inventory_transactions. Each exists because a real-world relationship is many-to-many and carries its own quantity, date and history. If you find yourself adding a true/false column or a lone quantity to a parent row, ask whether you are collapsing a join table that should exist.

B.13 Who actually uses this, and what their day looks like

Five people use this software. They want incompatible things. Reconciling those five people is the hard part of building an ERP, and it will take more of your time than the code does.

The rep, customer service and the picker

The sales rep, at a trade show. Day two of market week, standing at a rack in New York's Javits Convention Center with a buyer from a twelve-door chain — "doors" is the trade word for stores, so twelve shops. She must enter twenty lines in four minutes without breaking eye contact, and know instantly whether February delivery is possible. The convention wifi is unusable.

She wants a size grid she can tab through, current and forward ATS visible without a click, and an app that does not lose the order when the connection drops. Accounting is somebody else's problem. What she cares about is not looking foolish in front of a buyer.

Customer service, entering an at-once order. A boutique calls: "Six of the olive jacket, mediums and larges, shipped today?" She needs the answer in under ten seconds and she needs it to be true. She lives in the ATS number, and she handles the fallout when it is wrong: apology calls, substitutions, partial shipments. Her deepest need is that the number on her screen matches the number in the building. Her second need is search that finds a customer from half a name.

The warehouse picker. A scanner, cold hands, and 140 orders to pull before the 3pm carrier cutoff. He wants a pick ticket sorted by bin location so he walks the aisle once, big legible type, and a scan that beeps when he grabs the wrong size. He must never see prices and never be asked to make a judgment call. If his tool is slow or ambiguous he will work around it on paper, and every number in the system will decay.

The controller and the owner

The controller, closing the month. Fourth business day. She needs the AR subledger to tie exactly to the general ledger, and to know that nothing dated last month can still change. She needs to explain a $687.81 deduction, which means tracing a payment back to an invoice back to a shipment back to a carton. She wants completeness and immutability, which makes her the natural enemy of every other user, all of whom want to fix mistakes by editing history.

The owner, looking at sell-through. He opens the app once a week, on a phone. He wants four numbers: what sold, what is sitting, what is coming, and whether payroll clears. Aggregates across seasons, styles and accounts — no line detail, no jargon. He is also the person who says "can we just add a column for…", and every such request threatens the schema.

Where the five users conflict

Put those five in one room and the conflicts are direct. The rep wants ATS optimistic; the controller wants it conservative. Customer service wants to edit an order after it ships; the controller needs that to be impossible. The owner wants a flexible report builder; the picker needs three buttons. The warehouse wants stock allocated the instant an order is entered; sales wants stock held back for the accounts that matter.

None of these people are wrong. An ERP is the negotiated settlement between them, written down as software, which is why the answer to nearly every design question ahead is "it depends who is asking," and why the good answer is to make the policy explicit rather than pick a side silently.

B.14 Where ERP projects fail

ERP has an unusually well-documented failure record. Learn it before you contribute to it.

Panorama Consulting's 2026 ERP Report surveyed 170 organizations between January 2025 and January 2026, with a median annual revenue of $200.5 million and a median project timeline of nine months. More than a quarter of those projects went over budget, and the report puts unplanned additional technology and expanded scope at the top of the reasons why. Panorama reruns the survey annually, so check the current edition before you quote a figure at anyone.

Budget and schedule are the easy failures to count. The harder question is whether the system delivered the benefits that justified buying it, which is why the report also asks respondents which promised benefits actually materialized after go-live. Read that section before you promise an owner that new software will fix their stock accuracy. Installing an ERP and fixing inventory are two different projects, and the second one is mostly about process discipline in the warehouse.

What follows is a series of case studies. They are not horror stories. Each contains a specific mechanical failure you can map onto a table or a process in the system you are about to build, and each ends with the design rule it produces.

One warning first. ERP failure stories are retold constantly and degrade every time, so where a famous figure turned out to be folklore this section says so. Levi Strauss is said to have taken a "$192.5 million charge" for its 2008 project; the string "192.5" appears in none of its relevant SEC filings. Hershey is said to have been unable to fill "$150 million of orders"; that number is its decline in quarterly sales. FoxMeyer's suit against SAP is said to have settled for $100 million; neither side disclosed an amount. Almost every figure below instead comes from a company's own SEC filing, a court document, a regulator's decision or a government inquiry, all free and searchable. A 10-K's risk factors and its "Management's Discussion and Analysis" section will tell you, in the company's own lawyer-checked words, exactly what broke.

FailureYearDamageWhat actually went wrong
FoxMeyer Drug1996$60M project by its own count; an $18.5M inventory write-down plus a $15.5M receivables provision; Chapter 11 in August 1996; sold to McKesson for $23M cash to the debtors plus about $500M of secured debt paid off; sued SAP and Andersen Consulting for $500M each, both settled for undisclosed sumsPriced huge new distribution contracts against savings the system had not delivered yet, then shipped it untested to hit those dates. Bad data conversion, faulty old-to-new interfaces, and a warehouse built by five vendors whose kit would not integrate.
Hershey1999$112M project; third-quarter sales fell 12.4% and net income 18.6%; inventory up 29%; delivery time went from 5 days to 12Order entry worked and factories kept running; the break was between the order and the loading dock. Phase two slipped three months out of the quiet season into the pre-Halloween surge, across three vendors' software at once.
Nike and i22000–2001Nike told the market the quarter's shoe sales were reduced by $80M to $100M; guidance cut from $0.50–0.55 to $0.34–0.38 per share; the stock fell 19.5% in a day; a shareholder suit settled for $8.9M, paid by insurersDemand planning customized so heavily to bridge to 27 legacy systems that one entry could take a minute to record. It dropped some orders and duplicated others, and wrong forecasts became factory commitments nine months deep.
Levi Strauss2008Shipping halted at all three US distribution centers for a week; Americas revenue fell 19.3% in the quarter; operating income down $67M; net income $1M against $46M a year earlierSingle global SAP instance went live in the US mid-quarter. The company pre-shipped to cover the cutover, then could not fulfill, and customers canceled once the system came back up.
Shane Co.2009Budgeted at $8–10M over a year; cost $36M over 32 months; Chapter 11 in January 2009, creditors ultimately repaid in fullWent live without accurate inventory counts, so the buying system stocked 23 stores heavily and with the wrong mix. That cash drain met the collapse in luxury retail with no slack left.
Target Canada2013–2015About US$7.5B of cumulative pre-tax losses; US$5.4B of pre-tax losses on discontinued operations in one quarter; 133 stores closed and about 17,600 jobs lost within two years of openingMaster data. An internal team estimated item data was accurate about 30% of the time against 98–99% in the US business, with no input validation until 2014. Wrong dimensions, case packs and duty codes broke shipping, receiving and shelf replenishment at once.
Avon2013$117.2M pre-tax non-cash impairment ($74.1M after tax); global rollout halted after the Canadian pilot; about 650 positions cutThe technology worked. The independent representatives who used it every campaign found the new ordering process so much worse than the old one that they left.
Lidl2011–2018About €500M, reported from industry estimates, never confirmed by Lidl and publicly disputed by SAP; seven years abandoned and the legacy system retainedLidl valued stock at retail price; SAP for Retail values it at purchase price by default. Lidl refused to change, and the resulting modifications made the system slower and more fragile with every addition.
Revlon2018–2019About $64M of net sales it could not ship in 2018 plus $53.6M of charges; a disclosed material weakness in internal control over financial reporting and an adverse audit opinion; shares fell 6.9% on the disclosureA single-site go-live at the plant making a substantial share of US product disrupted manufacturing and fulfillment. Nobody detected the control failure until the following year's audit.
Haribo2018Press reporting put German Gold Bear unit sales down about 25% in 2018, dragging total volumes down about 10%; delivery rates near 80% against a 100% target. Haribo published no figuresAn S/4HANA changeover across all sites from October, at the top of the Christmas confectionery season. The software was named as the most important of several causes, not the only one.
Birmingham City Council2022 onwardBudgeted at £19M plus £1M contingency; forecast at £144.4M through 2027/28, total losses put near £216.5M; over £5M on manual workarounds; a Section 114 notice in September 2023Planned to run Oracle out of the box, then customized it back to the old processes, including a bespoke bank reconciliation module that never worked. Could not reconcile its accounts or produce auditable statements, and ran 18 months with fraud auditing off.
Hewlett-Packard2004$160M impact from a $120M order backlog and $40M of lost revenue, against a project analysts estimated at $30M; three executives leftOrders stalled between the legacy order-entry system and SAP. The contingency plan covered three weeks against six weeks of actual disruption. It was the 35th such consolidation; the previous 34 worked.
Vodafone UK2013–2015Ofcom fined Vodafone £4.625M, £3.7M of it for the billing failure; 10,452 customers lost credit they had paid for, refunded at an average of £14.35Migrating 28.5 million accounts off seven legacy platforms. Top-ups on long-dormant accounts took the money and confirmed the credit while leaving the account flagged as disconnected. Under 0.01% of accounts hit that path, so it hid for 16 months.
US Air Force ECSS2005–2012Over $1 billion spent, cancelled in November 2012 with less than $150M of usable capability; finishing it would have cost about $1.1B more for a quarter of the scopeBought commercial software to get commercial practice, then, when the integrator proposed changing Air Force processes to match, refused and had the software rewritten to match the processes.

FoxMeyer Drug, 1996: pricing contracts against savings you have not delivered

FoxMeyer bought pharmaceuticals in bulk and delivered them to pharmacies and hospitals, usually within 12 to 24 hours of the order. Its own annual report calls it the fourth largest wholesale drug distributor in the United States: $5.49 billion of sales in the year to March 1996, 23 distribution centers, 2,447 employees. Drug distribution runs on thin margins, and FoxMeyer's gross margin that year was 4.2%, down from 5.5% two years earlier.

That margin is the story. In July 1994 FoxMeyer won a contract to supply 67 academic medical centers, expected to add more than $700 million of sales, and opened seven distribution centers to service it. The contract was won on price, and the price assumed a new computer system would take roughly $40 million a year out of operating costs. The court opinion in the shareholder suit sets out the mechanism: FoxMeyer was counting on those savings to support low bids, and was then forced to implement the new systems before completing testing on them in order to meet its contractual obligations. Sales rose $681 million that year while gross profit fell $33 million. The project, Delta, was budgeted at $60 million, replacing a mainframe estate with SAP R/3, with Andersen Consulting as integrator.

The famous claim is that R/3 handled 10,000 orders a night against 420,000 on the old system. Trace it and it wobbles: contemporaneous reporting of the complaint says invoice lines, one account says transactions, another says orders, and the researcher who popularized it wrote it two different ways in two papers. It is a plaintiff's allegation in a 1998 filing, not a measurement. The defects that are documented are duller and more useful to you: data conversion performed with incorrect drug product codes, and faulty interfaces between old and new systems. At bankruptcy R/3 was live in six of 23 warehouses, and trade coverage a year later reports the system started on time and orders were filled, but widespread data errors corrupted the sales history, so the forecasting benefit that justified the project never arrived.

FoxMeyer's own description of the damage is the best sentence in the file. A fourth-quarter charge of $18.5 million to cost of goods sold arose because the systems problems "resulted in the Corporation shipping more product to some of its customers than it billed, and inflated the amount of credit given to certain of its customers. The ability of the Corporation to track certain shipments was significantly impaired as well." And then: "The magnitude of the problem was not identified until subsequent to year end." A further $15.5 million went into the allowance for doubtful receivables. Those two numbers sum to exactly $34.0 million, very likely the origin of the widely repeated "$34 million of lost inventory," only part of which was inventory.

FoxMeyer filed for Chapter 11 in August 1996. Chapter 11 is the section of the US bankruptcy code that lets a company keep trading while it reorganizes its debts; Chapter 7 is liquidation, and FoxMeyer converted to that in March 1997. McKesson bought the business at auction for about $23 million cash to the debtors plus roughly $500 million of secured debt paid off.

The lesson for your ERP. Never let a commercial promise depend on a system that is not yet in production. If the owner wants to quote a lower price to a chain on the strength of savings your software will produce, those savings arrive after the system is proven, not before. And note where FoxMeyer actually bled: bad reference data and bad interfaces between old and new. That is Chapter 7 on data import and migration and Chapter 4 on integrations and reconciliation, not an exotic scalability problem.

Hershey, 1999: the calendar is part of the design

Hershey Foods was a roughly $4 billion confectionery business. Its program, Enterprise 21, cost $112 million, a figure you can derive exactly from the annual report: $98.8 million of capitalized software and hardware plus $13.2 million of expense. The stack was SAP R/3 for finance, purchasing, materials management, warehousing, order processing and billing, Manugistics for production forecasting and Siebel for promotions, with IBM as lead consultant. Phase one went live in January 1999 and was uneventful. Phase two, covering sales order entry and billing, transportation planning, EDI with warehouses, finished goods inventory and receivables, went live on July 5, 1999.

Two things about that date deserve more attention than they get. Phase two was originally aimed at April and slipped three months. April is the confectionery off-season; July is when retailers order for back-to-school and Halloween. The date was not chosen badly so much as allowed to drift into the worst window on the calendar. And Hershey knew the cutover was risky, so it ran what its filing calls "a controlled advance purchase program in June intended to reduce disruptions during the final phase of the implementation." It pushed inventory to customers ahead of the switch, which helped the second quarter, emptied the pipeline, and made the third quarter worse.

What broke is precise. Order entry worked and factories kept running. As trade coverage put it, the chocolates were piling up in warehouses instead of sitting on store shelves. The break was between the order and the loading dock, in what the filing calls "order fulfillment (customer service, warehousing, and shipping)." The chief executive said it in a line: "While order patterns have remained strong, we have been unable to fill them completely, in a timely fashion." A Hershey distribution executive later gave the concrete version: the company had historically absorbed peak output by stashing product wherever space existed, in rented space and unused rooms in factories, locations never modeled as storage in the new system. An availability check looked at the official locations, found nothing, and could not promise stock that physically existed a few miles away.

Third-quarter sales fell 12.4%, a decline of $150.5 million, and net income fell 18.6%. Delivery time went from five days to twelve, inventory rose 29%, and receivables rose $57.1 million as customers paid late and deducted penalties. The widely repeated "$150 million of orders Hershey could not fill" is that sales decline, relabeled somewhere along the chain of retellings. Hershey then fixed it: sales rose 6% the following year, and in 2002 it upgraded the same system in eleven months, about 20% under budget, with no disruption at all.

Pick the go-live date from the business calendar, not the project plan

Every business has a season. For a wholesale apparel brand it is market week and the two big ship windows. In the first week of the project, write down the three worst weeks of the year to break the system and treat them as no-go periods the schedule bends around rather than through. When the project slips, and it will, the correct response is to slip to the next safe window, not the next available Monday. Hershey's phase two was three months late, which moved it from the quietest month of its year to the busiest.

The lesson for your ERP. Beyond the date, note the mechanism: the system could not promise stock it did not know about. Chapter 8 builds available-to-sell as a derived, rebuildable figure precisely so that a location or a state you forgot to model shows up as a reconciliation difference rather than a silent stockout.

Nike and i2, 2000–2001: customizing until it is a different program

Nike's revenue was about $9 billion, footwear around 57% of it. The business runs on Futures, in which retailers order roughly 90% of sneakers six months ahead in exchange for guaranteed delivery and fixed pricing, against a nine-month manufacturing lead time. By 1998 Nike had 27 order management systems worldwide, all customized and poorly linked. The program that followed was projected at $400 million and reported at $500 million by 2004: SAP R/3 as the base, i2 for demand and supply planning, Siebel for CRM. The i2 piece was about $10 million of that.

The decision that mattered was sequencing. Rather than deploy i2 as part of the SAP program, Nike installed it from 1999 while still running the legacy systems, which meant bridging a modern planning tool to 27 old ones. Court documents from the shareholder suits describe the result: the demand application and the supply chain planner used different business rules and stored data in different formats; the software had to be customized so heavily that a single entry could take as much as a minute to record; the system crashed frequently under the volume of Nike's product numbers; and it ignored some orders, duplicated others, and deleted ordering data six to eight weeks after entry, so planners could not recall what they had asked each factory to make.

The usual telling is that the system ordered thousands too many Air Garnetts and thousands too few Air Jordans. The shoe names come from a single 2004 feature; Nike's filings name no shoe. What the filings confirm, in Nike's own words three years later, is both halves of the mechanism: "Relatively higher air freight costs were incurred in fiscal 2001 due to supply chain system problems," and unusually high close-out sales in the second half of that year for the same reason. Air freight is what you pay when the thing you needed was not made. Close-outs are what you do with the thing you made and did not need.

On February 26, 2001 Nike cut quarterly guidance from $0.50–0.55 per share to $0.34–0.38, blaming "complications arising from the impact of implementing our new demand and supply planning systems and processes which resulted in product shortages and excesses as well as late deliveries." The release never names i2 and never quantifies the loss; Nike told reporters shoe sales were reduced by $80 million to $100 million. The stock fell 19.5% the next day, the biggest decliner in the S&P 500, and four class actions were consolidated in Oregon and settled for $8.9 million funded by insurers. Nike then moved sneaker planning into SAP, and that rollout succeeded: 6,350 users by the end of 2002, no disruption, and customer service representatives locked out of the system until they had completed 140 to 180 hours of training.

The lesson for your ERP. Every hour spent bending software to fit a legacy system builds something nobody supports. When you must run new and old side by side, keep the bridge narrow, explicit and reconciled, which is what Chapter 4 is for. And the specific defects here, duplicated orders and silently dropped ones, are the failure modes Chapter 3 addresses with idempotency keys and proper concurrency control. A planning system that sometimes writes an order twice does not have a planning problem.

Levi Strauss, 2008: the week the trucks stopped

This is the case closest to the business you are building for. Levi Strauss is a wholesale apparel brand: it sells jeans and Dockers to department stores and specialty retailers, ships from distribution centers, invoices against those shipments, and gets paid net 30 to 60 with deductions. Sections B.7 and B.9 describe its business. In the year in question it did $4.4 billion of net revenue. It was standardizing on a single global instance of SAP, a five-year program covering about 5,000 users, with Deloitte Consulting as systems integrator. The United States went live at the beginning of the second quarter of fiscal 2008.

Read what the company told the SEC, because it is unusually blunt for a filing. From the risk factors: "we implemented an enterprise resource planning ('ERP') system in the United States in the second quarter of 2008. Due to issues encountered during the stabilization of the system, we temporarily suspended shipments to our customers in the United States in the beginning of the second quarter of 2008, resulting in decreased revenues and increased administration expenses." A company with a fifty-state wholesale business told its investors that it stopped shipping. Contemporaneous reporting adds the detail the filing leaves out: Levi's confirmed it had taken the shipping systems at its three US distribution centers offline for a full week in April to fix problems receiving and fulfilling orders, and that it lost business twice, once during the shutdown and again when customers who had already ordered canceled after the systems came back.

Then the part that should make you uncomfortable, because it is the move Hershey made. From the same filing: "In anticipation of that implementation, we provided advance shipments to wholesale customers in the first quarter of 2008 that would normally have been shipped in the second quarter." Levi's pushed goods out early to build a buffer around the cutover. That buffer flattered the first quarter, emptied the second, and then the outage hit the emptied quarter anyway.

The numbers. Americas net revenues fell from $591.1 million to $477.3 million, down 19.3%. Consolidated net revenues fell 7.9% and operating income fell $67 million. Net income was $1 million against $46 million a year earlier, a drop of about 98%. Stabilization was substantially complete by November 30, 2008.

Now the folklore, because this case is where it is thickest. Levi Strauss is cited across dozens of articles, including a well-known 2011 Harvard Business Review piece, as having taken a "$192.5 million charge against earnings" for this project. A charge against earnings is a one-time cost booked to the income statement, a specific and identifiable accounting event. There was no such charge, and the figure appears in none of the three relevant filings. Its apparent origin is a July 2008 trade article that described $192.5 million as the combined cost of taking the distribution centers offline and the company's global retail expansion, and which noted that half the relevant increase was currency movement. A blended operating expense number became, through repetition, a fictitious write-off. The documented damage is large enough without it.

One further distinction worth holding onto. Levi's disclosed that the implementation "materially affected our system of internal control over financial reporting," but it did not report a material weakness and it did not restate. Revlon, a decade later, did.

Pre-shipping before a cutover is not a safety net

Hershey did it in June 1999. Levi Strauss did it in early 2008. Both pushed inventory to customers ahead of the switch to buy themselves room, and both made the damage worse, because it borrowed sales from exactly the period that then went wrong and hid the true demand signal at the moment they most needed to see it. If you are tempted to build a buffer before a go-live, build it in time instead: go live in the quiet window, keep the old system readable, and make sure you can still answer "what do we owe this customer and what did we promise them" from a source that is not the new system.

The lesson for your ERP. The one output that matters on day one of a wholesale system is a correct, shippable pick ticket. Every report and every screen can degrade for a week and the business survives; if shipping stops, the business stops. Build the fulfillment path first, test it hardest, and keep a documented manual fallback in the runbook of Chapter S: how to pick, pack, ship and invoice fifty orders with the application down. Levi Strauss lost a week and roughly a fifth of a region's quarterly revenue because that path had no fallback.

Shane Co., 2009: the small-company version

Every other case here involves a company with a corporate treasury. Shane Co. is the one you can see yourself in. It was a family jewelry retailer founded in 1971 by Tom Shane, whose flat voice on radio advertisements promised listeners "now you have a friend in the diamond business." At bankruptcy it ran 23 stores in 14 states, on revenue of $275 million in 2007 falling to an expected $207–210 million in 2008.

In 2005 it contracted with SAP for what its bankruptcy filing calls a "highly sophisticated 'point of sale' and inventory management system," projected at $8–10 million over about a year. It cost $36 million and took 32 months: four times the budget and three times the schedule, and these figures come from the debtor's own court filing rather than from a journalist.

The mechanical failure is one sentence in that filing and it should be pinned above your desk. At go-live the system "did not yet provide accurate inventory count numbers," and as a result the stores became "substantially overstocked with inventory, and with the wrong mix of inventory." That is the whole chain. A jewelry retailer's buying is driven by what the system says is on hand; if the counts are wrong the buying is wrong, and for a business whose inventory is diamonds, wrong buying converts cash into slow-moving stock faster than anything else can offset.

Shane Co. filed for Chapter 11 on January 12, 2009. Be careful about causation, because the company was: its own filing names as the major factor a "precipitous decline in retail sales, particularly in luxury goods." That is a recession, not a software failure. The honest reading is that a project which consumed roughly $26 million of unbudgeted cash and filled the stores with the wrong inventory removed all the margin for error, and then the worst luxury retail year in decades arrived. Shane Co. emerged from Chapter 11 in December 2010 with creditors repaid in full.

The lesson for your ERP. This is the failure mode you are most likely to cause. A four-person apparel brand has no $26 million to lose, but it has the same dependency: the owner buys next season based on what your screens say about this one. If your on-hand numbers are wrong you are not producing a reporting bug, you are producing a purchasing decision. That is why Chapter 1 builds inventory as an append-only ledger with balances derived from it rather than a mutable counter, and why Chapter 9 puts invariants on those balances and tests them.

Target Canada, 2013–2015: what bad master data actually does

Master data is the reference data that documents point at instead of copying: products, customers, warehouses, as defined in section B.2. Target Canada is the clearest demonstration in the record of what happens when it is wrong.

Target Corporation was a US$70 billion retailer with 1,763 stores that had never operated a store outside the United States. In January 2011 it agreed to buy the leasehold interests in up to 220 Canadian sites from Zellers for C$1,825,000,000, a figure readable in the contract filed with the SEC. It opened 124 stores during 2013, 33 of them in a single ten-day burst in November, and built three distribution centers totaling about four million square feet from scratch in under two years. A former employee's summary: "From the very beginning, there was a clock that was ticking. And that clock was absurd." Target's US systems were custom-built over decades and could not handle Canadian dollars or French-language characters, so Target Canada bought off the shelf: SAP for the item master, JDA for forecasting, Manhattan for warehouse management, with Accenture as lead consultant.

Here is the reasoning error at the center of the case, and it is a smart person's error. Target looked at other retailers' SAP failures, correctly concluded their problems came from converting data out of existing systems, and concluded it was safe because it was starting fresh: there was no data to convert, only new information to input. Greenfield entry was treated as a mitigation. It was the same risk with the safety rail removed, because a conversion at least has an old system to reconcile against and a blank database has nothing.

The task was to enter item data for roughly 75,000 products against a fixed schedule, with dozens of fields each: manufacturer, model, UPC, dimensions, weight, how many fit into a case for shipping. The work fell largely to young merchandising assistants who took the values from vendors, and vendor data is notoriously unreliable, and they were not experienced enough to challenge it. One of them later said: "There was never any talk about accuracy. You had these people we hired, straight out of school, pressured to do this insane amount of data entry, and nobody told them it had to be right." Then the sentence that should stop you cold, because it is a pure engineering decision: the company had not built a safety net into SAP, so the system could not notify users about data entry errors. Automatic verification, the kind that rejects a UPC one digit short, was not implemented until 2014, roughly 18 months into live operation. An internal team estimated the information in the system was accurate about 30% of the time. In the US business the equivalent figure was 98% to 99%.

Trace the consequences, because your system can produce every one of them. Dimensions were entered in inches instead of centimeters, or in the wrong order, so product did not fit in shipping containers and did not fit on store shelves. Tariff codes were missing, so goods sat at customs. Case pack quantities were wrong, and this is the cleanest worked example in the literature: someone might order 1,000 toothbrushes and record that they would arrive as 10 boxes of 100, when the shipment was actually four boxes of 250. That shipment then did not exist as far as the distribution center's software was concerned, could not be processed, and was set aside in an area designated for problems. Warehouse workers got desperate enough that they would slice open a crate meant to hold a dozen boxes of paper towels but holding ten, stuff in two more, tape it shut and send it on. Shelf replenishment failed for the same reason, because the auto-replenishment logic needed exact product and shelf dimensions; the president switched it off across the chain and had staff walk the floor by hand.

The result was empty shelves and full warehouses at once, the distribution centers overflowing by the fall of 2013 with tractor-trailers idling in the yards. The over-ordering had a separate cause worth naming: JDA needs years of history to forecast and there was none, so buying was done against optimistic projections derived from mature US stores. Target's own annual report ties the data problem to the profit and loss in one line, attributing a gross margin rate of 14.9% to "efforts to clear excess inventory following lower than anticipated sales and supply chain start-up challenges." The Canadian segment lost US$122 million in 2011, US$369 million in 2012 and US$941 million in 2013 at the operating line. On January 15, 2015 Target Canada filed under the Companies' Creditors Arrangement Act, Canada's equivalent of Chapter 11, with 133 stores and about 17,600 employees, and Target announced approximately US$5.4 billion of pre-tax losses on discontinued operations that quarter.

The most painful detail comes last. By the second half of 2014 the supply chain was largely fixed and employees felt they were finally running a normal retailer. The exit was announced weeks later. The data problem was survivable in itself; what made it fatal is that it consumed the eighteen months in which the brand had to earn a second visit from Canadian shoppers.

Validation at the point of entry is not a nice-to-have

Target Canada ran a national retailer for about eighteen months on an item master with no input validation, and an internal estimate put accuracy near 30%. Your system has the same exposure the first time someone bulk-imports a season. Every field that downstream logic depends on needs a constraint at write time: sizes from an ordered lookup table and never free text, a check digit verified on every GTIN, dimensions carrying an explicit unit, case pack quantities that must divide evenly into the ordered quantity, a required country of origin. Rejecting a bad row on import is cheap. Finding it in a container at customs is not.

The lesson for your ERP. Chapter 7 treats data import as a first-class feature with validation, a rejection report and a reconciliation step, rather than a script someone runs once. Do the same on a greenfield build: the absence of a legacy system to reconcile against makes the problem harder, not easier, so invent the reconciliation by counting a physical sample of stock against what the system claims. And Chapter 8's available-to-sell cache is exactly the mechanism that let Target's merchandisers believe stock existed when it did not. A cache that cannot be rebuilt and reconciled against the underlying events is a machine for producing confident wrong answers.

Avon, 2013: the users decide, not the architecture

Avon sold cosmetics through direct selling, meaning more than six million active independent representatives, contractors rather than employees, placed orders every campaign cycle and resold to their own customers. Revenue was about $10 billion across 62 countries. Its own annual report names the dependency: success is highly dependent on recruiting, retaining and servicing representatives, and turnover among them is already high. Order management at Avon was not a back-office system. It was the entire commercial interface with six million small businesses. The program, begun in 2009 and called Service Model Transformation, was piloted in Canada during 2013.

Nothing crashed. That is the point of this case. The chief executive's own summary is the most useful sentence any technology leader has said about adoption: "While the pilot technology platform [in Canada] worked well, the degree of impact or change in the daily processes to the Representative was significant. This resulted in a steep drop in the active representative count." The quarterly release reported North America revenue down 19% and active representatives down 16%, attributing the Canadian damage to "significant field disruptions" from the pilot. Trade reporting from representatives adds texture: trouble logging in, orders not saving reliably, inventory that was supposed to be reserved in real time and was not, and extra steps in a workflow they ran every campaign.

On December 9, 2013 Avon halted the global rollout, citing "the potential risk of further disruption" and adding, plainly, that "SMT did not show a clear return on investment." It recorded "a pre-tax non-cash impairment charge of $117.2 ($74.1 net of tax), reflecting the write-down of capitalized software." An impairment is the accounting recognition that an asset on the balance sheet is no longer worth what you paid for it. The commonly cited $125 million is a headline figure, not a filing figure.

The lesson for your ERP. Your users are the five people in section B.13, and at least two of them can simply refuse. A rep at a trade show who finds your order screen slower than the paper form will use the paper form, and then your data is wrong for reasons unrelated to your code. Measure the new workflow against the old one in keystrokes and seconds before you ship it, not after. Chapter N treats adoption and change management as engineering work with its own deliverables, because a system nobody uses has a defect rate of 100%.

Lidl, 2011–2018: refusing to change one thing, for seven years

Lidl is a German discount grocer, part of the Schwarz Group, running thousands of stores across Europe. In 2011 it began a program called eLWIS to replace its in-house merchandise management system, known internally as Wawi: around 90 modules on an old development environment, joined by more than 50 interfaces. eLWIS was to handle master data for more than 10,000 stores and 140 logistics centers, and Lidl put more than 1,000 employees and a three-digit number of consultants on it.

The blocking issue was inventory valuation, and it is worth getting the direction right because almost every English-language retelling has it backwards. Lidl valued its stock at retail price. SAP for Retail values stock at purchase price by default. The German reporting is explicit that this made Lidl an outlier among its competitors rather than a follower of some discount-grocery norm. Lidl would not change. As the German business press described the consequence, the mismatch led to elaborate modifications, for which the consultants diligently logged hours, and as such modifications accumulate the software becomes ever more complex and error-prone, performance drops, and costs rise to uncontrollable levels. A board member of the German-speaking SAP user group gave the analogy: standard software is like a prefabricated house, where you can choose the room layout or where the sockets go, but changing something fundamental is like tearing out a load-bearing wall.

It was also a study in nobody being able to stop. The head of IT systems was forced out in May 2017, and a person close to the project said that from then on it was only about how to get out of the whole story with as little loss of face as possible. A consultant described what forms in these projects: communities of fate between project owners and consultants, who close their eyes to the mounting problems so neither has to admit failure to the other. In April 2017, while this was going on, SAP gave Lidl a quality award at a ceremony in Walldorf.

In July 2018 Lidl stopped, seven years in, having deployed to Austria, Northern Ireland and the United States. The chief executive told staff the originally defined strategic goals were not achievable with justifiable effort and that on a cost-benefit basis everything favored continuing to develop the legacy system. The cost, about €500 million, was first reported by Lebensmittel Zeitung from industry estimates; Lidl declined to confirm it and SAP called it pure speculation and in no way comprehensible. The contrast that is solid: the same SAP retail package had been running at Aldi Nord, a direct competitor with the same business model, since 2016.

The lesson for your ERP. Section B.13 ends by calling an ERP a negotiated settlement written down as software. Lidl refused to negotiate on one accounting convention and paid for seven years of custom development to preserve it. For every deviation a customer asks you to build, ask whether it is a competitive advantage or a habit. Costing method, invoice numbering format and the order of fields on a screen are habits. If you cannot say what a deviation earns the business, change the business.

Revlon, 2018–2019: when the auditors find it before you do

Revlon is a cosmetics company that bought Elizabeth Arden in September 2016 for a total purchase price its accounts put at $1,034.3 million, and by the end of 2018 carried $3,142.5 million of debt against $2,564.5 million of net sales. It had been implementing a company-wide SAP system since at least 2014 and launched it in the United States in February 2018. The go-live was at Oxford, North Carolina, and Revlon's annual report carries a standing risk factor headed "The Company depends on its Oxford, North Carolina facility for production of a substantial portion of its products." Concentrating a cutover on the one plant that makes most of your product has an obvious failure mode, and it happened.

Revlon's own words: the launch "impacted the Company's ability to manufacture certain quantities of finished goods and fulfill shipments to retail customers in the U.S. and internationally and caused the Company to incur substantial costs during 2018 to remediate the decline in customer service levels." It quantifies the damage precisely: "The Company estimates that this ERP launch resulted in the Company being unable to fulfill product shipments representing approximately $64 million of net sales during 2018 and incurring $53.6 million of incremental charges in 2018." Note what the $64 million is, since it is often misdescribed: sales the company could not ship across the full year, not a charge. There is also a quieter second-order effect. Because Revlon could not ship, it could not invoice, so its receivables fell, and its borrowing capacity under its revolving credit facility was calculated from those receivables. A fulfillment outage tightened the company's access to cash.

The most instructive part is the sequence. In each of the three 2018 quarterly reports Revlon stated that the implementation "has not materially affected, nor is it reasonably likely to materially affect, the Company's ICFR," meaning internal control over financial reporting. Then, on March 18, 2019, it filed a notification that it could not file its annual report on time, "primarily related to the lack of design and maintenance of effective controls in connection with the previously-disclosed implementation of its enterprise resource planning ('ERP') system in the U.S."

A material weakness is a formal finding that a company's controls over its financial reporting have a deficiency serious enough that a material misstatement could occur without being caught. It is not an accusation of fraud, and Revlon's accounts were not misstated. It is a statement that the machinery meant to catch errors was not working. The disclosure named three causes: no effective risk assessment for the controls affected by the implementation; too few trained personnel who understood and were accountable for their control responsibilities; and, resulting from those, a failure to consistently operate the process-level controls that record inventory, receivables, net sales and cost of goods sold and reconcile the balance sheet. The auditor issued an adverse opinion on internal control, and the weakness was identified only after year end, which is to say nobody noticed during the year it happened.

Shares fell 6.87% on the notification and a further 6.4% when the annual report landed. A securities class action was filed and dismissed in September 2020, and the shareholders recovered nothing. One correction, since it is repeated everywhere: Revlon filed for Chapter 11 in June 2022, and this failure did not cause it. The company's own first-day bankruptcy declaration attributes the filing to declining demand, the pandemic, supply chain shortages, liquidity constraints and a dispute among its lenders, and mentions neither SAP nor ERP anywhere.

The lesson for your ERP. A material weakness is what happens when a system change silently removes a control a human used to perform. Every time you automate a check, write down which manual control it replaced and how you will prove it is running. Chapter 9 does this with invariants asserted continuously rather than reviewed annually: the receivables sub-ledger ties to the control account, allocations never exceed on-hand, no shipment exists without an inventory transaction. Revlon's problem was not that its numbers were wrong. It was that it could not demonstrate they were right.

Haribo, 2018: going live at the top of the season

Haribo makes Gold Bears, the gummy candy with 99% brand awareness in Germany and around 100 million units produced a day. It is privately held, employs about 7,000 people, and publishes no financial figures. In October 2018 it began converting its sites worldwide from a merchandise management system partly dating to the 1980s onto SAP S/4HANA. A spokesperson said the step had no alternative.

October to December is the run-up to Christmas, the busiest confectionery season in the German calendar. By mid-December the trade press was reporting greater delivery difficulties than expected, affecting practically all products, and supermarket managers complaining about missing deliveries. Süddeutsche Zeitung gives the only concrete mechanism in the record: the delivery rate ran around 80% against a 100% target, and specifically, when the bears were mixed with other fruit gum varieties, not all of them made it into the delivery vans. That points at order fulfillment and mixed-case picking rather than at production, which was fine.

Be careful with the headline number. The repeated "25%" is that newspaper's own reporting that Haribo would sell about a quarter fewer Gold Bears by volume in Germany in 2018 than 2017. Haribo published nothing. And the paper is explicit about causation in a way the retellings are not: there are several reasons for the decline, of which the software changeover is the most important, alongside demand that was already falling, competitive pressure, a documentary about the sourcing of gelatin, and a packaging problem. Several English-language accounts assert that warehouse management malfunctioned and replenishment flows broke; none cite a source.

The lesson for your ERP. The interesting detail is the picking failure: single-variety orders were fine and mixed ones were not. That is precisely the class of bug that survives testing, because test fixtures tend to contain one of everything while real orders contain awkward combinations. Chapter 9 covers building test data that looks like production, including the prepack and mixed-carton cases from section B.5.1.

Birmingham City Council, 2022 onward: the same mistakes, happening now

Every case so far is history. This one is live, and it is useful because a large, well-advised organization made the same choices in the 2020s that FoxMeyer made in the 1990s. Birmingham City Council is routinely described as the largest local authority in Europe by population, serving about 1.1 million people with annual expenditure between £3.2 billion and £4 billion. It set out to replace a legacy SAP system with Oracle, on a budget of £19 million plus £1 million of contingency, and went live in April 2022 after two replanned dates.

The council had planned to implement Oracle out of the box. It then created several customizations, including a bespoke banking reconciliation system that failed to function properly. Government-appointed commissioners later recorded that the council had "acknowledged the need to re-implement the core Oracle system and adopt the standard processes/configurations offered and not, as previously done, adapt the system to legacy ways of working," and that the council "is fixed in its traditional ways of working, which was one of the key factors that caused the Oracle implementation to fail in 2022."

The damage is the kind an accountant recognizes instantly. Posting errors in the bespoke bank reconciliation module were identified within weeks of go-live and it never worked; a third-party product was eventually bought to do the job. The council struggled to understand its own cash position and could not produce auditable accounts. Around £2 billion of transactions were allocated to the wrong year. Fraud detection auditing was switched off for more than 18 months, so the council could not say whether financial fraud had occurred. Over £5 million went on manual workaround labor, and external auditors went years without receiving financial statements because of problems with the ledger implementation.

The auditors' language is unusually direct for a public document. The failed implementation "has fundamentally impacted the Council's financial management and its operations," and the council "failed to fulfil its duty to deliver best value." The solution design "was not fully resolved when the system went live," and the council authorized go-live when there were known deficiencies in it. Officers' understanding was limited, so the relevant directorate lacked the Oracle knowledge to act as an "intelligent customer" able to critique the supplier's work, and leadership did not want to hear bad news. The commissioners' own verdict: "the Oracle implementation in April 2022 is the poorest ERP deployment Commissioners have seen."

The cost went from £19 million plus contingency to a forecast of £144.4 million through 2027/28, with total losses put near £216.5 million. In September 2023 the council issued a Section 114 notice, the statement a local authority's finance officer makes when it cannot balance its budget, which is the closest thing English local government has to declaring insolvency. Be precise about causation, because the auditors were: they concluded the Oracle failure was a contributory factor rather than the sole causal factor, the dominant liability being equal pay claims of up to £760 million. What the software contributed, beyond its own cost, was blindness: the council could not produce auditable accounts, so the emergency was declared on unaudited figures that a group of academics publicly disputed in October 2025. As of January 2026 the system is being rebuilt from scratch on the principle of adopting standard processes.

"Adopt, don't adapt" is a real engineering rule

Birmingham, Lidl and the US Air Force all bought standard software in order to get standard processes, and all three then paid to rewrite the software to preserve the processes they had bought it to replace. You will face the small version constantly: a customer wants the invoice to look exactly like the old one, wants a status the old system had, wants the order number to embed a season code. Each request is cheap. The rule that keeps you alive is to make configuration the default answer and code the exception, and to record every exception you do build in one list the owner has to look at.

The lesson for your ERP. The most transferable finding here is "intelligent customer." Birmingham could not evaluate what its supplier was delivering because nobody inside knew the system well enough. If you build this for a brand you are both the supplier and the only person who understands it, which means the customer has no way to check you. That obligation falls back on you: reconciliation reports anyone can read, the invariants of Chapter 9 running continuously, and the runbook of Chapter S written so a non-engineer can tell whether the system is healthy.

Hewlett-Packard, 2004: the contingency plan was three weeks short

This one is short and it is the best cutover story in the set, because HP was good at this. In the third quarter of its 2004 fiscal year HP migrated its North American server order processing onto SAP. Its own disclosure says that "in the United States, a migration to a new order processing and supply chain system was more disruptive than planned." Orders stalled between the legacy order-entry system and the new one. The result was a $120 million order backlog and about $40 million of lost revenue, roughly $160 million of impact against a project industry analysts estimated at $30 million. Three executives left, and the chief executive's summary was "We executed poorly on the migration."

The detail that makes it worth including: this was HP's thirty-fifth regional consolidation of this kind, and the previous thirty-four had worked. What the responsible executive concluded afterward was not that HP should have prevented the small failures, which were individually unremarkable and collectively unpredictable, but that the contingency plan should have covered five or six weeks instead of three. The actual disruption ran six weeks. The failure was not in the migration. It was in the size of the safety net underneath it.

The lesson for your ERP. Plan the fallback for longer than you think the problem can last, and make it something you have rehearsed. Before a cutover, write down how the business ships orders, invoices customers and answers "how many do we have" with the new system unavailable, then run a half-day drill on it while everything is still fine. Chapter S is where that document lives.

Vodafone UK, 2013–2015: the bug that hid in a rare path

Not an ERP, but the mechanism is one you will meet and a regulator published the whole thing. Vodafone migrated more than 28.5 million customer accounts and almost a billion data fields from seven legacy billing platforms onto one system, starting at the end of 2013. Some accounts migrated incorrectly, producing wrong billing data and wrong price plan records. That much is ordinary.

The specific defect is the interesting one. Customers who topped up a pay-as-you-go phone that had been dormant for nine months or more received a confirmation message saying the credit had been added, but the account continued to be flagged as disconnected. The money was taken. The confirmation was sent. The state transition never happened. It ran for about sixteen months before it was fixed, and Vodafone's own explanation of why is the part to remember: fewer than 0.01% of its pay-as-you-go customers had a phone inactive for more than nine months before reactivating it. The defect lived in a state transition so rare it stayed below the noise floor of ordinary monitoring. Ofcom, the UK communications regulator, fined Vodafone £3.7 million for the billing failure and £925,000 for how complaints about it were handled. 10,452 customers were affected and 10,422 were refunded at an average of £14.35, around £150,000 in total. The fine was roughly thirty times the money involved.

The lesson for your ERP. Migrating balances is different from migrating records, because a balance is money and a wrong one is theft or a gift. Your equivalents are customer credit balances, open invoice amounts, unapplied payments and on-hand quantities. Reconcile every one of them, per customer, and write the report that proves it before you write the importer, as Chapter 7 sets out. Then notice the shape of the bug: a state machine with a path almost nobody takes. Section B.9 defines the inventory state machine precisely so you can enumerate its transitions and test the rare ones, such as a return against an order already partly canceled, or a receipt against a purchase order closed early.

When it goes to court: what the contracts actually said

Two lawsuits are worth reading in outline, because they show what the paperwork on a failed project looks like when it is read out in public. A statement of work, usually abbreviated SOW, is the document attached to a services contract saying what will be delivered, by when, and by whom. It is the sentence-by-sentence definition of the job, and in both cases it is the battleground.

Waste Management sued SAP in Texas state court in March 2008, alleging that the pre-sale demonstrations "were in fact nothing more than fake, mock-up simulations that did not use the software ultimately licensed," and were "rigged and manipulated to depict false functionality." Set that aside and read the contract terms, which are more useful. The license warranted only that the software would "materially conform to the functional specifications contained in the Documentation," which is a warranty that the product matches its own manual, not that it will do your job. And the original statement of work contained a line the plaintiff leaned on hard: "No functional gap enhancements have been identified or are expected." SAP counterclaimed that Waste Management had expressly agreed SAP did not warrant the applications were designed to meet all of its business requirements, and that the customer had failed to define its requirements accurately and on time, failed to provide knowledgeable and empowered users, and failed to migrate its legacy data. The petition pleads no damages figure; the widely quoted "$500 million" is press arithmetic. It settled in April 2010 with a one-time cash payment that increased Waste Management's income from operations by $77 million.

The rigged-demo allegation is not unique. An Alabama pet food manufacturer won a $61.4 million jury verdict in 2010 against a mid-market ERP vendor where trial evidence showed the sales demonstrations had been assembled to look as though the product met the customer's needs without actually doing so. When you demonstrate software to an owner, demonstrate the running system against their real data, and say out loud which parts do not exist yet.

MillerCoors sued its integrator, HCL Technologies, in federal court in Illinois in March 2017, seeking damages in excess of $100 million on a fixed-price SAP engagement worth about $53 million. Its complaint says that at the first go-live in November 2015, testing had revealed 8 critical-severity and 47 high-severity defects, with thousands more recorded afterward. Note what that means: the testing worked. The defects were counted. The system went live anyway, which is a different and more uncomfortable failure than Hershey's. HCL's counterclaim is the mirror image and equally instructive: it argued the fixed price had been quoted on the assumption that another firm had completed the functional specifications, that those specifications were in fact around 75% incomplete, and that the customer kept demanding work outside the agreed scope. The case was dismissed with prejudice in November 2018 after a confidential settlement, each side bearing its own costs.

The lesson for your ERP. Read the statement of work as an engineering document. If a scope line says "no functional gaps expected," someone has promised the software already does everything and nobody has checked. Write down which of the day-one modules in section B.4 are in scope, what "done" means for each, and what the customer must supply and by when: their product data, their customer list, their opening balances. Chapter N covers this, and the honest version is short.

Six patterns behind the failures

These stories run from 1996 to the present, across every major vendor, in drug distribution, confectionery, footwear, apparel, jewelry, retail, cosmetics, grocery, telecoms and local government. They break in the same six ways.

Scope. Every ERP project starts with a defensible list and grows. Each addition is individually reasonable and collectively fatal, because ERP modules are coupled: adding production planning means touching inventory, costing and purchasing. The counter-move is the day-one table in section B.4, defended in writing.

Data. Target Canada, FoxMeyer, Shane Co. and Birmingham all broke on data rather than application code, and Nike and Vodafone broke on data moving between systems. Old data is always worse than anyone admits, and new data is worse still, because a fresh database has nothing to reconcile against. Look at the source data in week one and write the reconciliation report, old system total against new system total, per SKU and per customer, before you write the importer.

The "we do it differently" trap. Lidl spent seven years refusing to change one inventory valuation convention. Birmingham bought standard software and customized it back to its old processes. Ask, for every requested deviation: is this a competitive advantage, or a habit? Habits change to match the software. Genuine advantages, a few per company, justify code.

Timing. Hershey went live three months late, into the pre-Halloween surge. Haribo converted at the start of the Christmas season. Levi Strauss took its distribution centers down mid-quarter. The business calendar constrains the project schedule and not the other way around, and when a project slips it slips to the next safe window.

Adoption. Avon's technology worked and its users left. Birmingham's auditors found the operational teams who would use the system were not engaged in its design. Target Canada's data was entered by people nobody had told that accuracy mattered. A system is not delivered when it runs. It is delivered when the people who have alternatives choose it over them.

Over-configurability. The subtle one, and the trap a competent engineer is most likely to fall into. The conflicting user needs from section B.13 make a settings screen for everything look like the answer: configurable workflows, user-defined fields, a report builder, a rules engine. What you end up with is a platform for building ERPs, and your users must become consultants to operate it. Every option multiplies the states you must test, and small brands have nobody to do the configuring.

One observation runs through the whole file. In almost every case, somebody knew. Auditors found that Birmingham's leadership did not want to hear bad news and that known weaknesses were not reported to decision-makers. A Lidl consultant described project owners and consultants closing their eyes together so neither had to admit failure. At Target Canada's go or no-go meeting two senior people expressed strong reticence and neither advocated delay; afterward, "Nobody wanted to be the one to say, 'This is a disaster.'" MillerCoors alleged 55 critical and high-severity defects were open at go-live, which means they had been counted. The technical failures in this section are ordinary. What made them expensive is that the organizations could not say so out loud in time.

A narrow, opinionated ERP beats a general one for a small brand

SAP can model almost any business on earth, which is exactly why large-enterprise S/4HANA programs commonly run 18 to 36 months (as of 2025) and lean on a bench of specialist consultants. A four-person apparel brand has no consultants, no IT department, and no patience. You will never out-feature NetSuite. What you can do instead is decide things on the user's behalf: sizes are ordered, orders have a start ship and a cancel date, ATS excludes allocated stock, periods lock after close. Every decision you make correctly is a configuration screen you never build and a support call you never take. Being narrow is the product.

Field notes & further reading

Exercise

1. Map a real brand's order-to-cash. Pick an apparel brand you can actually observe — one whose wholesale linesheet you can find online, or better, one whose owner will talk to you for twenty minutes. Write out its order-to-cash as an eleven-step numbered list in the format of section B.7. For each step, name three things: the document produced, the system or place it currently lives (email, spreadsheet, QuickBooks, a portal), and the state change it causes. Then mark every step where the same fact is re-typed by a human into a second place. Those re-typings are the errors the ERP exists to eliminate, and counting them is the business case.

2. Write the entity list for one real business. Take the same brand and produce its entity list. For each table give: the name, one sentence of meaning, its parent table, and its grain — the precise thing one row represents ("one size of one color of one style," not "a product"). Include at minimum tenants, styles, variants, customers, ship-to locations, price lists, sales orders, order lines, allocations, warehouses, inventory transactions, purchase orders, receipts, shipments, invoices and payments — sixteen tables. Then answer three questions in writing: (a) what exactly does this brand's ATS formula include and exclude, stated as a policy sentence; (b) does it sell prepacks, and if so, at what grain will you store an order line; (c) which three of those sixteen tables could you defer past version one without breaking the other thirteen.

When you are done, you should have two artifacts you can hand to another engineer: a numbered process map annotated with documents and state changes, and a grain-annotated entity list with a written ATS policy. Together those are the functional specification for Chapters 3 through 8. If you cannot state the grain of a table in one sentence, you do not yet understand it well enough to build it.