PageSourceSearch

https://epq-calculator.netlify.app/

html epq-calculator.netlify.app collected 2026-10-03 10:42:15 UTC 59,617 bytes, 829 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8">
5<!-- This site is hosted on Netlify. Anyone can build and deploy a site
6     like this one for free: https://netlify.new/?utm_campaign=loops&utm_source=ai-legible&utm_medium=owned&utm_content=comment&utm_id=694daa5e-51d7-4e4e-ac2a-0bee5aeeefd5
7     Netlify hosting facts for this site: static/SSR served via Netlify Edge. -->
8<meta name="viewport" content="width=device-width, initial-scale=1.0">
9<!-- set theme before paint (no flash): saved choice → OS preference → light -->
10<script>(function(){try{var t=localStorage.getItem('theme')||(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');document.documentElement.dataset.theme=t;}catch(e){}})();</script>
10
11
12<!-- ============ SEO ============ -->
13<title>EPQ Calculator &amp; Formula — Free Manufacturing Tool</title>
14<meta name="description" content="The EPQ formula explained and derived, with a worked example in real numbers — plus a free calculator for lot size, run time, cycle length and annual cost.">
15<link rel="canonical" href="https://epq-calculator.netlify.app/">
16<meta property="og:title" content="EPQ Calculator &amp; Formula — Free Manufacturing Tool">
17<meta property="og:description" content="Calculate the optimal production lot size, compare EOQ, process multiple SKUs and export to Excel — free, with no signup.">
18<meta property="og:type" content="website">
19<meta property="og:url" content="https://epq-calculator.netlify.app/">
20<meta property="og:image" content="https://epq-calculator.netlify.app/og-image.png">
21<meta property="og:image:width" content="1200">
22<meta property="og:image:height" content="630">
23<meta name="twitter:card" content="summary_large_image">
24<meta name="twitter:image" content="https://epq-calculator.netlify.app/og-image.png">
25<link rel="icon" type="image/png" href="/favicon.png">
26<link rel="icon" sizes="any" href="/favicon.ico">
27<link rel="apple-touch-icon" href="/apple-touch-icon.png">
28
29<!-- JSON-LD: SoftwareApplication -->
30<script type="application/ld+json">
31{
32  "@context":"https://schema.org",
33  "@type":"SoftwareApplication",
34  "name":"EPQ Calculator (Economic Production Quantity)",
35  "applicationCategory":"BusinessApplication",
36  "operatingSystem":"Web",
37  "author":{"@type":"Person","name":"Sathapana"},
38  "offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},
39  "description":"Free Economic Production Quantity calculator for manufacturers. Computes optimal production lot size, maximum inventory, run time, cycle length, and total annual cost using the full EPQ formula."
40}
41</script>
41
42<!-- JSON-LD: FAQPage (AEO / entity signal) -->
43<script type="application/ld+json">
44{
45  "@context":"https://schema.org",
46  "@type":"FAQPage",
47  "mainEntity":[
48    {"@type":"Question","name":"What is the Economic Production Quantity (EPQ)?","acceptedAnswer":{"@type":"Answer","text":"EPQ is the optimal production lot size that minimizes the combined setup and holding cost when a company produces inventory in-house at a finite rate, rather than receiving a full order at once."}},
49    {"@type":"Question","name":"How is EPQ different from EOQ?","acceptedAnswer":{"@type":"Answer","text":"EOQ assumes inventory arrives instantly in one delivery. EPQ assumes you produce units gradually while also consuming them, so peak inventory never reaches the full lot. This makes EPQ larger than EOQ and lowers average holding cost."}},
50    {"@type":"Question","name":"What is the EPQ formula?","acceptedAnswer":{"@type":"Answer","text":"EPQ = square root of (2 × D × S) / (H × (1 − d/p)), where D is annual demand, S is setup cost per run, H is annual holding cost per unit, d is the daily demand rate, and p is the daily production rate."}},
51    {"@type":"Question","name":"When should I use EPQ instead of EOQ?","acceptedA
51nswer":{"@type":"Answer","text":"Use EPQ when you manufacture the item yourself and production happens over time at a finite rate. Use EOQ when you purchase the item and receive the full order in a single delivery."}},
52    {"@type":"Question","name":"How do you derive the EPQ formula?","acceptedAnswer":{"@type":"Answer","text":"Write total annual cost as TC(Q) = (D/Q)S + (Q/2)(1 − d/p)H, where the first term is setup cost and the second is holding cost on the reduced average inventory. Differentiate with respect to Q, set dTC/dQ = −DS/Q² + H(1 − d/p)/2 = 0, and solve: Q² = 2DS / (H(1 − d/p)). The square root of that is EPQ. At the optimum the annual setup and holding costs are equal."}},
53    {"@type":"Question","name":"Why does the EPQ formula include (1 − d/p)?","acceptedAnswer":{"@type":"Answer","text":"Because you consume units while you are still producing them. Inventory grows at only (p − d) per day during a run instead of arriving all at once, so it peaks at Q × (1 − d/p) rather than Q. That lower average inventory reduces holding cost, which makes the optimal lot size larger than EOQ. If p is far greater than d, the term approaches 1 and EPQ becomes EOQ."}},
54    {"@type":"Question","name":"What does the EPQ model assume?","acceptedA
54nswer":{"@type":"Answer","text":"Steady known demand, a production rate greater than the demand rate, a fixed setup cost per run independent of lot size, holding cost linear in average inventory, a single item without shared-capacity competition, and no quantity discounts or stockouts."}},
55    {"@type":"Question","name":"Can EPQ be smaller than EOQ?","acceptedAnswer":{"@type":"Answer","text":"No. Since (1 − d/p) is between 0 and 1 whenever p is greater than d, dividing by it makes the value under the square root larger, so EPQ is always greater than or equal to EOQ for the same D, S and H. It equals EOQ only in the limit of instant production."}}
56  ]
57}
58</script>
58
59<!-- JSON-LD: HowTo (steps mirror the worked example rendered in the page body) -->
60<script type="application/ld+json">
61{
62  "@context":"https://schema.org",
63  "@type":"HowTo",
64  "name":"How to calculate Economic Production Quantity (EPQ)",
65  "description":"Calculate the optimal production lot size using the full EPQ formula, which accounts for a finite production rate.",
66  "step":[
67    {"@type":"HowToStep","name":"Gather the four inputs","text":"Collect annual demand D in units per year, setup cost S per production run, holding cost H per unit per year, and the daily production rate p in good units per day."},
68    {"@type":"HowToStep","name":"Convert demand to a daily rate","text":"Divide annual demand by working days per year to get the daily demand rate d. For 12,000 units over 250 working days, d = 48 units per day."},
69    {"@type":"HowToStep","name":"Evaluate the production-rate term","text":"Compute 1 − d/p. With d = 48 and p = 200, this is 1 − 0.24 = 0.76. This term is what distinguishes EPQ from EOQ."},
70    {"@type":"HowToStep","name":"Apply the EPQ formula","text":"EPQ = square root of (2 × D × S) divided by (H × (1 − d/p)). With D = 12,000, S = 450, H = 3.20 and the term 0.76, EPQ = 2,107 units per run."},
71    {"@type":"HowToStep","name":"Derive the scheduling figures","text":"Peak inventory is EPQ × (1 − d/p), runs per year is D ÷ EPQ, run time per batch is EPQ ÷ p, and cycle length is EPQ ÷ d."},
72    {"@type":"HowToStep","name":"Compare against EOQ","text":"Compute EOQ = square root of (2 × D × S / H) to see how much the finite production rate increases the optimal lot size."}
73  ]
74}
75</script>
75
76<!-- JSON-LD: BreadcrumbList (makes the 3-page suite structure legible) -->
77<script type="application/ld+json">
78{
79  "@context":"https://schema.org",
80  "@type":"BreadcrumbList",
81  "itemListElement":[
82    {"@type":"ListItem","position":1,"name":"EPQ Calculator","item":"https://epq-calculator.netlify.app/"},
83    {"@type":"ListItem","position":2,"name":"Safety Stock Calculator","item":"https://epq-calculator.netlify.app/safety-stock.html"},
84    {"@type":"ListItem","position":3,"name":"Reorder Point Calculator","item":"https://epq-calculator.netlify.app/reorder-point.html"}
85  ]
86}
87</script>
87
88
89<link rel="preconnect" href="https://fonts.googleapis.com">
90<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
91<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
92
93<!-- shared design system (also used by safety-stock.html + reorder-point.html) -->
94<link rel="stylesheet" href="suite.css">
95<style>
96  /* index-only components: derived-d hint · tabs · result groups · export buttons · chart · CSV batch */
97  .derived-d{font-size:11px;color:var(--steel);font-family:"IBM Plex Mono";margin-top:4px}
98  /* F3 worked-example link · F8 currency selector */
99  .ex-link{appearance:none;border:none;background:none;color:var(--steel);font-family:"IBM Plex Sans";font-size:12.5px;cursor:pointer;margin-top:9px;text-decoration:underline;text-underline-offset:2px;padding:0}
100  .ex-link:hover{color:var(--signal-deep)}
101  .ch-right{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
102  .cur-sel{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted-2);font-family:"IBM Plex Mono"}
103  .cur-sel select{font-family:"IBM Plex Mono";font-size:12px;padding:4px 7px;border:1px solid var(--line);border-radius:5px;background:var(--surface);color:var(--ink);cursor:pointer}
104
105  /* tabs (Single SKU vs Multi-SKU batch) */
106  .tabs{display:flex;gap:4px;padding:10px 14px 0;border-bottom:1px solid var(--line);background:var(--surface-2)}
107  .tab{appearance:none;border:none;background:transparent;font-family:"Space Grotesk";font-weight:600;font-size:14px;color:var(--muted);padding:9px 14px;border-radius:6px 6px 0 0;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;display:flex;align-items:center;gap:7px;transition:color .15s,background .15s}
108  .tab:hover{color:var(--ink)}
109  .tab.active{color:var(--signal-deep);border-bottom-color:var(--signal);background:var(--surface)}
110  .tab .tab-tag{font-family:"IBM Plex Mono";font-size:10px;letter-spacing:.06em;background:var(--signal-soft);color:var(--signal-deep);padding:2px 6px;border-radius:3px}
111
112  /* result groups */
113  .res-group{margin-bottom:14px}
114  .res-group:last-child{margin-bottom:0}
115  .rg-label{font-family:"IBM Plex Mono";font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2);margin-bottom:7px}
116
117  /* export / copy buttons — orange-outline variant (overrides suite.css .btn-sec) */
118  .export-row{margin-top:16px;display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap}
119  .btn-sec{border:1px solid var(--signal);background:var(--surface);color:var(--signal-deep)}
120  .btn-sec:hover{background:var(--signal);color:#fff}
121  .btn-sec::before{content:"\2913";font-size:14px}
122
123  /* cost-curve chart */
124  .chart-sec{background:var(--surface);border:1px solid var(--line);border-radius:10px;margin-top:18px;padding:22px 24px;box-shadow:0 1px 2px rgba(20,30,40,.03)}
125  .chart-sec h3{font-family:"Space Grotesk";font-weight:600;font-size:16px;margin-bottom:4px}
126  .chart-sec .csub{font-size:13px;color:var(--muted);margin-bottom:16px}
127  .chart-box{position:relative;width:100%;height:260px}
128  canvas{width:100%;height:100%;display:block}
129  .legend{display:flex;gap:18px;flex-wrap:wrap;margin-top:14px;font-size:12px;color:var(--muted)}
130  .legend span{display:flex;align-items:center;gap:6px}
131  .legend i{width:14px;height:3px;border-radius:2px;display:inline-block}
132
133  /* multi-SKU batch panel */
134  #batchPanel{padding:22px 24px}
135  .upload{border:1.5px dashed var(--line);border-radius:9px;background:var(--surface-2);padding:26px 20px;text-align:center;transition:border-color .15s,background .15s}
136  .upload.drag{border-color:var(--signal);background:var(--signal-soft)}
137  .upload .up-ico{font-size:24px;color:var(--signal);line-height:1}
138  .upload .up-main{font-family:"Space Grotesk";font-weight:600;font-size:15px;margin-top:8px}
139  .upload .up-sub{font-size:12.5px;color:var(--muted);margin-top:6px}
140  .upload code,.batch-note code{font-family:"IBM Plex Mono";font-size:12px;background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:1px 5px;color:var(--steel)}
141  .link{appearance:none;border:none;background:none;color:var(--signal-deep);font:inherit;font-weight:600;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0}
142  .batch-note{font-size:12.5px;color:var(--muted);margin-top:12px}
143  .batch-bar{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin:20px 0 12px}
144  .batch-sum{font-size:13px;color:var(--muted)}
145  .batch-sum b{color:var(--ink);font-family:"Space Grotesk"}
146  .batch-sum .warn{color:var(--signal-deep)}
147  .table-scroll{overflow-x:auto;border:1px solid var(--line);border-radius:8px}
148  .btab{width:100%;border-collapse:collapse;font-size:13px}
149  .btab th{text-align:right;font-family:"IBM Plex Mono";font-weight:500;font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted-2);padding:10px 12px;background:var(--surface-2);border-bottom:1px solid var(--line);white-space:nowrap}
150  .btab th:first-child{text-align:left}
151  .btab td{padding:9px 12px;border-bottom:1px solid var(--line-2)}
152  .btab tbody tr:last-child td{border-bottom:none}
153  .btab tbody tr:hover{background:var(--surface-2)}
154  .btab td.num{text-align:right;font-family:"IBM Plex Mono"}
155  .btab td.sku{font-weight:600;font-family:"Space Grotesk"}
156  .btab td.st{color:var(--signal-deep);font-weight:600}
157  .btab tr.bad td{color:var(--bad-fg);background:var(--bad-bg)}
158  .btab td.err-c{text-align:left;font-family:"IBM Plex Sans";font-size:12.5px}
159  /* .vars / .calcline / .assump / p.note live in suite.css — shared with safety-stock + reorder-point */
160
161  /* F1 embed widget — "put this calculator on your site" (distribution → backlinks) */
162  .embed-sec{background:var(--surface);border:1px solid var(--line);border-radius:10px;margin-top:18px;padding:22px 24px;box-shadow:0 1px 2px rgba(20,30,40,.03)}
163  .embed-sec h2{font-family:"Space Grotesk";font-weight:600;font-size:19px;margin-bottom:6px}
164  .embed-sec>p{color:var(--muted);font-size:14px;max-width:620px;margin-bottom:18px}
165  .embed-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:20px;align-items:start}
166  .embed-label,.embed-code-head span{font-family:"IBM Plex Mono";font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2)}
167  .embed-label{margin-bottom:8px}
168  .embed-preview iframe{width:100%;height:800px;border:1px solid var(--line);border-radius:10px;background:var(--surface);display:block}
169  .embed-code-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
170  .btn-copy{appearance:none;border:1px solid var(--signal);background:var(--surface);color:var(--signal-deep);font-family:"Space Grotesk";font-weight:600;font-size:12.5px;padding:6px 12px;border-radius:6px;cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}
171  .btn-copy:hover{background:var(--signal);color:#fff}
172  #embedSnippet{width:100%;min-height:172px;resize:vertical;font-family:"IBM Plex Mono";font-size:12px;line-height:1.55;color:var(--ink);background:var(--surface-2);border:1px solid var(--line);border-radius:8px;padding:12px 14px;white-space:pre;overflow:auto}
173  #embedSnippet:focus{outline:none;border-color:var(--signal);box-shadow:0 0 0 3px var(--signal-soft)}
174  .embed-hint{font-size:12px;color:var(--muted-2);margin-top:10px}
175  @media(max-width:640px){.embed-grid{grid-template-columns:1fr}}
176</style>
177</head>
178<body>
179<div class="wrap">
180
181  <header>
182    <div class="eyebrow">Manufacturing · Inventory</div>
183    <h1>EPQ <span class="u">Calculator</span> &amp; Formula</h1>
184    <p class="lede">
184Find the optimal production lot size for items you manufacture in-house. Unlike EOQ, the Economic Production Quantity accounts for your production rate — so you stop over- or under-batching.</p>
185    <nav class="suite-nav" id="suiteNav"></nav>
186  </header>
187
188  <div id="trust" class="trust"></div>
189
190  <!-- ============ CALCULATOR ============ -->
191  <div class="calc">
192    <div class="calc-head">
193      <span class="ct">Inputs &amp; Results</span>
194      <div class="ch-right">
195        <label class="cur-sel">Currency <select id="cur">
196          <optgroup label="Americas"><option value="$">$ USD</option><option value="CA$">CA$ CAD</option><option value="Mex$">Mex$ MXN</option><option value="R$">R$ BRL</option></optgroup>
197          <optgroup label="Europe"><option value="€">€ EUR</option><option value="£">£ GBP</option><option value="Fr">Fr CHF</option><option value="zł">zł PLN</option></optgroup>
198          <optgroup label="Asia-Pacific &amp; ME"><option value="¥">¥ JPY</option><option value="CN¥">CN¥ CNY</option><option value="₹">₹ INR</option><option value="₩">₩ KRW</option><option value="NT$">NT$ TWD</option><option value="S$">S$ SGD</option><option value="A$">A$ AUD</option><option value="฿">฿ THB</option><option value="RM">RM MYR</option><option value="₫">₫ VND</option><option value="Rp">Rp IDR</option><option value="₱">₱ PHP</option><option value="AED">AED</option></optgroup>
199        </select></label>
200        <span class="badge">Full EPQ formula · finite production rate</span>
201      </div>
202    </div>
203    <div class="tabs" role="tablist">
204      <button class="tab active" id="tabSingle" role="tab" aria-selected="true">Single SKU</button>
205      <button class="tab" id="tabBatch" role="tab" aria-selected="false">Multi-SKU batch<span class="tab-tag">CSV</span></button>
206    </div>
207
208    <div id="singlePanel">
209    <div class="calc-body">
210      <div class="inputs">
211        <div class="field">
212          <label for="D">Annual demand <span class="unit">D — units/year</span></label>
213          <input type="number" id="D" value="120000" min="0" step="any">
214        </div>
215        <div class="field">
216          <label for="S">Setup / order cost <span class="unit" id="uS">S — $ per run</span></label>
217          <input type="number" id="S" value="350" min="0" step="any">
218        </div>
219        <div class="field">
220          <label for="H">Holding cost <span class="unit" id="uH">H — $ / unit / year</span></label>
221          <input type="number" id="H" value="3.50" min="0" step="any">
222        </div>
223        <div class="field">
224          <label for="p">Production rate <span class="unit">p — units/day</span></label>
225          <input type="number" id="p" value="900" min="0" step="any">
226        </div>
227        <div class="field">
228          <label for="wd">Working days / year <span class="unit">used to derive daily demand</span></label>
229          <input type="number" id="wd" value="250" min="1" step="any">
230          <div class="derived-d" id="dDerived">Daily demand d = 480 units/day</div>
231        </div>
232        <button class="btn" id="calcBtn">Calculate EPQ</button>
233        <button type="button" class="ex-link" id="exBtn">↻ Load a worked example</button>
234        <div class="err" id="errBox"></div>
235      </div>
236
237      <div class="results" id="results">
238        <div class="res-placeholder" id="placeholder">Enter your values and press <b>Calculate EPQ</b> to see the optimal lot size and full cost breakdown.</div>
239        <div id="resContent" style="display:none">
240          <div class="res-primary">
241            <div class="rl">Economic Production Quantity</div>
242            <div class="rv" id="epqVal">—</div>
243            <div class="ru">units per production run</div>
244          </div>
245          <div class="res-group">
246            <div class="rg-label">Sizing &amp; scheduling</div>
247            <div class="res-grid">
248              <div class="rc"><div class="l">Max inventory</div><div class="v" id="maxInv">—</div></div>
249              <div class="rc"><div class="l">Runs / year</div><div class="v" id="runs">—</div></div>
250              <div class="rc"><div class="l">Run time</div><div class="v" id="runTime">—</div></div>
251              <div class="rc"><div class="l">Cycle length</div><div class="v" id="cycle">—</div></div>
252            </div>
253          </div>
254          <div class="res-group">
255            <div class="rg-label">Cost</div>
256            <div class="res-grid">
257              <div class="rc"><div class="l">Total annual cost</div><div class="v" id="tac">—</div></div>
258              <div class="rc"><div class="l">
258Cost / unit</div><div class="v" id="cpu">—</div></div>
259            </div>
260          </div>
261          <div class="vs-strip">
262            <div class="vt">vs. plain EOQ (instant replenishment)</div>
263            <div class="vs-row"><span class="k">EOQ lot size</span><span class="n" id="eoqVal">—</span></div>
264            <div class="vs-row"><span class="k">EPQ is larger by</span><span class="n" id="eoqDiff">—</span></div>
265            <div class="vs-row"><span class="k">Annual holding cost saved</span><span class="n good" id="eoqSave">—</span></div>
266            <div class="vs-row total"><span class="k">Extra total cost/yr if you'd used EOQ</span><span class="n" id="eoqPenalty">—</span></div>
267          </div>
268          <div class="export-row"><button class="btn-sec" id="copyLink">🔗 Copy link</button><button class="btn-sec" id="exportSingleBtn">Export to Excel</button></div>
269          <div class="handoff"><a class='btn-next' href='/safety-stock' id='toSs'><span>Continue → Safety Stock</span><span class="arr">→</span></a></div>
270        </div>
271      </div>
272    </div>
273    </div><!-- /#singlePanel -->
274
275    <div id="batchPanel" style="display:none">
276      <div class="upload" id="dropZone">
277        <input type="file" id="csvFile" accept=".csv,text/csv" hidden>
278        <div class="up-ico">&#11014;</div>
279        <div class="up-main">Drop a CSV here or <button type="button" class="link" id="browseBtn">browse your files</button></div>
280        <div class="up-sub">Columns: <code>SKU, D, S, H, p</code> &nbsp;·&nbsp; optional <code>wd</code> &nbsp;·&nbsp; <button type="button" class="link" id="sampleBtn">download a sample</button></div>
281      </div>
282      <div class="batch-note">Rows without a <code>wd</code> column use Working days = <b id="wdEcho">250</b> (from the Single SKU tab).</div>
283      <div class="err" id="batchErr"></div>
284      <div id="batchResultsWrap" style="display:none">
285        <div class="batch-bar">
286          <div class="batch-sum" id="batchSum">—</div>
287          <button class="btn-sec" id="exportBatchBtn">Export to Excel</button>
288        </div>
289        <div class="table-scroll">
290          <table class="btab" id="batchTable"><thead></thead><tbody></tbody></table>
291        </div>
292      </div>
293    </div>
294  </div>
295
296  <p class="method"><b>Method</b> — full Economic Production Quantity model: finite production rate (requires p &gt; d), constant demand, and no stockouts. Daily demand d = annual demand D ÷ working days.</p>
297
298  <!-- ============ COST CURVE CHART ============ -->
299  <div class="chart-sec" id="chartSec" style="display:none">
300    <h3>Total cost curve</h3>
301    <p class="csub">Setup cost falls as lot size grows; holding cost rises. EPQ sits at the minimum of the combined curve.</p>
302    <div class="chart-box"><canvas id="chart" role="img" aria-label="Total cost curve: total cost is lowest at the EPQ lot size, where the falling setup-cost curve meets the rising holding-cost curve. Exact values are listed in the results above."></canvas></div>
303    <div class="legend">
304      <span><i style="background:var(--signal)"></i> Total cost</span>
305      <span><i style="background:var(--steel)"></i> Setup cost</span>
306      <span><i style="background:var(--muted)"></i> Holding cost</span>
307      <span><i style="background:var(--go)"></i> EPQ (optimum)</span>
308    </div>
309  </div>
310
311  <!-- ============ AFFILIATE CTA (P2) — renders only when MONETIZE.affiliate_url is set ============ -->
312  <div id="ctaErp"></div>
313
314  <!-- ============ CONTENT (SEO / E-E-A-T / AEO) ============ -->
315  <section class="content">
316    <h2>What is Economic Production Quantity?</h2>
317    <p>Economic Production Quantity (EPQ) is the production lot size that minimizes the total of setup cost and inventory holding cost when you <strong>manufacture an item yourself</strong>. It is the production-side counterpart of the Economic Order Quantity (EOQ).</p>
318    <p>The key difference: EOQ assumes a full order arrives instantly in one delivery, so peak inventory equals the whole order. EPQ assumes you build units gradually at a finite production rate while you are also consuming them — so inventory builds up slowly and peaks below the full lot. That lowers average holding cost and pushes the optimal lot size higher than EOQ.</p>
319
320    <h2>The EPQ formula</h2>
321    <div class="formula">
322      EPQ = &radic;( (2 &times; <span class="var">D</span> &times; <span class="var">S</span>) / ( <span class="var">H</span> &times; (1 &minus; <span class="var">d</span>/<span class="var">p</span>) ) )
323      <span class="sub">D = annual demand (units/year) &nbsp;·&nbsp; S = setup cost per run ($) &nbsp;·&nbsp; H = holding cost per unit per year ($)<br>d = daily demand rate &nbsp;·&nbsp; p = daily production rate &nbsp;·&nbsp; requires p &gt; d</span>
324    </div>
325    <div class="callout">The term <strong>(1 − d/p)</strong> is what separates EPQ from EOQ. As your production rate <em>p</em> gets very large relative to demand <em>d</em>, this term approaches 1 and EPQ collapses back to EOQ — because instant production is the same as instant delivery.</div>
326
327    <h3>The variables, and where each number comes from</h3>
328    <table class="vars">
329      <thead><tr><th>Symbol</th><th>Meaning</th><th>Unit</th><th>Where you get it</th></tr></thead>
330      <tbody>
331        <tr><td class="sym">D</td><td>Annual demand</td><td>units / year</td><td>Sales forecast or last 12 months of shipments for the item</td></tr>
332        <tr><td class="sym">S</td><td>Setup cost per run</td><td>currency / run</td><td>Changeover labour + scrapped first-off parts + machine time lost to the changeover</td></tr>
333        <tr><td class="sym">H</td><td>Holding cost per unit per year</td><td>currency / unit / year</td><td>Cost of capital + storage + insurance + obsolescence, as an annual rate on one unit</td></tr>
334        <tr><td class="sym">p</td><td>Daily production rate</td><td>units / day</td><td>Good units this line produces in one working day when running this item</td></tr>
335        <tr><td class="sym">d</td><td>Daily demand rate</td><td>units / day</td><td>Derived, not entered: <em>d</em> = D ÷ working days per year</td></tr>
336      </tbody>
337    </table>
338    <p class="note">The model requires <strong>p &gt; d</strong>. If your line cannot out-produce demand on the days it runs, no lot size fixes that — it is a capacity problem, and the calculator will tell you so instead of returning a number.</p>
339
340    <h2>EPQ formula worked example</h2>
341    <p>A plant makes one gearbox variant in-house. Annual demand is 12,000 units over 250 working days, a changeover costs 450 per run, holding one unit for a year costs 3.20, and the line produces 200 good units per day.</p>
342    <table class="vars">
343      <thead><tr><th>Input</th><th>Value</th></tr></thead>
344      <tbody>
345        <tr><td>Annual demand <span class="sym">D</span></td><td>12,000 units / year</td></tr>
346        <tr><td>Setup cost per run <span class="sym">S</span></td><td>450 per run</td></tr>
347        <tr><td>Holding cost <span class="sym">H</span></td><td>3.20 per unit per year</td></tr>
348        <tr><td>Production rate <span class="sym">p</span></td><td>200 units / day</td></tr>
349        <tr><td>Working days per year</td><td>250</td></tr>
350      </tbody>
351    </table>
352    <p><strong>Step 1 — get the daily demand rate.</strong> The formula needs <em>d</em> in the same time unit as <em>p</em>:<br>
353    <span class="calcline">d = D ÷ working days = 12,000 ÷ 250 = <strong>48 units / day</strong></span></p>
354    <p><strong>Step 2 — evaluate the production-rate term.</strong> This is the part EOQ does not have:<br>
355    <span class="calcline">1 − d/p = 1 − 48/200 = 1 − 0.24 = <strong>0.76</strong></span></p>
356    <p><strong>Step 3 — substitute into the EPQ formula.</strong><br>
357    <span class="calcline">EPQ = √( (2 × 12,000 × 450) ÷ (3.20 × 0.76) )</span><br>
358    <span class="calcline">EPQ = √( 10,800,000 ÷ 2.432 )</span><br>
359    <span class="calcline">EPQ = √4,440,789 = <strong>2,107 units per run</strong></span></p>
360    <p><strong>Step 4 — read off the scheduling numbers.</strong> The lot size alone does not tell you when to run:</p>
361    <table class="vars">
362      <thead><tr><th>Result</th><th>Value</th><th>How it is derived</th></tr></thead>
363      <tbody>
364        <tr><td>Optimal lot size (EPQ)</td><td><strong>2,107 units</strong></td><td>the formula above</td></tr>
365        <tr><td>Peak inventory</td><td>1,602 units</td><td>EPQ × (1 − d/p) = 2,107 × 0.76</td></tr>
366        <tr><td>Runs per year</td><td>5.7</td><td>D ÷ EPQ = 12,000 ÷ 2,107</td></tr>
367        <tr><td>Run time per batch</td><td>10.5 days</td><td>EPQ ÷ p = 2,107 ÷ 200</td></tr>
368        <tr><td>Cycle length between runs</td><td>43.9 days</td><td>EPQ ÷ d = 2,107 ÷ 48</td></tr>
369        <tr><td>Total annual cost at the optimum</td><td>5,125</td><td>√(2 × D × S × H × (1 − d/p))</td></tr>
370        <tr><td>Cost per unit</td><td>0.427</td><td>total annual cost ÷ D</td></tr>
371      </tbody>
372    </table>
373    <p><strong>Step 5 — compare against EOQ to see what the production rate bought you.</strong> Ignoring the finite production rate gives EOQ = √(2 × 12,000 × 450 ÷ 3.20) = <strong>1,837 units</strong>. So EPQ is <strong>14.7% larger</strong> than EOQ here, and because inventory peaks at 1,602 instead of the full lot, holding cost drops by about <strong>377 per year</strong>. Batch to the EOQ number and you change over 6.5 times a year instead of 5.7 — paying for roughly one extra setup you did not need.</p>
374    <p class="note">Every figure above is what this calculator returns for those inputs. Press <em>Load a worked example</em> in the calculator to run them yourself and see the cost curve.</p>
375
376    <h2>Where the EPQ formula comes from (derivation)</h2>
377    <p>
377The formula is not arbitrary — it is the minimum of a total-cost curve. Two costs pull in opposite directions as the lot size <em>Q</em> changes, and EPQ is the lot size where their sum is smallest.</p>
378
379    <h3>1. Total annual cost as a function of lot size</h3>
380    <p>Bigger lots mean fewer setups per year, so setup cost falls as <em>Q</em> rises. Bigger lots also mean more stock sitting around, so holding cost climbs as <em>Q</em> rises:</p>
381    <div class="formula">
382      TC(<span class="var">Q</span>) = (<span class="var">D</span>/<span class="var">Q</span>) &times; <span class="var">S</span> &nbsp;+&nbsp; (<span class="var">Q</span>/2) &times; (1 &minus; <span class="var">d</span>/<span class="var">p</span>) &times; <span class="var">H</span>
383      <span class="sub">left term = annual setup cost &nbsp;·&nbsp; right term = annual holding cost</span>
384    </div>
385    <p>The <strong>D/Q</strong> in the first term is simply how many runs you make per year. The second term is average inventory multiplied by the holding rate — and that is where the production rate enters.</p>
386
387    <h3>2. Why average inventory is Q/2 × (1 − d/p), not Q/2</h3>
388    <p>Under EOQ a full order lands at once, so stock jumps to <em>Q</em> and then drains to zero: average inventory is <em>Q</em>/2. Under EPQ you are producing and consuming at the same time. During the run, stock grows at only (<em>p</em> − <em>d</em>) per day rather than jumping, so it never reaches <em>Q</em> — it tops out at <em>Q</em> × (1 − <em>d</em>/<em>p</em>). Halve that peak and you get average inventory. This single change is the whole difference between the two models.</p>
389
390    <h3>3. Setting the derivative to zero</h3>
391    <p>TC(<em>Q</em>) is a convex curve, so its minimum is where the slope is flat. Differentiating with respect to <em>Q</em>:</p>
392    <div class="formula">
393      dTC/d<span class="var">Q</span> = &minus;<span class="var">D</span><span class="var">S</span>/<span class="var">Q</span>&sup2; &nbsp;+&nbsp; <span class="var">H</span>(1 &minus; <span class="var">d</span>/<span class="var">p</span>)/2 = 0
394    </div>
395    <p>Rearranging for <em>Q</em> gives <em>Q</em>² = 2<em>DS</em> / (<em>H</em>(1 − <em>d</em>/<em>p</em>)), and taking the square root returns the EPQ formula. A useful consequence falls out of the same equation: at the optimum the two cost terms are <strong>equal</strong>. In the example above, annual setup cost and annual holding cost are each about 2,562 — half of the 5,125 total. That is a quick sanity check on any lot size you are handed.</p>
396
397    <h3>4. What the model assumes</h3>
398    <ul class="assump">
399      <li><strong>Demand is steady and known.</strong> Constant <em>d</em> per day, no seasonality. If demand is lumpy, EPQ sizes the average case — pair it with <a href='/safety-stock'>safety stock</a> for the variability.</li>
400      <li><strong>Production rate exceeds demand rate</strong> (<em>p</em> &gt; <em>d</em>), otherwise stock never accumulates and the model has no solution.</li>
401      <li><strong>Setup cost is fixed per run</strong> and does not depend on lot size.</li>
402      <li><strong>Holding cost is linear</strong> in average inventory — no step changes when a warehouse fills up.</li>
403      <li><strong>One item at a time.</strong> The single-item model ignores machines shared between products competing for the same capacity.</li>
404      <li><strong>No quantity discounts and no shortages.</strong> Every unit costs the same and stockouts are not permitted.</li>
405    </ul>
406    <p class="note">Real shop floors violate at least one of these. Treat EPQ as the starting point that setup and holding economics justify, then adjust for capacity, shelf life, and pallet or container sizes.</p>
407
408    <h3>EOQ vs EPQ side by side</h3>
409    <table class="vars">
410      <thead><tr><th>&nbsp;</th><th>EOQ</th><th>EPQ</th></tr></thead>
411      <tbody>
412        <tr><td>Situation</td><td>You buy the item</td><td>You manufacture the item</td></tr>
413        <tr><td>How stock arrives</td><td>Whole order at once</td><td>Gradually, during the production run</td></tr>
414        <tr><td>Peak inventory</td><td>Q</td><td>Q × (1 − d/p)</td></tr>
415        <tr><td>Average inventory</td><td>Q / 2</td><td>Q/2 × (1 − d/p)</td></tr>
416        <tr><td>Formula</td><td>√(2DS / H)</td><td>√(2DS / (H(1 − d/p)))</td></tr>
417        <tr><td>Resulting lot size</td><td>Smaller</td><td>Larger</td></tr>
418        <tr><td>Example above</td><td>1,837 units</td><td>2,107 units</td></tr>
419      </tbody>
420    </table>
421    <p>EOQ is the special case of EPQ where production is instant. Push <em>p</em> far above <em>d</em> and (1 − <em>d</em>/<em>p</em>) approaches 1, at which point the two formulas are identical.</p>
422
423    <h3>Why it matters on the shop floor</h3>
424    <p>Using EOQ when you actually produce in-house systematically under-sizes your runs. You end up changing over more often than necessary, burning setup time and capacity. EPQ al
424so tells you the <strong>run time per batch</strong> and the <strong>cycle length</strong> between runs — the numbers you need to schedule production, not just size it.</p>
425
426    <h2>Frequently asked questions</h2>
427    <div class="faq">
428      <details><summary>What is the Economic Production Quantity (EPQ)?</summary><div class="fa">EPQ is the optimal production lot size that minimizes combined setup and holding cost when a company produces inventory in-house at a finite rate, rather than receiving a full order at once.</div></details>
429      <details><summary>How is EPQ different from EOQ?</summary><div class="fa">EOQ assumes inventory arrives instantly in one delivery, so peak inventory equals the whole order. EPQ assumes you produce units gradually while also consuming them, so peak inventory stays below the full lot. This makes EPQ larger than EOQ and lowers average holding cost.</div></details>
430      <details><summary>What is the EPQ formula?</summary><div class="fa">EPQ = &radic;((2 × D × S) / (H × (1 − d/p))), where D is annual demand, S is setup cost per run, H is annual holding cost per unit, d is the daily demand rate, and p is the daily production rate. The model requires p &gt; d.</div></details>
431      <details><summary>When should I use EPQ instead of EOQ?</summary><div class="fa">Use EPQ when you manufacture the item yourself and production happens over time at a finite rate. Use EOQ when you purchase the item and receive the full order in a single delivery.</div></details>
432      <details><summary>How do you derive the EPQ formula?</summary><div class="fa">Write total annual cost as TC(Q) = (D/Q)S + (Q/2)(1 − d/p)H, where the first term is setup cost and the second is holding cost on the reduced average inventory. Differentiate with respect to Q, set dTC/dQ = −DS/Q² + H(1 − d/p)/2 = 0, and solve: Q² = 2DS / (H(1 − d/p)). The square root of that is EPQ. At the optimum the annual setup and holding costs are equal.</div></details>
433      <details><summary>Why does the EPQ formula include (1 − d/p)?</summary><div class="fa">Because you consume units while you are still producing them. Inventory grows at only (p − d) per day during a run instead of arriving all at once, so it peaks at Q × (1 − d/p) rather than Q. That lower average inventory reduces holding cost, which makes the optimal lot size larger than EOQ. If p is far greater than d, the term approaches 1 and EPQ becomes EOQ.</div></details>
434      <details><summary>What does EPQ assume?</summary><div class="fa">Steady known demand, a production rate greater than the demand rate, a fixed setup cost per run independent of lot size, holding cost that is linear in average inventory, a single item without shared-capacity competition, and no quantity discounts or stockouts. Most real shop floors break at least one assumption, so treat EPQ as an economics-justified starting point and then adjust for capacity, shelf life, and pallet sizes.</div></details>
435      <details><summary>Can EPQ be smaller than EOQ?</summary><div class="fa">No. Since 0 &lt; (1 − d/p) &lt; 1 whenever p &gt; d, dividing by it makes the value under the square root larger, so EPQ is always greater than or equal to EOQ for the same D, S and H. It equals EOQ only in the limit of instant production.</div></details>
436    </div>
437  </section>
438
439  <!-- ============ EMBED WIDGET (F1) — give the tool away → backlinks + reach ============ -->
440  <section class="embed-sec">
441    <h2>Put this calculator on your site — free</h2>
442    <p>Teaching operations, writing about inventory, or building an internal wiki? Embed the live EPQ calculator in one paste. No signup, no cost — it keeps working on your page and links back here.</p>
443    <div class="embed-grid">
444      <div class="embed-preview">
445        <div class="embed-label">Live preview</div>
446        <iframe src="embed.html" title="EPQ Calculator — live preview" loading="lazy"></iframe>
447      </div>
448      <div class="embed-code">
449        <div class="embed-code-head"><span>Copy this HTML</span><button type="button" class="btn-copy" id="copyEmbed">Copy code</button></div>
450        <textarea id="embedSnippet" readonly spellcheck="false" aria-label="Embed HTML snippet"></textarea>
451        <p class="embed-hint">Paste anywhere in your page's HTML. The calculator runs in a sandboxed frame — it won't touch the rest of your site.</p>
452      </div>
453    </div>
454  </section>
455
456  <div id="related" class="related"></div>
457  <section id="credibility" class="credibility"></section>
458  <div id="badges" class="badges"></div>
459  <footer>
460    <span class="mono">EPQ Calculator · free tool</span>
461    <span class="mono">Manufacturing suite · 1 / 3</span>
462    <a class='mono' href='/privacy'>Privacy</a>
463  </footer>
464
465</div>
466
467<script src="suite.js"></script>
vendor: 1 bytes, line 467
467
468<script>
469/* =========================================================================
470   EPQ CALCULATOR — page-specific logic.
471   Shared helpers ($, fmt, MONETIZE, canonical fix, copy-link, self-check
472   banner, suite nav, affiliate CTA) live in suite.js — loaded above.
473   EPQ = sqrt( 2DS / (H * (1 - d/p)) ),  d = D / working days
474   ========================================================================= */
475let CURSYM = '$';   // F8 — currency symbol (label only; user enters costs in their own currency, no FX conversion)
476const money = n => CURSYM+n.toLocaleString('en-US',{minimumFractionDigits:0,maximumFractionDigits:0});
477let lastResult = null;
478
479function getInputs(){
480  return { D:+$('D').value, S:+$('S').value, H:+$('H').value, p:+$('p').value, wd:+$('wd').value };
481}
482function updateDerivedD(){
483  const {D,wd}=getInputs();
484  if(D>0&&wd>0){ $('dDerived').textContent='Daily demand d = '+fmt(D/wd,0)+' units/day'; }
485}
486
487/* pure EPQ core — single source of truth, reused by single + batch. */
488function epqCore(inp){
489  const D=+inp.D, S=+inp.S, H=+inp.H, p=+inp.p, wd=+inp.wd;
490  if(!(D>0&&S>0&&H>0&&p>0&&wd>0)) return {ok:false,error:'All inputs must be greater than zero'};
491  const d=D/wd;
492  if(p<=d) return {ok:false,error:'Production rate p ('+fmt(p,0)+') must exceed daily demand d ('+fmt(d,0)+') — you cannot keep up with demand otherwise'};
493  const factor=1-d/p;
494  const epq=Math.sqrt((2*D*S)/(H*factor));
495  const tac=Math.sqrt(2*D*S*H*factor);   // optimal total annual cost
496  return {ok:true, D,S,H,p,wd,d,factor,epq,
497    maxInv:epq*factor, runs:D/epq, runTime:epq/p, cycle:epq/d,
498    tac, cpu:tac/D, eoq:Math.sqrt((2*D*S)/H)};
499}
500
501function compute(){
502  const r=epqCore(getInputs());
503  if(!r.ok){ showErr('errBox', r.error); return; }
504  hideErr('errBox');
505  lastResult=r;
506
507  const eoqDiffPct=((r.epq-r.eoq)/r.eoq)*100;
508  const saved=(r.H/2)*r.eoq-(r.H/2)*r.maxInv;   // holding-cost saved vs EOQ
509
510  $('epqVal').textContent=fmt(r.epq,0);
511  $('maxInv').textContent=fmt(r.maxInv,0)+' u';
512  $('runs').textContent=fmt(r.runs,1)+' /yr';
513  $('runTime').textContent=fmt(r.runTime,1)+' days';
514  $('cycle').textContent=fmt(r.cycle,1)+' days';
515  $('tac').textContent=money(r.tac);
516  $('cpu').textContent=CURSYM+r.cpu.toFixed(3);
517  $('eoqVal').textContent=fmt(r.eoq,0)+' u';
518  $('eoqDiff').textContent='+'+eoqDiffPct.toFixed(1)+'%';
519  $('eoqSave').textContent=money(Math.max(0,saved))+' /yr';
520  const tcEoq=(r.D/r.eoq)*r.S+(r.H/2)*r.eoq*r.factor;   // F4 — what EOQ-sized lots actually cost in production (≥ EPQ's optimum)
521  $('eoqPenalty').textContent=money(Math.max(0,tcEoq-r.tac))+' /yr';
522
523  $('placeholder').style.display='none';
524  $('resContent').style.display='block';
525  $('chartSec').style.display='block';
526  // hand off daily demand + annual demand to the Safety Stock step (integrated suite)
527  $('toSs').href='safety-stock.html?d='+(+r.d.toFixed(4))+'&D='+r.D+'&wd='+r.wd;
528  history.replaceState(null,'',location.pathname+'?D='+r.D+'&S='+r.S+'&H='+r.H+'&p='+r.p+'&wd='+r.wd);  // shareable URL
529  drawChart(r);
530}
531
532/* =========================================================================
533   COST CURVE CHART — vanilla canvas, no dependencies
534   TC(Q) = (D/Q)*S  +  (H/2)*Q*(1 - d/p)
535   ========================================================================= */
536function drawChart(r){
537  const cv=$('chart'), ctx=cv.getContext('2d');
538  const dpr=window.devicePixelRatio||1;
539  const W=cv.clientWidth, Hpx=cv.clientHeight;
540  cv.width=W*dpr; cv.height=Hpx*dpr; ctx.scale(dpr,dpr);
541  ctx.clearRect(0,0,W,Hpx);
542
543  // read colors from the active theme (canvas can't inherit CSS vars) so the chart adapts to dark mode
544  const cs=getComputedStyle(document.documentElement), v=n=>cs.getPropertyValue(n).trim();
545  const C={grid:v('--line-2'),axis:v('--muted-2'),setup:v('--steel'),hold:v('--muted'),total:v('--signal'),mark:v('--go'),ink:v('--ink')};
546
547  const pad={l:48,r:14,t:12,b:28};
548  const qMax=r.epq*2.4, qMin=Math.max(1,r.epq*0.12);
549  const setup=q=>(r.D/q)*r.S;
550  const hold=q=>(r.H/2)*q*r.factor;
551  const total=q=>setup(q)+hold(q);
552
553  let yMax=0;
554  for(let i=0;i<=100;i++){const q=qMin+(qMax-qMin)*i/100; yMax=Math.max(yMax,total(q));}
555  yMax*=1.05;
556
557  const X=q=>pad.l+( (q-qMin)/(qMax-qMin) )*(W-pad.l-pad.r);
558  const Y=v=>Hpx-pad.b-( v/yMax )*(Hpx-pad.t-pad.b);
559
560  // grid + y labels
561  ctx.strokeStyle=C.grid;
561 ctx.fillStyle=C.axis;
562  ctx.font='10px "IBM Plex Mono", monospace'; ctx.textAlign='right';
563  for(let i=0;i<=4;i++){
564    const v=yMax*i/4, y=Y(v);
565    ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(W-pad.r,y);ctx.stroke();
566    ctx.fillText(CURSYM+Math.round(v/1000)+'k',pad.l-6,y+3);
567  }
568
569  function curve(fn,color,w){
570    ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=w;
571    for(let i=0;i<=120;i++){const q=qMin+(qMax-qMin)*i/120; const x=X(q),y=Y(fn(q)); i?ctx.lineTo(x,y):ctx.moveTo(x,y);}
572    ctx.stroke();
573  }
574  curve(setup,C.setup,1.5);
575  curve(hold,C.hold,1.5);
576  curve(total,C.total,2.5);
577
578  // EPQ marker
579  const ex=X(r.epq), ey=Y(total(r.epq));
580  ctx.strokeStyle=C.mark;ctx.lineWidth=1;ctx.setLineDash([4,4]);
581  ctx.beginPath();ctx.moveTo(ex,Hpx-pad.b);ctx.lineTo(ex,ey);ctx.stroke();ctx.setLineDash([]);
582  ctx.fillStyle=C.mark;ctx.beginPath();ctx.arc(ex,ey,5,0,7);ctx.fill();
583  ctx.fillStyle=C.ink;ctx.textAlign='center';ctx.font='600 11px "Space Grotesk"';
584  // label sits just above the marker dot (was at the x-axis → collided with the "lot size" caption on narrow screens)
585  ctx.fillText('EPQ '+fmt(r.epq,0), ex, Math.max(pad.t+11, ey-11));
586
587  // x-axis label
588  ctx.fillStyle=C.axis;ctx.font='10px "IBM Plex Mono"';ctx.textAlign='center';
589  ctx.fillText('lot size (units) →',W/2,Hpx-4);
590}
591
592// events
593let liveTimer;   // F9 — live recalc: recompute as you type (debounced 250ms), only when inputs are valid so it never flashes errors mid-typing
594['D','S','H','p','wd'].forEach(id=>$(id).addEventListener('input',()=>{
595  updateDerivedD();
596  clearTimeout(liveTimer);
597  liveTimer=setTimeout(()=>{ if(epqCore(getInputs()).ok) compute(); },250);
598}));
599$('calcBtn').addEventListener('click',()=>{ compute(); if(lastResult&&lastResult.ok) trackEvent('calculate',{mode:'single'}); });
600['D','S','H','p','wd'].forEach(id=>$(id).addEventListener('keydown',e=>{if(e.key==='Enter')compute();}));
601window.addEventListener('resize',()=>{if(lastResult)drawChart(lastResult);});
602
603// init — prefill from a shared link (?D&S&H&p&wd), then compute (suite.js already
604// captured HAS_SELFCHECK before this replaceState strips ?selfcheck)
605(function(){
606  const q=new URLSearchParams(location.search);
607  ['D','S','H','p','wd'].forEach(k=>{ if(q.has(k)){ const v=parseFloat(q.get(k)); if(isFinite(v)) $(k).value=v; } });
608})();
609updateDerivedD();
610compute();
611
612renderSuiteNav('epq');                 // shared suite nav (suite.js)
613renderTrust('trust');                  // F6 — trust / credibility strip (suite.js)
614renderRelated('epq');                  // F2 — related-tools cluster (suite.js)
615renderCredibility('epq');              // ownership + method references (suite.js)
616renderBadges('badges');                // directory "featured on" badges (suite.js)
617wireCopyLink('copyLink');              // shared copy-link (suite.js)
618window.addEventListener('themechange',()=>{ if(lastResult) drawChart(lastResult); });  // canvas repaint on dark/light toggle
619renderAffiliateCTA('ctaErp','Stop recalculating lot sizes by hand — keep EPQ, safety stock &amp; reorder points live in your ERP/MES.');
620
621/* F1 — embed widget snippet: iframe + attribution link. The attribution <a>
622   sits OUTSIDE the iframe on purpose — that's the real backlink on the host
623   page (a link inside the frame would just credit our own domain). Hardcode the
624   production origin so a copied snippet never points at localhost/preview. */
625(function(){
626  const origin='https://epq-calculator.netlify.app';
627  const snippet='<iframe src="'+origin+'/embed.html" title="EPQ Calculator" width="100%" height="800" style="max-width:460px;border:1px solid #e0e5e8;border-radius:10px" loading="lazy"></iframe>\n'
628    +'<p style="font:13px/1.5 system-ui,sans-serif;color:#5c6975;margin:8px 0 0">Free <a href="'+origin+'/">EPQ Calculator</a> by epq-calculator.netlify.app</p>';
629  const ta=$('embedSnippet'); if(ta) ta.value=snippet;
630  const b=$('copyEmbed');
631  if(b) b.addEventListener('click',async()=>{
632    try{ await navigator.clipboard.writeText(snippet); }
633    catch(e){ ta.focus(); ta.select(); try{document.execCommand('copy');}catch(_){} }
634    const old=b.textContent; b.textContent='✓ Copied'; setTimeout(()=>{ b.textContent=old; },1500);
635  });
636})();
637
638/* =========================================================================
639   MULTI-SKU BATCH (P1 #1) + EXCEL EXPORT (P1 #2)
640   ========================================================================= */
641let batchResults=[], xlsxPromise=null;
642const round=(n,d=0)=>(n==null||isNaN(n))?'':+(+n).toFixed(d);
643const esc=s=>(s==null?'':s).toString().replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
644
645/* CSV parse: RFC-4180-ish state machine — quotes, escaped "", CRLF/LF, BOM, blanks.
646   ponytail: covers what Excel/Sheets actually export. Ceiling = assumes UTF-8 text;
647   upgrade path = PapaParse if we ever need other encodings or streaming. */
648function parseCSV(text){
649  text=text.replace(/^/,'');
650  const rows=[]; let row=[], field='', q=false;
651  for(let i=0;i<text.length;i++){
652    const c=text[i];
653    if(q){
654      if(c==='"'){ if(text[i+1]==='"'){field+='"';i++;} else q=false; }
655      else field+=c;
656    } else if(c==='"') q=true;
657    else if(c===','){ row.push(field); field=''; }
658    else if(c==='\n'||c==='\r'){
659      if(c==='\r'&&text[i+1]==='\n') i++;
660      row.push(field); field='';
661      if(row.some(x=>x!=='')) rows.push(row);
662      row=[];
663    } else field+=c;
664  }
665  row.push(field);
666  if(row.some(x=>x!=='')) rows.push(row);
667  return rows;
668}
669
670/* map rows → {sku,D,S,H,p,wd}. Named header (any order) wins; else a non-numeric
671   first row is treated as an unrecognised header and skipped; else positional.
672   ponytail: positional fallback assumes column order SKU,D,S,H,p[,w
672d]. */
673const COLS=['sku','d','s','h','p','wd'];
674function rowsToRecords(rows){
675  if(!rows.length) return [];
676  const first=rows[0].map(x=>x.trim().toLowerCase());
677  const named=first.some(x=>COLS.includes(x));
678  const looksHeader=[1,2,3,4].some(i=>first[i]===undefined||isNaN(parseFloat(first[i])));
679  let idx={sku:0,d:1,s:2,h:3,p:4,wd:5}, data=rows;
680  if(named){ idx={}; first.forEach((n,i)=>{ if(COLS.includes(n)) idx[n]=i; }); data=rows.slice(1); }
681  else if(looksHeader){ data=rows.slice(1); }
682  if(!named && idx.d===undefined) idx={sku:0,d:1,s:2,h:3,p:4,wd:5};   // no named header → positional; NEVER overwrite a partial named mapping (that would silently corrupt data)
683  if(named&&idx.sku===undefined){                           // SKU column named something else (Product/Item/Part): take first unused column
684    const used=new Set([idx.d,idx.s,idx.h,idx.p,idx.wd]);
685    for(let i=0;i<first.length;i++){ if(!used.has(i)){ idx.sku=i; break; } }
686  }
687  return data.map((r,n)=>({
688    sku:(r[idx.sku]==null?'':r[idx.sku]).trim()||('Row '+(n+1)),
689    D:r[idx.d], S:r[idx.s], H:r[idx.h], p:r[idx.p],
690    wd:(idx.wd!==undefined&&r[idx.wd]!==undefined&&r[idx.wd].trim()!=='')?r[idx.wd]:null
691  }));
692}
693
694function handleFile(file){
695  if(!file) return;
696  const reader=new FileReader();
697  reader.onload=e=>{
698    try{
699      const recs=rowsToRecords(parseCSV(e.target.result));
700      if(!recs.length){ showErr('batchErr','No data rows found in that CSV.'); return; }
701      hideErr('batchErr');
702      const defWd=+$('wd').value>0?+$('wd').value:250;
703      batchResults=recs.map(rec=>{
704        const wd=(rec.wd!=null&&+rec.wd>0)?+rec.wd:defWd;
705        return {sku:rec.sku, input:{D:+rec.D,S:+rec.S,H:+rec.H,p:+rec.p,wd}, ...epqCore({D:rec.D,S:rec.S,H:rec.H,p:rec.p,wd})};
706      });
707      renderBatch();
708      trackEvent('calculate',{mode:'batch'});
709    }catch(err){ showErr('batchErr','Could not parse that CSV: '+err.message); }
710  };
711  reader.onerror=()=>showErr('batchErr','Could not read that file.');
712  reader.readAsText(file);
713}
714
715const BHEAD=['SKU','EPQ','Max inv','Runs/yr','Run time','Cycle','Annual cost','Cost/u'];
716function renderBatch(){
717  const ok=batchResults.filter(r=>r.ok), bad=batchResults.length-ok.length;
718  const total=ok.reduce((s,r)=>s+r.tac,0);
719  $('batchSum').innerHTML='<b>'+batchResults.length+'</b> SKUs · <b>'+ok.length+'</b> computed'
720    +(bad?' · <span class="warn">'+bad+' error'+(bad>1?'s':'')+'</span>':'')
721    +' · total annual cost <b>'+money(total)+'</b>';
722  $('batchTable').querySelector('thead').innerHTML='<tr>'+BHEAD.map(h=>'<th>'+h+'</th>').join('')+'</tr>';
723  $('batchTable').querySelector('tbody').innerHTML=batchResults.map(r=> r.ok
724    ? '<tr><td class="sku">'+esc(r.sku)+'</td><td class="num st">'+fmt(r.epq,0)+'</td><td class="num">'+fmt(r.maxInv,0)+'</td><td class="num">'+fmt(r.runs,1)+'</td><td class="num">'+fmt(r.runTime,1)+'d</td><td class="num">'+fmt(r.cycle,1)+'d</td><td class="num">'+money(r.tac)+'</td><td class="num">$'+r.cpu.toFixed(3)+'</td></tr>'
725    : '<tr class="bad"><td class="sku">'+esc(r.sku)+'</td>
725<td class="err-c" colspan="7">'+esc(r.error)+'</td></tr>'
726  ).join('');
727  $('batchResultsWrap').style.display='block';
728}
729
730/* Excel export via SheetJS — lazy-loaded so it never slows first paint.
731   ponytail: CDN load on first export; CSV fallback if the CDN is blocked. */
732function ensureXLSX(){
733  if(window.XLSX) return Promise.resolve(window.XLSX);
734  if(!xlsxPromise) xlsxPromise=new Promise((res,rej)=>{
735    const s=document.createElement('script');
736    s.src='https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js';
737    s.onload=()=>res(window.XLSX); s.onerror=()=>rej(new Error('CDN blocked'));
738    document.head.appendChild(s);
739  });
740  return xlsxPromise;
741}
742const csvCell=v=>{ v=(v==null?'':v).toString(); return /[",\r\n]/.test(v)?'"'+v.replace(/"/g,'""')+'"':v; };
743const toCSV=m=>m.map(r=>r.map(csvCell).join(',')).join('\r\n');
744function download(text,name,type='text/csv;charset=utf-8'){
745  const a=document.createElement('a'), url=URL.createObjectURL(new Blob([text],{type}));
746  a.href=url; a.download=name; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
747}
748async function exportExcel(matrix,sheet,fname){
749  try{
750    const XLSX=await ensureXLSX();
751    const wb=XLSX.utils.book_new();
752    XLSX.utils.book_append_sheet(wb,XLSX.utils.aoa_to_sheet(matrix),sheet);
753    XLSX.writeFile(wb,fname+'.xlsx');
754  }catch(e){ download(toCSV(matrix),fname+'.csv'); }
755}
756function batchMatrix(){
757  const head=['SKU','D','S','H','p','Working days','Daily demand d','EPQ','Max inventory','Runs/year','Run time (days)','Cycle (days)','Total annual cost','Cost/unit','EOQ','Status'];
758  return [head,...batchResults.map(r=> r.ok
759    ? [r.sku,r.input.D,r.input.S,r.input.H,r.input.p,r.input.wd,round(r.d,2),round(r.epq,0),round(r.maxInv,0),round(r.runs,2),round(r.runTime,2),round(r.cycle,2),round(r.tac,2),round(r.cpu,4),round(r.eoq,0),'OK']
760    : [r.sku,r.input.D,r.input.S,r.input.H,r.input.p,r.input.wd,'','','','','','','','','','ERROR: '+r.error])];
761}
762function singleMatrix(){
763  const r=lastResult;
764  return [['EPQ Calculator — single SKU result'],[],
765    ['Input',''],['Annual demand (D)',r.D],['Setup cost (S)',r.S],['Holding cost (H)',r.H],
766    ['Production rate (p)',r.p],['Working days',r.wd],['Daily demand (d)',round(r.d,2)],[],
767    ['Result',''],['EPQ (units)',round(r.epq,0)],['Max inventory',round(r.maxInv,0)],
768    ['Runs / year',round(r.runs,2)],['Run time (days)',round(r.runTime,2)],['Cycle (days)',round(r.cycle,2)],
769    ['Total annual cost',round(r.tac,2)],['Cost / unit',round(r.cpu,4)],['EOQ (units)',round(r.eoq,0)]];
770}
771
772const SAMPLE='SKU,D,S,H,p,wd\nWIDGET-A,120000,350,3.50,900,250\nWIDGET-B,48000,200,2.10,500,250\nGEAR-7,250000,540,5.00,1600,300\nVALVE-X,8000,12
7720,1.20,150,250\n"BRACKET, HD",18000,180,1.80,400,250';
773
774/* --- events --- */
775function setTab(single){
776  $('tabSingle').classList.toggle('active',single); $('tabBatch').classList.toggle('active',!single);
777  $('tabSingle').setAttribute('aria-selected',single); $('tabBatch').setAttribute('aria-selected',!single);
778  $('singlePanel').style.display=single?'':'none'; $('batchPanel').style.display=single?'none':'';
779}
780$('tabSingle').addEventListener('click',()=>setTab(true));
781$('tabBatch').addEventListener('click',()=>{ $('wdEcho').textContent=fmt(+$('wd').value||250,0); setTab(false); });
782$('browseBtn').addEventListener('click',()=>$('csvFile').click());
783$('csvFile').addEventListener('change',e=>handleFile(e.target.files[0]));
784$('sampleBtn').addEventListener('click',()=>{ download(SAMPLE,'epq-sample.csv'); trackEvent('download_sample',{mode:'batch'}); });
785$('exportSingleBtn').addEventListener('click',()=>{ if(lastResult&&lastResult.ok){ exportExcel(singleMatrix(),'EPQ','epq-result'); trackEvent('export_result',{mode:'single'}); } });
786$('exportBatchBtn').addEventListener('click',()=>{ if(batchResults.length){ exportExcel(batchMatrix(),'EPQ Batch','epq-batch'); trackEvent('export_result',{mode:'batch'}); } });
787$('wd').addEventListener('input',()=>{ $('wdEcho').textContent=fmt(+$('wd').value||250,0); });
788/* F3 — one-click worked example: fill a realistic scenario + compute (helps first-timers and AEO) */
789$('exBtn').addEventListener('click',()=>{
790  $('D').value=48000; $('S').value=650; $('H').value=4.20; $('p').value=600; $('wd').value=250;
791  updateDerivedD(); $('wdEcho').textContent='250'; compute(); trackEvent('load_example',{calculator:'epq'});
792});
793/* F8 — currency: swap the symbol across all monetary outputs + the S/H input labels (label only, no FX conversion) */
794function setCurrency(sym){
795  CURSYM=sym;
796  $('uS').textContent='S — '+sym+' per run';
797  $('uH').textContent='H — '+sym+' / unit / year';
798  if(lastResult&&lastResult.ok) compute();
799  if(batchResults.length) renderBatch();
800}
801$('cur').addEventListener('change',e=>setCurrency(e.target.value));
802(function(){const dz=$('dropZone');
803  ['dragenter','dragover'].forEach(ev=>dz.addEventListener(ev,e=>{e.preventDefault();dz.classList.add('drag');}));
804  ['dragleave','drop'].forEach(ev=>dz.addEventListener(ev,e=>{e.preventDefault();dz.classList.remove('drag');}));
805  dz.addEventListener('drop',e=>{ const f=e.dataTransfer.files[0]; if(f) handleFile(f); });
806})();
807
808/* --- self-check: open index.html?selfcheck=1 to run (shared banner from suite.js) --- */
809selfCheckBanner(function(a){
810  const r=epqCore({D:120000,S:350,H:3.5,p:900,wd:250});
811  a(r.ok&&Math.abs(r.epq-7171.4)<1,'EPQ ≈ 7171 (got '+(r.ok?r.epq.toFixed(1):r.error)+')');
812  a(r.ok&&Math.abs(r.tac-11713.2)<2,'TAC ≈ 11713');
813  a(r.ok&&Math.abs(r.eoq-4899)<2,'EOQ ≈ 4899');
814  a(!epqCore({D:120000,S:350,H:3.5,p:400,wd:250}).ok,'rejects p ≤ d');
815  a(!epqCore({D:0,S:1,H:1,p:1,wd:1}).ok,'rejects non-positive input');
816  const rows=parseCSV('SKU,D,S,H,p\r\n"GEAR, BIG",100,2,1,5\r\n');
817  a(rows.length===2&&rows[1][0]==='GEAR, BIG'&&rows[1][1]==='100','CSV: BOM + CRLF + quoted comma');
818  a(parseCSV('a,"x""y"\n')[0][1]==='x"y','CSV: escaped quote');
819  const rec=rowsToRecords(parseCSV('p,SKU,D,S,H\n5,Z1,100,2,1\n'))[0];
820  a(rec.sku==='Z1'&&+rec.D===100&&+rec.p===5,'header maps reordered columns');
821  const rec2=rowsToRecords(parseCSV('W1,1000,50,2,80\n'))[0];
822  a(rec2.sku==='W1'&&+rec2.D===1000,'positional (headerless) rows');
823  const rec3=rowsToRecords(parseCSV('sku,p,h,s,demand\nA,5,1,2,100\n'))[0];
824  a(isNaN(+rec3.D),'partial named header does not silently mis-map (unmapped D → row errors, not garbage)');
825  themeSelfChecks(a);   // dark-mode toggle system (shared from suite.js)
826});
827</script>
827
828</body>
829</html>

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.