Bulk Import — inventory + storage in one upload
Migrating from another WMS, or landing a backlog of pre-EMS shipments, used to mean running two CSV imports back-to-back — one for inventory, one for storage. v3.5.5 collapsed them into a single template. One row per (product × AWB) declares what arrived and (optionally) where it now lives. EMS lands the migration shipment, the products, the per-box records, the skid placements, and the storage zone stamps in one transaction.
One CSV, not two
Before v3.5.5, bulk-import was a two-step:
- Inventory Bulk Import — created a migration shipment per AWB with rolled-up item totals (no real boxes).
- Storage Bulk Import — separately attached skids + zone/slot placements, only after step 1 had completed for the same AWB.
The two-step had three problems: it doubled the operator's CSV authoring time, it left a window where inventoried units existed without a storage placement (and vice versa), and it never materialised per-box records — so production scan, damage tracking, and the customer portal's box detail were all blind on bulk-imported inventory.
The unified import fixes all three. The button on the Inventory page and the Storage page both open the same modal; you upload one CSV; EMS handles the two-job hand-off internally.
For each unique (Consignee, AWB) in the CSV, EMS creates (or updates) a migration shipment at status inventoried. For each row, EMS finds or creates the matching product (keyed on brand + name + flavour + mL + mg) and writes the per-box records on the shipment with status: 'received'. If the row carries Skid Number + Zone, EMS also creates the skid with the listed boxes attached and stamps each box's currentLocation with the zone/slot.
End-to-end workflow
- Open Inventory (or Storage) → click Bulk Import.
- Click Download Template. Edit in Excel or Google Sheets.
- Save as CSV (UTF-8). Drag onto the modal or click Upload Completed CSV.
- Review the preview — every row is validated against the catalogue + zone config; errors are flagged inline so you can fix them before commit.
- Click Import. EMS commits everything in one pass and reports counts (shipments created/updated, products created/reused, boxes created/merged, skids created/updated).
- Verify placement in Receiving → Put-Aways — every bulk-imported skid lands in the queue for manual sign-off (see below).
Template columns
The template downloads with header comments + example rows. The columns are:
Required
- Consignee Name — must match a consignee already in the system (case-insensitive). Create the consignee first if it doesn't exist.
- AWB — tracking/inbound reference. Rows sharing an AWB merge into one migration shipment.
- Import Date — when the stock landed at your facility.
YYYY-MM-DDorMM/DD/YYYY. Used as the shipment'sreceivedAt/inventoriedAt. - Brand, Product Name — auto Title-Cased.
- Category — one of the configured categories (Disposables, Pods, E-Liquid, Packaging, Promotional Material, Batteries, or any custom category you've added via the Categories button).
- Box Numbers — see the format section below. This is the biggest change from the old two-step: every row declares which physical boxes carry that product so per-box records get created.
- Quantity — total units of this product on this AWB.
Optional (storage placement)
- Supplier — defaults to "Legacy migration".
- Flavour — blank for unflavoured products.
- Volume (mL), Nicotine (mg) — variant attributes. Two rows with the same Brand + Product + Flavour but different mL/mg create two distinct products and stamp accounting tracks them separately.
- Units per Box — if blank, EMS computes
Quantity / box count. If both are filled, EMS warns when the math doesn't reconcile. - Skid Number — fill in to attach the row's boxes to a skid. Leave blank for inventory-only (units land on the shipment but aren't placed on a skid yet).
- Zone — required when Skid Number is filled. Must match a configured zone (Settings → Racks).
- Slot — for racked zones use the
A1-3format (zone letter + rack number + slot number). Floor zones leave blank. - Notes — free-text, lands on the shipment record for audit.
Box Numbers format
Three accepted forms; combine freely with commas:
- Range —
1-50expands to box numbers 1, 2, 3, ..., 50. - Comma list —
1,2,3stays as those exact numbers. - Combo —
1-50, 75, 80-85mixes ranges with single numbers.
Box numbers are case-sensitive strings, so BATCH1-44 stays as BATCH1-44 (not expanded). Each parsed number becomes one shipment.boxes[] entry that downstream modules — production scan, damage tracking, box-level reports, the customer portal — can address by name.
How merges work
Mixed boxes
Two rows on the same AWB sharing a box number → the box becomes a mixed box carrying both products. Quantities sum, items dedupe by productId, and the items list on the box reflects both rows. Example:
Row 1 — Orbito · Blueberry Raspberry · 30mL · 20mg · boxes 1-50 · 5,000 units
Row 2 — Linvo · White Peach · 30mL · 10mg · boxes 25-30 · 600 units
Result: Boxes 25 through 30 each carry both products. Box 26 (for example) has Blueberry Raspberry × 100 units AND White Peach × 100 units — production scan resolves it as a mixed box and asks the operator to pick which flavour they're stamping.
Multi-product skids
Two rows sharing Consignee + AWB + Skid Number → one skid carrying both products. Same skid label, same zone/slot, items merge by productId. Useful when you've packed several flavours onto one physical skid at your warehouse.
Re-runs are safe
The import is idempotent. Re-running the same CSV does not double-count anything:
- Products dedupe on
brand + name + flavour + ml + mg. The second run reuses, doesn't recreate. - Shipments dedupe on
(Consignee, AWB). The second run updates the existing migration shipment, doesn't spawn a new one. - Boxes already carrying a given productId get skipped (no qty double-count). Brand-new product-in-box combos merge in as mixed boxes.
- Skids dedupe on
(Consignee, AWB, Skid Number). The second run absorbs additional items + box numbers into the existing skid rather than creating a duplicate.
Auto-receive — units land as Available
Bulk-imported boxes get status: 'received' + a receivedAt timestamp matching the row's Import Date. That means the inventory rollup counts them as Available immediately — not stranded in the "Expected" or "In Transit" buckets the way they were before v3.5.5.040.
Practical effect: as soon as the import finishes, the units appear on the Inventory page under the right consignee/brand/variant, customers can request POs against them via the portal, and the shipment's milestone shows Receiving as complete. No separate "mark as received" step.
Pre-3.5.5.040: bulk-imported shipments showed units in the Expected column and the milestone bar stuck on "Receiving — Step 3 of 3". You'd open the shipment hoping for a Receive button and find nothing. Post-.040: units appear under Available; the milestone bar shows Receiving complete; the shipment moves cleanly to Inventoried status.
Put-Aways verification
Every bulk-imported skid is created with putAwayStatus: 'pending', so it appears in Receiving → Put-Aways for manual placement verification — the same model used for live receiving.
The Put-Aways tab shows each pending skid with its label, AWB, consignee, box count, units, planned zone/slot, and age. A floor supervisor walks the warehouse, confirms each skid is physically where the CSV said it should be, and clicks Confirm placement (or scans the location QR if you're using printed location labels). The skid flips to stored and disappears from the queue.
If the physical location doesn't match the CSV, the supervisor can reassign on the spot — the put-away modal lets you scan or type a different slot, and EMS logs both the original planned location and the new actual one for audit.
Common errors
- "consignee not found" — the Consignee Name column doesn't match any consignee record. Create the consignee on the Consignees page first.
- "must be one of Disposables, Pods, ..." — the Category column has a value EMS doesn't recognise. Either fix the spelling or add it as a custom category via Inventory → Categories.
- "token '5x' must be a number or range" — the Box Numbers column has an unparseable token. Check for stray letters or non-ASCII dashes (EMS accepts
-and–but not—). - "Zone required when Skid Number is filled" — you provided a skid number but no zone. Either fill Zone (and Slot for racked zones) or clear Skid Number to make the row inventory-only.
- "slot A1-3 is already occupied" — another shipment's skid is in that slot. Pick a different slot or clear the existing skid first.
Errors are reported per-row in the preview before commit. Fix them in the CSV and re-upload — nothing's written to the system until you click Import.
Best practices
- Use a
LEG-orMIG-prefix on legacy AWBs. It keeps migration shipments sorted away from real inbound AWBs in the shipments list. - Validate your zones in Settings first. The preview catches unknown zones, but it's faster to set them up before authoring the CSV than after.
- Author one CSV per consignee. Technically the template handles multi-consignee imports in one file, but a per-consignee CSV makes the audit trail and any re-import much cleaner.
- Don't skip Box Numbers. A row without box numbers is rejected — by design — because production scan, damage tracking, and the customer portal all key off per-box records. If you genuinely don't have box-level data, generate a synthetic range (
1-N) so downstream modules still have something to address. - Run a dry-run on a single AWB first. Pull one AWB from the larger CSV, import it, verify the result on the Inventory + Put-Aways pages, then re-author the full file with confidence.
