PageSourceSearch

https://my-docusaurus-pi.vercel.app/assets/js/fbca3bce.eb959890.js

js my-docusaurus-pi.vercel.app collected 2026-10-03 07:22:39 UTC 17,843 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkmy_website=globalThis.webpackChunkmy_website||[]).push([[7980],{5890(e,n,i){i.r(n),i.d(n,{assets:()=>r,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>t,toc:()=>c});const t=JSON.parse('{"id":"bundles-and-boms/how-synplex-tracks-ato-inventory","title":"How Synplex Tracks Assembly-to-Order Inventory","description":"This page explains the two automated mechanics that keep assembly-to-order (ATO) BOMs accurate: buildable quantity calculation (driven by component inventory changes) and Shopify inventory sync (writing the result back to the parent SKU in Shopify).","source":"@site/docs/bundles-and-boms/how-synplex-tracks-ato-inventory.md","sourceDirName":"bundles-and-boms","slug":"/bundles-and-boms/how-synplex-tracks-ato-inventory","permalink":"/docs/bundles-and-boms/how-synplex-tracks-ato-inventory","draft":false,"unlisted":false,"editUrl":"https://github.com/synplex/help-center/tree/main/docs/bundles-and-boms/how-synplex-tracks-ato-inventory.md","tags":[],"version":"current","sidebarPosition":6,"frontMatter":{"id":"how-synplex-tracks-ato-inventory","title":"How Synplex Tracks Assembly-to-Order Inventory","sidebar_position":6},"sidebar":"docs","previous":{"title":"How Synplex Explodes ATO Demand to Components","permalink":"/docs/bundles-and-boms/how-synplex-explodes-ato-demand"},"next":{"title":"Product Features Overview","permalink":"/docs/product-features/product-features-readme"}}');var s=i(4848),l=i(8453);const a={id:"how-synplex-tracks-ato-inventory",title:"How Synplex Tracks Assembly-to-Order Inventory",sidebar_position:6},o="How Synplex Tracks Assembly-to-Order Inventory",r={},c=[{value:"Why ATO BOMs Need Continuous Recalculation",id:"why-ato-boms-need-continuous-recalculation",level:2},{value:"Part 1: Buildable Quantity Calculation",id:"part-1-buildable-quantity-calculation",level:2},{value:"What triggers a recalculation",id:"what-triggers-a-recalculation",level:3},{value:"How buildable quantity is calculated",id:"how-buildable-quantity-is-calculated",level:3},{value:"Per-location granularity",id:"per-location-granularity",level:3},{value:"Part 2: Shopify Inventory Sync",id:"part-2-shopify-inventory-sync",level:2},{value:"What gets synced and when",id:"what-gets-synced-and-when",level:3},{value:"Change detection",id:"change-detection",level:3},{value:"What Shopify receives",id:"what-shopify-receives",level:3},{value:"Sync log",id:"sync-log",level:3},{value:"What Happens When a BOM Status Changes",id:"what-happens-when-a-bom-status-changes",level:2},{value:"Activating a BOM (draft \u2192 active)",id:"activating-a-bom-draft--active",level:3},{value:"Archiving a BOM (active \u2192 archived)",id:"archiving-a-bom-active--archived",level:3},{value:"Reactivating a BOM (archived \u2192 active)",id:"reactivating-a-bom-archived--active",level:3},{value:"Changing BOM type on an active BOM",id:"changing-bom-type-on-an-active-bom",level:3},{value:"Troubleshooting",id:"troubleshooting",level:2},{value:"Parent SKU shows wrong available in Shopify",id:"parent-sku-shows-wrong-available-in-shopify",level:3},{value:"Buildable quantity shows 0 unexpectedly",id:"buildable-quantity-shows-0-unexpectedly",level:3},{value:"Sync succeeded but Shopify shows a different number",id:"sync-succeeded-but-shopify-shows-a-different-number",level:3}];function d(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",hr:"hr",li:"li",ol:"ol",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,l.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"how-synplex-tracks-assembly-to-order-inventory",children:"How Synplex Tracks Assembly-to-Order Inventory"})}),"\n",(0,s.jsxs)(n.p,{children:["This page explains the two automated mechanics that keep assembly-to-order (ATO) BOMs accurate: ",(0,s.jsx)(n.strong,{children:"buildable quantity calculation"})," (driven by component inventory changes) and ",(0,s.jsx)(n.strong,{children:"Shopify inventory sync"})," (writing the result back to the parent SKU in Shopify)."]}),"\n",(0,s.jsx)(n.p,{children:"These run automatically once a BOM is active. You do not need to trigger them manually."}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"why-ato-boms-need-continuous-recalculation",children:"Why ATO BOMs Need Continuous Recalculation"}),"\n",(0,s.jsx)(n.p,{children:"A pre-assembly kit holds real finished goods stock on the parent SKU. Shopify manages that number directly."}),"\n",(0,s.jsxs)(n.p,{children:['An ATO BOM is different. The parent SKU holds no finished goods. Its "available" figure in Shopify should reflect how many units ',(0,s.jsx)(n.em,{children:"could"})," be assembled right now given current component stock. Every time a component's inventory changes \u2014 a sale, a receipt, a manual adjustment \u2014 the parent SKU's available figure becomes stale and must be recalculated."]}),"\n",(0,s.jsx)(n.p,{children:"The cascade works as follows:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"A component's stock changes"}),"\n",(0,s.jsx)(n.li,{children:"Buildable quantity is recalculated per location"}
1),"\n",(0,s.jsx)(n.li,{children:"The new figure is written back to the parent SKU in Shopify"}),"\n",(0,s.jsx)(n.li,{children:"Shopify shows accurate available-to-promise to customers"}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"part-1-buildable-quantity-calculation",children:"Part 1: Buildable Quantity Calculation"}),"\n",(0,s.jsx)(n.h3,{id:"what-triggers-a-recalculation",children:"What triggers a recalculation"}),"\n",(0,s.jsxs)(n.p,{children:["A recalculation is enqueued whenever a component's inventory level changes. This is handled by the ",(0,s.jsx)(n.code,{children:"recalculateBomWorker"})," background action, called with the ",(0,s.jsx)(n.code,{children:"bomId"})," and the ",(0,s.jsx)(n.code,{children:"locationId"})," where the change occurred."]}),"\n",(0,s.jsx)(n.h3,{id:"how-buildable-quantity-is-calculated",children:"How buildable quantity is calculated"}),"\n",(0,s.jsxs)(n.p,{children:["For each component in the BOM, Synplex reads the available stock at the specified location and divides it by the required quantity per finished unit. The lowest result across all components is the buildable quantity. The component responsible for the lowest result is the ",(0,s.jsx)(n.strong,{children:"bottleneck"}),"."]}),"\n",(0,s.jsx)(n.p,{children:'Example \u2014 BOM: "Custom PC Build \u2013 Base"'}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Component"}),(0,s.jsx)(n.th,{children:"Required qty per unit"}),(0,s.jsx)(n.th,{children:"Stock at London Warehouse"}),(0,s.jsx)(n.th,{children:"Buildable"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"CPU (Intel i5)"}),(0,s.jsx)(n.td,{children:"1"}),(0,s.jsx)(n.td,{children:"120"}),(0,s.jsx)(n.td,{children:"120"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"RAM 16GB"}),(0,s.jsx)(n.td,{children:"2"}),(0,s.jsx)(n.td,{children:"90"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"45 \u2190 bottleneck"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"SSD 512GB"}),(0,s.jsx)(n.td,{children:"1"}),(0,s.jsx)(n.td,{children:"200"}),(0,s.jsx)(n.td,{children:"200"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Buildable quantity at London Warehouse: 45 units. Bottleneck: RAM 16GB."})}),(0,s.jsx)(n.td,{}),(0,s.jsx)(n.td,{}),(0,s.jsx)(n.td,{})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["The formula: ",(0,s.jsx)(n.code,{children:"buildable = min over all components of floor(available \xf7 required_qty)"})]}),"\n",(0,s.jsx)(n.p,{children:"If any component has no inventory level record at the location, buildable quantity is treated as 0 for that location."}),"\n",(0,s.jsx)(n.h3,{id:"per-location-granularity",children:"Per-location granularity"}),"\n",(0,s.jsx)(n.p,{children:"The calculation runs independently per location. If your shop has three warehouses, Synplex produces three separate buildable quantities and tracks the bottleneck component independently for each."}),"\n",(0,s.jsx)(n.p,{children:"The overall buildable quantity shown in the BOM table is the sum across all included locations."}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"part-2-shopify-inventory-sync",children:"Part 2: Shopify Inventory Sync"}),"\n",(0,s.jsx)(n.h3,{id:"what-gets-synced-and-when",children:"What gets synced and when"}),"\n",(0,s.jsxs)(n.p,{children:["After the buildable quantity is calculated, Synplex writes the new figure to the parent SKU's inventory level in Shopify using the ",(0,s.jsx)(n.code,{children:"inventorySetQuantities"})," GraphQL mutation (reason: ",(0,s.jsx)(n.code,{children:"correction"}),")."]}),"\n",(0,s.jsxs)(n.p,{children:["This sync ",(0,s.jsx)(n.strong,{children:"only runs for ATO BOMs"}),". Pre-assembly kits are excluded because their parent SKU inventory is managed by Shopify directly through Production Orders, not derived from component stock."]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"BOM type: assemble-to-order \u2192 sync runs"}),"\n",(0,s.jsx)(n.li,{children:"BOM type: pre-assembled \u2192 sync skipped"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"change-detection",children:"Change detection"}),"\n",(0,s.jsx)(n.p,{children:"Synplex compares the new buildable quantity against the current value stored in the inventory level record before calling Shopify. If the values are identical, the Shopify API call is skipped entirely, avoiding unnecessary API calls and rate limit consumption."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Example \u2014 no change needed:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Previous available in Shopify: 45"}),"\n",(0,s.jsx)(n.li,{children:"New buildable quantity: 45"}),"\n",(0,s.jsx)(n.li,{children:"Result: Shopify call skipped"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Example \u2014 sync required:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Previous available in Shopify: 45"}),"\n",(0,s.jsx)(n.li,{children:"New buildable quantity: 38"}),"\n",(0,s.jsx)(n.li,{children:"Result: sync runs, Shopify updated to 38"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"what-shopify-receives",children:"What Shopify receives"}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"inventorySetQuantities"})," mutation is called with:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"reason"}),": ",(0,s.jsx)(n.code,{children:'"correction"'})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"name"}),": ",(0,s.jsx)(n.code,{children:'"available"'})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"inventoryItemId"}),": ",(0,s.jsx)(n.code,{children:"gid://shopify/InventoryItem/{id}"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"locationId"}),": ",(0,s.jsx)(n.code,{children:"gid://shopify/Location/{id}"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"quantity"}),": the new buildable quantity"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"sync-log",children:"Sync log"}),"\n",(0,s.jsxs)(n.p,{children:["Every sync attempt \u2014 whether it succeeds or fails \u2014 is recorded in the ",(0,s.jsx)(n.code,{children:"InventorySyncLog"}
1)," model. Each record captures:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"bom"})," \u2014 which BOM triggered the sync"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"location"})," \u2014 which location was synced"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"productVariant"})," \u2014 the parent SKU"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"previousInventoryLevel"})," \u2014 value before sync"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"actualInventoryLevel"})," \u2014 value written"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"delta"})," \u2014 change applied (positive or negative)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"reason"})," \u2014 ",(0,s.jsx)(n.code,{children:'"bom_recalculation"'})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"success"})," \u2014 true / false"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"errorMessage"})," \u2014 populated on failure"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"You can review sync history in the app to diagnose discrepancies between Synplex and Shopify."}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"what-happens-when-a-bom-status-changes",children:"What Happens When a BOM Status Changes"}),"\n",(0,s.jsx)(n.p,{children:"Status transitions on an active BOM trigger additional cascading updates beyond the buildable quantity sync."}),"\n",(0,s.jsx)(n.h3,{id:"activating-a-bom-draft--active",children:"Activating a BOM (draft \u2192 active)"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"bomStatus"})," field synced to ",(0,s.jsx)(n.code,{children:'"active"'})," on all BomComponent records"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"isBomComponent"})," flag set to ",(0,s.jsx)(n.code,{children:"true"})," on all component inventory levels"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"recalculateComponentDemandHistory"})," enqueued for each component (see the demand explosion doc for what this does)"]}),"\n",(0,s.jsx)(n.li,{children:"First buildable quantity calculation enqueued per location"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"archiving-a-bom-active--archived",children:"Archiving a BOM (active \u2192 archived)"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"bomStatus"})," field synced to ",(0,s.jsx)(n.code,{children:'"archived"'})," on all BomComponent records"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"isBomComponent"})," flag set to ",(0,s.jsx)(n.code,{children:"false"})," on all component inventory levels"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"recalculateComponentDemandHistory"})," enqueued for each component (zeroes out the BOM demand contribution)"]}),"\n",(0,s.jsx)(n.li,{children:"Parent SKU inventory in Shopify is no longer updated by Synplex"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"reactivating-a-bom-archived--active",children:"Reactivating a BOM (archived \u2192 active)"}),"\n",(0,s.jsx)(n.p,{children:"Same cascade as activation. All flags are reset and demand recalculation re-runs from scratch."}),"\n",(0,s.jsx)(n.h3,{id:"changing-bom-type-on-an-active-bom",children:"Changing BOM type on an active BOM"}),"\n",(0,s.jsxs)(n.p,{children:["If an active BOM is switched between ",(0,s.jsx)(n.code,{children:"pre-assembled"})," and ",(0,s.jsx)(n.code,{children:"assemble-to-order"}),", Synplex re-enqueues ",(0,s.jsx)(n.code,{children:"recalculateComponentDemandHistory"})," for all components because the demand attribution logic differs between the two types."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,s.jsx)(n.h3,{id:"parent-sku-shows-wrong-available-in-shopify",children:"Parent SKU shows wrong available in Shopify"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:'BOM status is "active" \u2014 draft BOMs do not sync'}),"\n",(0,s.jsx)(n.li,{children:'BOM type is "assemble-to-order" \u2014 pre-assembled BOMs do not sync'}),"\n",(0,s.jsx)(n.li,{children:"Check the InventorySyncLog for this BOM \u2014 was the last sync successful?"}),"\n",(0,s.jsx)(n.li,{children:"Component inventory levels exist at the location in question"}),"\n",(0,s.jsxs)(n.li,{children:["No component is missing an ",(0,s.jsx)(n.code,{children:"inventoryItemId"})," linkage"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"buildable-quantity-shows-0-unexpectedly",children:"Buildable quantity shows 0 unexpectedly"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"At least one component has 0 stock at the location"}),"\n",(0,s.jsxs)(n.li,{children:["A component is missing its ",(0,s.jsx)(n.code,{children:"inventoryItemId"})," (visible in BOM detail)"]}),"\n",(0,s.jsx)(n.li,{children:'The location is marked as "included" in Synplex location settings \u2014 excluded locations are not used in calculations'}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"sync-succeeded-but-shopify-shows-a-different-number",children:"Sync succeeded but Shopify shows a different number"}),"\n",(0,s.jsx)(n.p,{children:"Possible causes:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Shopify applied its own inventory adjustment after the sync (e.g. a sale processed between sync and your check)"}),"\n",(0,s.jsx)(n.li,{children:"Another app or manual edit overwrote the value in Shopify"}),"\n",(0,s.jsx)(n.li,{children:"The sync ran against a different location than expected"}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Questions?"})," Contact ",(0,s.jsx)(n.a,{href:"mailto:[email protected]",children:"[email protected]"})]})]})}function h(e={}){const{wrapper:n}={...(0,l.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},8453(e,n,i){i.d(n,{R:()=>a,x:()=>o});var t=i(6540);const s={},l=t.createContext(s);function a(e){const n=t.useContext(l);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),t.createElement(l.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.