Map Inventory Policy
Map raw rows into the scenario's inventory policy table: the rule for inventory at each location, the per-unit carrying rate, the period it is expressed over, and the days of inventory held.
This is the input side of inventory. Pair it with Map Inventory State for the observed level and the realized total.
When to use this¶
- Inventory is part of the scenario and you have a per-unit holding rate
- You want the rate saved on the scenario so a what-if edit can recompute carrying cost without re-deriving it from totals
Network Optimization charges carrying cost on a row that has all three of
Unit Carrying Cost, Unit Carrying Cost Period and
Average Days of Inventory, and weighs it against transport, sourcing and
handling when it picks a route. Create Scenario refuses a row with a cost and
days of inventory but no period, and any row whose period is not day, week,
month or year, and names the rows. A row with no cost or no days of
inventory carries no cost and only travels with the scenario.
Set the product to ALL and the rate applies to every product held at that
location, the same way a location-wide handling rate does. As there, an ALL
row beside a row naming a product at the same location is refused.
What you need¶
- An upstream Import Data node with one row per (location, product) you have an inventory rate for
- A per-unit carrying-cost column. If you only have realized totals, use Map Inventory State instead
- The unit the cost is per, such as
palletorkg: a column, or one unit for the whole file
Field mapping¶
Fields are matched by name unless you set them. When the workflow runs, every field you have not set a column for is matched against the headers listed below, case-insensitively and ignoring spaces around the header, and a space inside your header reads as an underscore, so Site Name matches site_name. Setting a column for one field fixes that field only; the others keep following your file's headers, so a renamed column is still found. A required field that no column supplies — no header matched, or you set none or one your table lacks — is reported once, with the headers your table does have.
Set mappings in the node's panel. Connect an upstream table and select the mapping node. A field matched by name is marked Matched by name; pick a column to set it yourself, and Match by name puts it back. Match all by name clears every field you set. Required fields without a match are marked in the panel.
Reshape rows in a Script first. A mapping node only says which column is which field. To drop rows, recode values, or split or combine columns, connect a Script node before it and map the columns the Script returns.
Same for every row. A file with no unit column still states its unit: in Quantity Unit, pick Same for every row and type the unit, such as pallets, and every row is in it. Over MCP, set it in field_mappings as "quantity_unit": "__const__:pallets". Quantity Unit is required on every row with Unit Carrying Cost, and the panel marks it required once it is mapped.
| Field | Type | Required | Example | What it is | Auto-detected headers |
|---|---|---|---|---|---|
| Location | text | Yes | Warehouse A |
Where inventory is held. Must match a Location name. | location, loc, name, id, location_id, loc_id |
| Product | text | Yes | Widget |
Product or SKU held, or ALL for a rate that applies to every product at this location. Any other value must match a Product name. | product, sku, item, material, product_id |
| Unit Carrying Cost | number | No | 0.1 |
Cost to carry one unit of inventory for one period, in the scenario currency per the row's Quantity Unit. Blank = no carrying cost is charged. Must be greater than or equal to 0. | unit_carrying_cost, carrying_rate, holding_rate, cost_per_unit_inventory |
| Average Days of Inventory | number | No | 14.0 |
Target days of inventory on hand; combined with outbound flow this sets the average inventory level. Blank = inventory level is not modelled. Must be greater than or equal to 0. | average_days_of_inventory |
| Unit Carrying Cost Period | text | No | month |
Time period the unit carrying cost is expressed over: day, week, month, or year. Required on every row with both a carrying cost and days of inventory. | unit_carrying_cost_period |
| Quantity Unit | text | No | pallets |
Unit the carrying cost is per. Required on every row that states a cost. On an ALL row in another unit than the scenario's, each product's cost is converted with its own conversion. | quantity_unit, qty_unit, unit, uom |
How it works¶
Each row becomes an inventory policy record, stored on the scenario beside the inventory state rows. Cost to Serve does not read it. The carrying cost it allocates is the realized total on the state side.
Every cost states its unit. Quantity Unit says what one unit of the
carrying cost is. Create Scenario refuses a row with a cost and no unit, and
a unit with no conversion to the scenario's Unit. An ALL row in another
unit than the scenario's is converted for each product with that product's
own conversion.
Workflow wiring¶
flowchart LR
n1["Import Data (inventory.csv)"] --> n2["Map Inventory Policy"]
n1 --> n3["Map Inventory State"]
n4["Import Data (locations.csv)"] --> n5["Map Locations"]
n6["Import Data (products.csv)"] --> n7["Map Products"]
n2 & n3 & n5 & n7 --> n8["Create Scenario"]
Common mistakes¶
- Mapping the realized total here. Unit Carrying Cost is per unit. The total belongs on Map Inventory State as Realized Carrying Cost.
- Mapping the average inventory level here. A quantity belongs on Map Inventory State as Average Inventory.
- Leaving Quantity Unit blank. A cost per unit with no unit is refused, not read as the scenario's unit.
Messages this node can show¶
- Field matches several columns
- Mapped table empty
- Number outside its allowed bound
- Required field absent
- Row doesn’t fit its table
- Text padded with spaces
- Value not a plain number
The table this node makes is checked again with the rest of the scenario when Create Scenario assembles it: see messages about the scenario.
A run gives this node 5 minutes. A node that takes longer stops with Time limit reached.
Like every node, it can also stop on the checks before a run and the run's limits.