[{"data":1,"prerenderedAt":2816},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report":2781,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report":2790,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report":2795,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report":2796},{"id":4,"title":5,"body":6,"dateModified":2747,"datePublished":2747,"description":2748,"extension":2749,"faq":2750,"keyword":2759,"kind":2760,"meta":2761,"navigation":325,"path":2770,"seo":2771,"slug":2774,"stem":2775,"topics":2776,"type":2779,"__hash__":2780},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report\u002Findex.md","Build an On-Time Delivery (OTIF) Report with Python",{"type":7,"value":8,"toc":2734},"minimark",[9,19,24,56,70,74,108,277,281,284,661,672,676,689,947,950,954,957,1127,1130,1134,1137,1345,1415,1418,1421,1623,1627,1630,2593,2601,2605,2650,2654,2660,2664,2670,2680,2686,2692,2696,2730],[10,11,12,13,18],"p",{},"On-time delivery is the number customers judge a supplier by, and the one that starts arguments internally: was the order late because it shipped late, because the carrier was slow, or because it arrived in two parts? An OTIF (on time, in full) report answers that only if the rules are precise: what counts as on time, what happens with split shipments, whether weekends count. Python makes the rules explicit and applies them to every order line the same way. This page is part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas numpy xlsxwriter\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50,53],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," numpy",[35,54,55],{"class":45}," xlsxwriter\n",[10,57,58,61,62,65,66,69],{},[32,59,60],{},"deliveries_q3.xlsx"," has two sheets, the shape most ERP and warehouse systems export: ",[32,63,64],{},"Orders",", one row per order line with the quantity ordered, the promised delivery date, the customer and the carrier; and ",[32,67,68],{},"Shipments",", one row per delivery against an order line, with the quantity delivered and the proof-of-delivery date. Some lines were delivered in two shipments; some are short by a unit. The report covers July to September 2026 with a 95% OTIF target.",[20,71,73],{"id":72},"the-rules","The rules",[75,76,77,90,96,102],"ul",{},[78,79,80,84,85,89],"li",{},[81,82,83],"strong",{},"On time:"," the line's ",[86,87,88],"em",{},"last"," delivery arrives on or before the promised date. A delivery the day before counts; early deliveries are not penalised here, though some customers do penalise them.",[78,91,92,95],{},[81,93,94],{},"In full:"," the total quantity delivered against the line equals the quantity ordered.",[78,97,98,101],{},[81,99,100],{},"OTIF:"," both. A line delivered in full but a day late fails OTIF; so does one delivered on time but one unit short.",[78,103,104,107],{},[81,105,106],{},"Lateness"," is measured in working days, excluding weekends and the August bank holiday, so a delivery due Friday that arrives Monday is one day late, not three.",[109,110,118,119,118,123,118,127,118,135,118,145,118,156,118,162,118,166,118,170,118,173,118,178,118,187,118,192,118,200,118,207,118,212,118,217,118,219,118,222,118,226,118,229,118,234,118,241,118,245,118,247,118,249,118,252,118,255,118,258,118,263,118,270,118,274],"svg",{"viewBox":111,"role":112,"ariaLabelledBy":113,"xmlns":116,"style":117},"4 0 752 272","img",[114,115],"ot-rules-t","ot-rules-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[120,121,122],"title",{"id":114},"Scoring one order line",[124,125,126],"desc",{"id":115},"A decision diagram for one order line. If the total delivered is less than ordered, the line is not in full and fails OTIF. If the last delivery is after the promised date, the line is late and fails OTIF. If both checks pass, the line counts as OTIF.",[128,129],"rect",{"x":130,"y":131,"width":132,"height":133,"fill":134},"4","0","752","272","#ffffff",[128,136],{"x":137,"y":138,"width":139,"height":140,"rx":141,"fill":142,"stroke":143,"style":144},"240.0","16.0","280.0","75.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[146,147,149],"g",{"transform":148},"translate(368.0,29.0) scale(1.000)",[150,151],"path",{"d":152,"fill":153,"stroke":154,"style":155},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[157,158,161],"text",{"x":159,"y":140,"style":160},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","One order line",[37,163],{"x1":159,"y1":164,"x2":159,"y2":165,"stroke":143,"style":144},"91.5","111.5",[37,167],{"x1":168,"y1":165,"x2":169,"y2":165,"stroke":143,"style":144},"134.7","625.3",[37,171],{"x1":168,"y1":165,"x2":168,"y2":172,"stroke":143,"style":144},"157.5",[174,175],"polygon",{"points":176,"fill":177},"134.7,161.5 129.7,152.5 139.7,152.5","#5b5cf0",[128,179],{"x":180,"y":181,"width":182,"height":183,"rx":184,"fill":134,"stroke":185,"style":186},"67.5","121.5","134.3","21.0","10","var(--line,#cdd5e6)","stroke-width:1.2px",[157,188,191],{"x":168,"y":189,"style":190},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","delivered \u003C ordered",[128,193],{"x":194,"y":195,"width":196,"height":197,"rx":141,"fill":198,"stroke":199,"style":144},"20.0","163.5","229.3","93.0","#fdefd8","var(--gold,#b4740a)",[146,201,203],{"transform":202},"translate(122.7,176.5) scale(1.000)",[150,204],{"d":205,"fill":153,"stroke":206,"style":155},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[157,208,211],{"x":168,"y":209,"style":210},"223.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Not in full",[157,213,216],{"x":168,"y":214,"style":215},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","fails OTIF",[37,218],{"x1":159,"y1":165,"x2":159,"y2":172,"stroke":143,"style":144},[174,220],{"points":221,"fill":177},"380.0,161.5 375.0,152.5 385.0,152.5",[128,223],{"x":224,"y":181,"width":225,"height":183,"rx":184,"fill":134,"stroke":185,"style":186},"300.0","160.0",[157,227,228],{"x":159,"y":189,"style":190},"last delivery > promised",[128,230],{"x":231,"y":195,"width":196,"height":197,"rx":141,"fill":232,"stroke":233,"style":144},"265.3","#fce9e9","var(--danger,#dc2626)",[146,235,237],{"transform":236},"translate(368.0,176.5) scale(1.000)",[150,238],{"d":239,"fill":153,"stroke":240,"style":155},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","var(--accent-ink,#be185d)",[157,242,244],{"x":159,"y":209,"style":243},"font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Late",[157,246,216],{"x":159,"y":214,"style":215},[37,248],{"x1":169,"y1":165,"x2":169,"y2":172,"stroke":143,"style":144},[174,250],{"points":251,"fill":177},"625.3,161.5 620.3,152.5 630.3,152.5",[128,253],{"x":254,"y":181,"width":181,"height":183,"rx":184,"fill":134,"stroke":185,"style":186},"564.6",[157,256,257],{"x":169,"y":189,"style":190},"both checks pass",[128,259],{"x":260,"y":195,"width":196,"height":197,"rx":141,"fill":261,"stroke":262,"style":144},"510.7","#d9f4f1","var(--teal,#0f9488)",[146,264,266],{"transform":265},"translate(613.3,176.5) scale(1.000)",[150,267],{"d":268,"fill":153,"stroke":269,"style":155},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[157,271,273],{"x":169,"y":209,"style":272},"font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","OTIF",[157,275,276],{"x":169,"y":214,"style":215},"counts toward target",[20,278,280],{"id":279},"step-1-one-row-per-order-line","Step 1 — One row per order line",[10,282,283],{},"Shipments are many-to-one with order lines, so aggregate them first: the total quantity delivered, the number of shipments, and the date of the last one. Then join to the order lines with a left join, so lines with no shipment at all stay in the report:",[25,285,289],{"className":286,"code":287,"language":288,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import numpy as np\nimport pandas as pd\n\norders = pd.read_excel(\"deliveries_q3.xlsx\", sheet_name=\"Orders\", parse_dates=[\"PromisedDate\"])\nshipments = pd.read_excel(\"deliveries_q3.xlsx\", sheet_name=\"Shipments\", parse_dates=[\"DeliveredDate\"])\n\nper_line = shipments.groupby([\"OrderID\", \"Line\"]).agg(\n    QtyDelivered=(\"QtyDelivered\", \"sum\"),\n    Shipments=(\"DeliveredDate\", \"size\"),\n    LastDelivery=(\"DeliveredDate\", \"max\"),\n).reset_index()\nlines = orders.merge(per_line, on=[\"OrderID\", \"Line\"], how=\"left\", validate=\"one_to_one\")\nlines[\"QtyDelivered\"] = lines[\"QtyDelivered\"].fillna(0).astype(int)\nprint(f\"{len(lines)} order lines; {int((lines['Shipments'] > 1).sum())} split across shipments; \"\n      f\"{int(lines['LastDelivery'].isna().sum())} with no delivery yet\")\n","python",[32,290,291,307,320,327,370,404,409,431,453,472,491,497,545,577,633],{"__ignoreMap":30},[35,292,293,297,301,304],{"class":37,"line":38},[35,294,296],{"class":295},"s-kum","import",[35,298,300],{"class":299},"skGVy"," numpy ",[35,302,303],{"class":295},"as",[35,305,306],{"class":299}," np\n",[35,308,310,312,315,317],{"class":37,"line":309},2,[35,311,296],{"class":295},[35,313,314],{"class":299}," pandas ",[35,316,303],{"class":295},[35,318,319],{"class":299}," pd\n",[35,321,323],{"class":37,"line":322},3,[35,324,326],{"emptyLinePlaceholder":325},true,"\n",[35,328,330,333,336,339,342,345,349,351,354,356,359,361,364,367],{"class":37,"line":329},4,[35,331,332],{"class":299},"orders ",[35,334,335],{"class":295},"=",[35,337,338],{"class":299}," pd.read_excel(",[35,340,341],{"class":45},"\"deliveries_q3.xlsx\"",[35,343,344],{"class":299},", ",[35,346,348],{"class":347},"sa561","sheet_name",[35,350,335],{"class":295},[35,352,353],{"class":45},"\"Orders\"",[35,355,344],{"class":299},[35,357,358],{"class":347},"parse_dates",[35,360,335],{"class":295},[35,362,363],{"class":299},"[",[35,365,366],{"class":45},"\"PromisedDate\"",[35,368,369],{"class":299},"])\n",[35,371,373,376,378,380,382,384,386,388,391,393,395,397,399,402],{"class":37,"line":372},5,[35,374,375],{"class":299},"shipments ",[35,377,335],{"class":295},[35,379,338],{"class":299},[35,381,341],{"class":45},[35,383,344],{"class":299},[35,385,348],{"class":347},[35,387,335],{"class":295},[35,389,390],{"class":45},"\"Shipments\"",[35,392,344],{"class":299},[35,394,358],{"class":347},[35,396,335],{"class":295},[35,398,363],{"class":299},[35,400,401],{"class":45},"\"DeliveredDate\"",[35,403,369],{"class":299},[35,405,407],{"class":37,"line":406},6,[35,408,326],{"emptyLinePlaceholder":325},[35,410,412,415,417,420,423,425,428],{"class":37,"line":411},7,[35,413,414],{"class":299},"per_line ",[35,416,335],{"class":295},[35,418,419],{"class":299}," shipments.groupby([",[35,421,422],{"class":45},"\"OrderID\"",[35,424,344],{"class":299},[35,426,427],{"class":45},"\"Line\"",[35,429,430],{"class":299},"]).agg(\n",[35,432,434,437,439,442,445,447,450],{"class":37,"line":433},8,[35,435,436],{"class":347},"    QtyDelivered",[35,438,335],{"class":295},[35,440,441],{"class":299},"(",[35,443,444],{"class":45},"\"QtyDelivered\"",[35,446,344],{"class":299},[35,448,449],{"class":45},"\"sum\"",[35,451,452],{"class":299},"),\n",[35,454,456,459,461,463,465,467,470],{"class":37,"line":455},9,[35,457,458],{"class":347},"    Shipments",[35,460,335],{"class":295},[35,462,441],{"class":299},[35,464,401],{"class":45},[35,466,344],{"class":299},[35,468,469],{"class":45},"\"size\"",[35,471,452],{"class":299},[35,473,475,478,480,482,484,486,489],{"class":37,"line":474},10,[35,476,477],{"class":347},"    LastDelivery",[35,479,335],{"class":295},[35,481,441],{"class":299},[35,483,401],{"class":45},[35,485,344],{"class":299},[35,487,488],{"class":45},"\"max\"",[35,490,452],{"class":299},[35,492,494],{"class":37,"line":493},11,[35,495,496],{"class":299},").reset_index()\n",[35,498,500,503,505,508,511,513,515,517,519,521,524,527,529,532,534,537,539,542],{"class":37,"line":499},12,[35,501,502],{"class":299},"lines ",[35,504,335],{"class":295},[35,506,507],{"class":299}," orders.merge(per_line, ",[35,509,510],{"class":347},"on",[35,512,335],{"class":295},[35,514,363],{"class":299},[35,516,422],{"class":45},[35,518,344],{"class":299},[35,520,427],{"class":45},[35,522,523],{"class":299},"], ",[35,525,526],{"class":347},"how",[35,528,335],{"class":295},[35,530,531],{"class":45},"\"left\"",[35,533,344],{"class":299},[35,535,536],{"class":347},"validate",[35,538,335],{"class":295},[35,540,541],{"class":45},"\"one_to_one\"",[35,543,544],{"class":299},")\n",[35,546,548,551,553,556,558,561,563,566,569,572,575],{"class":37,"line":547},13,[35,549,550],{"class":299},"lines[",[35,552,444],{"class":45},[35,554,555],{"class":299},"] ",[35,557,335],{"class":295},[35,559,560],{"class":299}," lines[",[35,562,444],{"class":45},[35,564,565],{"class":299},"].fillna(",[35,567,131],{"class":568},"sP0c6",[35,570,571],{"class":299},").astype(",[35,573,574],{"class":568},"int",[35,576,544],{"class":299},[35,578,580,583,585,588,591,595,598,601,604,607,609,611,614,617,619,622,625,628,630],{"class":37,"line":579},14,[35,581,582],{"class":568},"print",[35,584,441],{"class":299},[35,586,587],{"class":295},"f",[35,589,590],{"class":45},"\"",[35,592,594],{"class":593},"sSjpA","{",[35,596,597],{"class":568},"len",[35,599,600],{"class":299},"(lines)",[35,602,603],{"class":593},"}",[35,605,606],{"class":45}," order lines; ",[35,608,594],{"class":593},[35,610,574],{"class":568},[35,612,613],{"class":299},"((lines[",[35,615,616],{"class":45},"'Shipments'",[35,618,555],{"class":299},[35,620,621],{"class":295},">",[35,623,624],{"class":568}," 1",[35,626,627],{"class":299},").sum())",[35,629,603],{"class":593},[35,631,632],{"class":45}," split across shipments; \"\n",[35,634,636,639,641,643,645,648,651,654,656,659],{"class":37,"line":635},15,[35,637,638],{"class":295},"      f",[35,640,590],{"class":45},[35,642,594],{"class":593},[35,644,574],{"class":568},[35,646,647],{"class":299},"(lines[",[35,649,650],{"class":45},"'LastDelivery'",[35,652,653],{"class":299},"].isna().sum())",[35,655,603],{"class":593},[35,657,658],{"class":45}," with no delivery yet\"",[35,660,544],{"class":299},[10,662,663,666,667,671],{},[32,664,665],{},"validate=\"one_to_one\""," makes pandas raise if the aggregation step was skipped or the keys are duplicated, rather than quietly multiplying order lines. ",[14,668,670],{"href":669},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvalidate-a-merge-to-avoid-duplicate-rows\u002F","Validate a pandas Merge to Avoid Duplicate Rows"," explains the options.",[20,673,675],{"id":674},"step-2-working-day-lateness","Step 2 — Working-day lateness",[10,677,678,681,682,684,685,688],{},[32,679,680],{},"numpy.busday_count(a, b)"," counts working days from ",[32,683,14],{}," up to but not including ",[32,686,687],{},"b",", which is exactly \"how many working days after the promise did it arrive\". Pass the holidays that apply in the delivery country:",[25,690,692],{"className":286,"code":691,"language":288,"meta":30,"style":30},"HOLIDAYS = np.array([\"2026-08-31\"], dtype=\"datetime64[D]\")     # August bank holiday\n\ndelivered = lines[\"LastDelivery\"].notna()\npromised = lines.loc[delivered, \"PromisedDate\"].to_numpy().astype(\"datetime64[D]\")\nlast = lines.loc[delivered, \"LastDelivery\"].to_numpy().astype(\"datetime64[D]\")\nlate_by = np.where(last > promised, np.busday_count(promised, last, holidays=HOLIDAYS), 0)\nlines.loc[delivered, \"DaysLate\"] = late_by\nlines[\"DaysLate\"] = lines[\"DaysLate\"].astype(\"Int64\")\n\nlines[\"OnTime\"] = delivered & (lines[\"LastDelivery\"] \u003C= lines[\"PromisedDate\"])\nlines[\"InFull\"] = lines[\"QtyDelivered\"] >= lines[\"QtyOrdered\"]\nlines[\"OTIF\"] = lines[\"OnTime\"] & lines[\"InFull\"]\nprint(lines[\"DaysLate\"].value_counts().sort_index().to_string())\n",[32,693,694,725,729,744,763,780,809,824,846,850,883,911,936],{"__ignoreMap":30},[35,695,696,699,702,705,708,710,713,715,718,721],{"class":37,"line":38},[35,697,698],{"class":568},"HOLIDAYS",[35,700,701],{"class":295}," =",[35,703,704],{"class":299}," np.array([",[35,706,707],{"class":45},"\"2026-08-31\"",[35,709,523],{"class":299},[35,711,712],{"class":347},"dtype",[35,714,335],{"class":295},[35,716,717],{"class":45},"\"datetime64[D]\"",[35,719,720],{"class":299},")     ",[35,722,724],{"class":723},"s-wDw","# August bank holiday\n",[35,726,727],{"class":37,"line":309},[35,728,326],{"emptyLinePlaceholder":325},[35,730,731,734,736,738,741],{"class":37,"line":322},[35,732,733],{"class":299},"delivered ",[35,735,335],{"class":295},[35,737,560],{"class":299},[35,739,740],{"class":45},"\"LastDelivery\"",[35,742,743],{"class":299},"].notna()\n",[35,745,746,749,751,754,756,759,761],{"class":37,"line":329},[35,747,748],{"class":299},"promised ",[35,750,335],{"class":295},[35,752,753],{"class":299}," lines.loc[delivered, ",[35,755,366],{"class":45},[35,757,758],{"class":299},"].to_numpy().astype(",[35,760,717],{"class":45},[35,762,544],{"class":299},[35,764,765,768,770,772,774,776,778],{"class":37,"line":372},[35,766,767],{"class":299},"last ",[35,769,335],{"class":295},[35,771,753],{"class":299},[35,773,740],{"class":45},[35,775,758],{"class":299},[35,777,717],{"class":45},[35,779,544],{"class":299},[35,781,782,785,787,790,792,795,798,800,802,805,807],{"class":37,"line":406},[35,783,784],{"class":299},"late_by ",[35,786,335],{"class":295},[35,788,789],{"class":299}," np.where(last ",[35,791,621],{"class":295},[35,793,794],{"class":299}," promised, np.busday_count(promised, last, ",[35,796,797],{"class":347},"holidays",[35,799,335],{"class":295},[35,801,698],{"class":568},[35,803,804],{"class":299},"), ",[35,806,131],{"class":568},[35,808,544],{"class":299},[35,810,811,814,817,819,821],{"class":37,"line":411},[35,812,813],{"class":299},"lines.loc[delivered, ",[35,815,816],{"class":45},"\"DaysLate\"",[35,818,555],{"class":299},[35,820,335],{"class":295},[35,822,823],{"class":299}," late_by\n",[35,825,826,828,830,832,834,836,838,841,844],{"class":37,"line":433},[35,827,550],{"class":299},[35,829,816],{"class":45},[35,831,555],{"class":299},[35,833,335],{"class":295},[35,835,560],{"class":299},[35,837,816],{"class":45},[35,839,840],{"class":299},"].astype(",[35,842,843],{"class":45},"\"Int64\"",[35,845,544],{"class":299},[35,847,848],{"class":37,"line":455},[35,849,326],{"emptyLinePlaceholder":325},[35,851,852,854,857,859,861,864,867,870,872,874,877,879,881],{"class":37,"line":474},[35,853,550],{"class":299},[35,855,856],{"class":45},"\"OnTime\"",[35,858,555],{"class":299},[35,860,335],{"class":295},[35,862,863],{"class":299}," delivered ",[35,865,866],{"class":295},"&",[35,868,869],{"class":299}," (lines[",[35,871,740],{"class":45},[35,873,555],{"class":299},[35,875,876],{"class":295},"\u003C=",[35,878,560],{"class":299},[35,880,366],{"class":45},[35,882,369],{"class":299},[35,884,885,887,890,892,894,896,898,900,903,905,908],{"class":37,"line":493},[35,886,550],{"class":299},[35,888,889],{"class":45},"\"InFull\"",[35,891,555],{"class":299},[35,893,335],{"class":295},[35,895,560],{"class":299},[35,897,444],{"class":45},[35,899,555],{"class":299},[35,901,902],{"class":295},">=",[35,904,560],{"class":299},[35,906,907],{"class":45},"\"QtyOrdered\"",[35,909,910],{"class":299},"]\n",[35,912,913,915,918,920,922,924,926,928,930,932,934],{"class":37,"line":499},[35,914,550],{"class":299},[35,916,917],{"class":45},"\"OTIF\"",[35,919,555],{"class":299},[35,921,335],{"class":295},[35,923,560],{"class":299},[35,925,856],{"class":45},[35,927,555],{"class":299},[35,929,866],{"class":295},[35,931,560],{"class":299},[35,933,889],{"class":45},[35,935,910],{"class":299},[35,937,938,940,942,944],{"class":37,"line":547},[35,939,582],{"class":568},[35,941,647],{"class":299},[35,943,816],{"class":45},[35,945,946],{"class":299},"].value_counts().sort_index().to_string())\n",[10,948,949],{},"Lines without any delivery are neither on time nor in full; if their promised date is still in the future they should be excluded instead, which Step 3 does.",[20,951,953],{"id":952},"step-3-the-headline-rates","Step 3 — The headline rates",[10,955,956],{},"Only score lines that were due in the period. A line promised for next week is not late yet:",[25,958,960],{"className":286,"code":959,"language":288,"meta":30,"style":30},"PERIOD_END = pd.Timestamp(\"2026-09-30\")\ndue = lines[lines[\"PromisedDate\"] \u003C= PERIOD_END]\n\nheadline = pd.Series({\n    \"Lines due\": len(due),\n    \"On time %\": due[\"OnTime\"].mean() * 100,\n    \"In full %\": due[\"InFull\"].mean() * 100,\n    \"OTIF %\": due[\"OTIF\"].mean() * 100,\n    \"Avg days late (late lines)\": due.loc[~due[\"OnTime\"] & delivered, \"DaysLate\"].mean(),\n})\nprint(headline.round(1).to_string())\n",[32,961,962,977,998,1002,1012,1025,1047,1064,1081,1109,1114],{"__ignoreMap":30},[35,963,964,967,969,972,975],{"class":37,"line":38},[35,965,966],{"class":568},"PERIOD_END",[35,968,701],{"class":295},[35,970,971],{"class":299}," pd.Timestamp(",[35,973,974],{"class":45},"\"2026-09-30\"",[35,976,544],{"class":299},[35,978,979,982,984,987,989,991,993,996],{"class":37,"line":309},[35,980,981],{"class":299},"due ",[35,983,335],{"class":295},[35,985,986],{"class":299}," lines[lines[",[35,988,366],{"class":45},[35,990,555],{"class":299},[35,992,876],{"class":295},[35,994,995],{"class":568}," PERIOD_END",[35,997,910],{"class":299},[35,999,1000],{"class":37,"line":322},[35,1001,326],{"emptyLinePlaceholder":325},[35,1003,1004,1007,1009],{"class":37,"line":329},[35,1005,1006],{"class":299},"headline ",[35,1008,335],{"class":295},[35,1010,1011],{"class":299}," pd.Series({\n",[35,1013,1014,1017,1020,1022],{"class":37,"line":372},[35,1015,1016],{"class":45},"    \"Lines due\"",[35,1018,1019],{"class":299},": ",[35,1021,597],{"class":568},[35,1023,1024],{"class":299},"(due),\n",[35,1026,1027,1030,1033,1035,1038,1041,1044],{"class":37,"line":406},[35,1028,1029],{"class":45},"    \"On time %\"",[35,1031,1032],{"class":299},": due[",[35,1034,856],{"class":45},[35,1036,1037],{"class":299},"].mean() ",[35,1039,1040],{"class":295},"*",[35,1042,1043],{"class":568}," 100",[35,1045,1046],{"class":299},",\n",[35,1048,1049,1052,1054,1056,1058,1060,1062],{"class":37,"line":411},[35,1050,1051],{"class":45},"    \"In full %\"",[35,1053,1032],{"class":299},[35,1055,889],{"class":45},[35,1057,1037],{"class":299},[35,1059,1040],{"class":295},[35,1061,1043],{"class":568},[35,1063,1046],{"class":299},[35,1065,1066,1069,1071,1073,1075,1077,1079],{"class":37,"line":433},[35,1067,1068],{"class":45},"    \"OTIF %\"",[35,1070,1032],{"class":299},[35,1072,917],{"class":45},[35,1074,1037],{"class":299},[35,1076,1040],{"class":295},[35,1078,1043],{"class":568},[35,1080,1046],{"class":299},[35,1082,1083,1086,1089,1092,1095,1097,1099,1101,1104,1106],{"class":37,"line":455},[35,1084,1085],{"class":45},"    \"Avg days late (late lines)\"",[35,1087,1088],{"class":299},": due.loc[",[35,1090,1091],{"class":295},"~",[35,1093,1094],{"class":299},"due[",[35,1096,856],{"class":45},[35,1098,555],{"class":299},[35,1100,866],{"class":295},[35,1102,1103],{"class":299}," delivered, ",[35,1105,816],{"class":45},[35,1107,1108],{"class":299},"].mean(),\n",[35,1110,1111],{"class":37,"line":474},[35,1112,1113],{"class":299},"})\n",[35,1115,1116,1118,1121,1124],{"class":37,"line":493},[35,1117,582],{"class":568},[35,1119,1120],{"class":299},"(headline.round(",[35,1122,1123],{"class":568},"1",[35,1125,1126],{"class":299},").to_string())\n",[10,1128,1129],{},"Reporting on-time and in-full separately next to OTIF shows which problem to fix: if on-time is 97% and in-full 90%, the warehouse is short-picking; if the reverse, look at dispatch or the carriers.",[20,1131,1133],{"id":1132},"step-4-by-carrier-customer-and-week","Step 4 — By carrier, customer and week",[10,1135,1136],{},"The same three rates grouped different ways point at the cause. A helper keeps the definitions in one place:",[25,1138,1140],{"className":286,"code":1139,"language":288,"meta":30,"style":30},"def rates(df):\n    return pd.Series({\n        \"Lines\": len(df),\n        \"On time %\": df[\"OnTime\"].mean() * 100,\n        \"In full %\": df[\"InFull\"].mean() * 100,\n        \"OTIF %\": df[\"OTIF\"].mean() * 100,\n    })\n\nby_carrier = due.groupby(\"Carrier\")[due.columns].apply(rates).sort_values(\"OTIF %\")\nby_customer = due.groupby(\"Customer\")[due.columns].apply(rates).sort_values(\"OTIF %\")\nweekly = due.groupby(due[\"PromisedDate\"].dt.to_period(\"W-SUN\"))[due.columns].apply(rates)\nweekly.index = weekly.index.start_time.strftime(\"w\u002Fc %d %b\")\nprint(by_carrier.round(1).to_string())\nprint(by_customer.round(1).head(3).to_string())\n",[32,1141,1142,1154,1161,1173,1191,1208,1225,1230,1234,1255,1273,1294,1317,1328],{"__ignoreMap":30},[35,1143,1144,1147,1151],{"class":37,"line":38},[35,1145,1146],{"class":295},"def",[35,1148,1150],{"class":1149},"s_Opv"," rates",[35,1152,1153],{"class":299},"(df):\n",[35,1155,1156,1159],{"class":37,"line":309},[35,1157,1158],{"class":295},"    return",[35,1160,1011],{"class":299},[35,1162,1163,1166,1168,1170],{"class":37,"line":322},[35,1164,1165],{"class":45},"        \"Lines\"",[35,1167,1019],{"class":299},[35,1169,597],{"class":568},[35,1171,1172],{"class":299},"(df),\n",[35,1174,1175,1178,1181,1183,1185,1187,1189],{"class":37,"line":329},[35,1176,1177],{"class":45},"        \"On time %\"",[35,1179,1180],{"class":299},": df[",[35,1182,856],{"class":45},[35,1184,1037],{"class":299},[35,1186,1040],{"class":295},[35,1188,1043],{"class":568},[35,1190,1046],{"class":299},[35,1192,1193,1196,1198,1200,1202,1204,1206],{"class":37,"line":372},[35,1194,1195],{"class":45},"        \"In full %\"",[35,1197,1180],{"class":299},[35,1199,889],{"class":45},[35,1201,1037],{"class":299},[35,1203,1040],{"class":295},[35,1205,1043],{"class":568},[35,1207,1046],{"class":299},[35,1209,1210,1213,1215,1217,1219,1221,1223],{"class":37,"line":406},[35,1211,1212],{"class":45},"        \"OTIF %\"",[35,1214,1180],{"class":299},[35,1216,917],{"class":45},[35,1218,1037],{"class":299},[35,1220,1040],{"class":295},[35,1222,1043],{"class":568},[35,1224,1046],{"class":299},[35,1226,1227],{"class":37,"line":411},[35,1228,1229],{"class":299},"    })\n",[35,1231,1232],{"class":37,"line":433},[35,1233,326],{"emptyLinePlaceholder":325},[35,1235,1236,1239,1241,1244,1247,1250,1253],{"class":37,"line":455},[35,1237,1238],{"class":299},"by_carrier ",[35,1240,335],{"class":295},[35,1242,1243],{"class":299}," due.groupby(",[35,1245,1246],{"class":45},"\"Carrier\"",[35,1248,1249],{"class":299},")[due.columns].apply(rates).sort_values(",[35,1251,1252],{"class":45},"\"OTIF %\"",[35,1254,544],{"class":299},[35,1256,1257,1260,1262,1264,1267,1269,1271],{"class":37,"line":474},[35,1258,1259],{"class":299},"by_customer ",[35,1261,335],{"class":295},[35,1263,1243],{"class":299},[35,1265,1266],{"class":45},"\"Customer\"",[35,1268,1249],{"class":299},[35,1270,1252],{"class":45},[35,1272,544],{"class":299},[35,1274,1275,1278,1280,1283,1285,1288,1291],{"class":37,"line":493},[35,1276,1277],{"class":299},"weekly ",[35,1279,335],{"class":295},[35,1281,1282],{"class":299}," due.groupby(due[",[35,1284,366],{"class":45},[35,1286,1287],{"class":299},"].dt.to_period(",[35,1289,1290],{"class":45},"\"W-SUN\"",[35,1292,1293],{"class":299},"))[due.columns].apply(rates)\n",[35,1295,1296,1299,1301,1304,1307,1310,1313,1315],{"class":37,"line":499},[35,1297,1298],{"class":299},"weekly.index ",[35,1300,335],{"class":295},[35,1302,1303],{"class":299}," weekly.index.start_time.strftime(",[35,1305,1306],{"class":45},"\"w\u002Fc ",[35,1308,1309],{"class":593},"%d",[35,1311,1312],{"class":593}," %b",[35,1314,590],{"class":45},[35,1316,544],{"class":299},[35,1318,1319,1321,1324,1326],{"class":37,"line":547},[35,1320,582],{"class":568},[35,1322,1323],{"class":299},"(by_carrier.round(",[35,1325,1123],{"class":568},[35,1327,1126],{"class":299},[35,1329,1330,1332,1335,1337,1340,1343],{"class":37,"line":579},[35,1331,582],{"class":568},[35,1333,1334],{"class":299},"(by_customer.round(",[35,1336,1123],{"class":568},[35,1338,1339],{"class":299},").head(",[35,1341,1342],{"class":568},"3",[35,1344,1126],{"class":299},[109,1346,118,1352,118,1355,118,1358,118,1362,118,1369,118,1375,118,1380,118,1385,118,1388,118,1392,118,1395,118,1400,118,1403,118,1407,118,1410],{"viewBox":1347,"role":112,"ariaLabelledBy":1348,"xmlns":116,"style":1351},"4 0 742 140",[1349,1350],"ot-carriers-t","ot-carriers-d","width:100%;max-width:742px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[120,1353,1354],{"id":1349},"OTIF by carrier, July to September 2026",[124,1356,1357],{"id":1350},"A horizontal bar chart of OTIF percentage by carrier for the quarter. FastFreight reaches 90.1 percent, ParcelOne 85.8 and RoadLink 78.9; all three are below the 95 percent target.",[128,1359],{"x":130,"y":131,"width":1360,"height":1361,"fill":134},"742","140",[128,1363],{"x":194,"y":138,"width":1364,"height":1365,"rx":1366,"fill":1367,"stroke":185,"style":1368},"101.2","28.0","7","#e7ebef","stroke-width:1px",[157,1370,1374],{"x":1371,"y":1372,"style":1373},"70.6","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","FastFreight",[128,1376],{"x":1377,"y":138,"width":1378,"height":1365,"rx":1366,"fill":261,"stroke":262,"style":1379},"131.2","551.6","stroke-width:1.5px",[157,1381,1384],{"x":1382,"y":1372,"style":1383},"690.7","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","90.1%",[128,1386],{"x":194,"y":1387,"width":1364,"height":1365,"rx":1366,"fill":1367,"stroke":185,"style":1368},"56.0",[157,1389,1391],{"x":1371,"y":1390,"style":1373},"74.5","ParcelOne",[128,1393],{"x":1377,"y":1387,"width":1394,"height":1365,"rx":1366,"fill":198,"stroke":199,"style":1379},"525.2",[157,1396,1399],{"x":1397,"y":1390,"style":1398},"664.4","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","85.8%",[128,1401],{"x":194,"y":1402,"width":1364,"height":1365,"rx":1366,"fill":1367,"stroke":185,"style":1368},"96.0",[157,1404,1406],{"x":1371,"y":1405,"style":1373},"114.5","RoadLink",[128,1408],{"x":1377,"y":1402,"width":1409,"height":1365,"rx":1366,"fill":232,"stroke":233,"style":1379},"483.0",[157,1411,1414],{"x":1412,"y":1405,"style":1413},"622.2","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","78.9%",[10,1416,1417],{},"Every carrier misses the 95% target, but not equally, and not for the same reason. RoadLink's on-time rate of 84.9% is the main problem: it is late on roughly one line in seven. ParcelOne is late less often but by more when it is: its late lines average about 2.5 working days against RoadLink's 1.9. FastFreight's few late lines average 3.2 days, mostly lines where a split second shipment arrived days after the first. In-full rates are similar across carriers, as expected, since short deliveries start in the warehouse rather than on the road. By customer, Northwind Cafe fares worst at 77.6% OTIF; nearly half of its lines go with RoadLink, which explains part of it, and the next question is whether its delivery windows are tighter than other customers', which the promised-date data can answer.",[10,1419,1420],{},"The weekly view is where a new problem shows first. Individual weeks swing because each has only a few dozen lines, so read the trend rather than any single point: here the last two weeks of September, at 76.9% and 77.8%, sit below every week since mid-July and are worth a look before the next quarter starts.",[109,1422,118,1428,118,1431,118,1434,118,1438,118,1443,118,1448,118,1451,118,1455,118,1460,118,1466,118,1470,118,1474,118,1477,118,1481,118,1484,118,1488,118,1491,118,1494,118,1497,118,1502,118,1506,118,1510,118,1514,118,1518,118,1522,118,1526,118,1530,118,1534,118,1538,118,1542,118,1546,118,1550,118,1554,118,1559,118,1562,118,1565,118,1568,118,1571,118,1574,118,1576,118,1579,118,1582,118,1584,118,1587,118,1590,118,1593,118,1596,118,1599,118,1601,118,1603,118,1605,118,1607,118,1609,118,1611,118,1613,118,1615,118,1617,118,1619,118,1621],{"viewBox":1423,"role":112,"ariaLabelledBy":1424,"xmlns":116,"style":1427},"4 0 748 280",[1425,1426],"ot-weekly-t","ot-weekly-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[120,1429,1430],{"id":1425},"OTIF by week against the 95% target",[124,1432,1433],{"id":1426},"A line chart of weekly OTIF percentage for thirteen weeks from 29 June to 21 September 2026, with the 95 percent target as a flat line. OTIF ranges from 73.5 percent in the week of 13 July to 97.7 percent in the week of 17 August, the only week above target, and falls to about 77 percent in the last two weeks.",[128,1435],{"x":130,"y":131,"width":1436,"height":1437,"fill":134},"748","280",[128,1439],{"x":1440,"y":1441,"width":1442,"height":1442,"rx":1342,"fill":142,"stroke":143,"style":1379},"64.0","18.0","14.0",[157,1444,273],{"x":1445,"y":1446,"style":1447},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start",[128,1449],{"x":1450,"y":1441,"width":1442,"height":1442,"rx":1342,"fill":232,"stroke":233,"style":1379},"141.1",[157,1452,1454],{"x":1453,"y":1446,"style":1447},"161.1","Target",[37,1456],{"x1":1457,"y1":1458,"x2":1459,"y2":1458,"stroke":185,"style":1368},"62.6","242.0","736.0",[157,1461,1465],{"x":1462,"y":1463,"style":1464},"35.3","246.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","0%",[37,1467],{"x1":1457,"y1":1468,"x2":1459,"y2":1468,"stroke":185,"style":1469},"194.5","stroke-width:1px;stroke-dasharray:5 4",[157,1471,1473],{"x":1462,"y":1472,"style":1464},"198.5","25%",[37,1475],{"x1":1457,"y1":1476,"x2":1459,"y2":1476,"stroke":185,"style":1469},"147.0",[157,1478,1480],{"x":1462,"y":1479,"style":1464},"151.0","50%",[37,1482],{"x1":1457,"y1":1483,"x2":1459,"y2":1483,"stroke":185,"style":1469},"99.5",[157,1485,1487],{"x":1462,"y":1486,"style":1464},"103.5","75%",[37,1489],{"x1":1457,"y1":1490,"x2":1459,"y2":1490,"stroke":185,"style":1469},"52.0",[157,1492,1493],{"x":1462,"y":1387,"style":1464},"100%",[37,1495],{"x1":1457,"y1":1458,"x2":1459,"y2":1458,"stroke":1496,"style":1379},"var(--muted,#5b6780)",[157,1498,1501],{"x":1499,"y":1500,"style":215},"88.5","260.0","29 Jun",[157,1503,1505],{"x":1504,"y":1500,"style":215},"140.3","6 Jul",[157,1507,1509],{"x":1508,"y":1500,"style":215},"192.1","13 Jul",[157,1511,1513],{"x":1512,"y":1500,"style":215},"243.9","20 Jul",[157,1515,1517],{"x":1516,"y":1500,"style":215},"295.7","27 Jul",[157,1519,1521],{"x":1520,"y":1500,"style":215},"347.5","3 Aug",[157,1523,1525],{"x":1524,"y":1500,"style":215},"399.3","10 Aug",[157,1527,1529],{"x":1528,"y":1500,"style":215},"451.1","17 Aug",[157,1531,1533],{"x":1532,"y":1500,"style":215},"502.9","24 Aug",[157,1535,1537],{"x":1536,"y":1500,"style":215},"554.7","31 Aug",[157,1539,1541],{"x":1540,"y":1500,"style":215},"606.5","7 Sep",[157,1543,1545],{"x":1544,"y":1500,"style":215},"658.3","14 Sep",[157,1547,1549],{"x":1548,"y":1500,"style":215},"710.1","21 Sep",[150,1551],{"d":1552,"fill":153,"stroke":143,"style":1553},"M88.5,79.2 L140.3,87.0 L192.1,102.3 L243.9,74.4 L295.7,85.1 L347.5,83.7 L399.3,74.4 L451.1,56.4 L502.9,73.8 L554.7,73.8 L606.5,77.1 L658.3,95.9 L710.1,94.2","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[1555,1556],"circle",{"cx":1499,"cy":1557,"r":1558,"fill":134,"stroke":143,"style":144},"79.2","4.0",[1555,1560],{"cx":1504,"cy":1561,"r":1558,"fill":134,"stroke":143,"style":144},"87.0",[1555,1563],{"cx":1508,"cy":1564,"r":1558,"fill":134,"stroke":143,"style":144},"102.3",[1555,1566],{"cx":1512,"cy":1567,"r":1558,"fill":134,"stroke":143,"style":144},"74.4",[1555,1569],{"cx":1516,"cy":1570,"r":1558,"fill":134,"stroke":143,"style":144},"85.1",[1555,1572],{"cx":1520,"cy":1573,"r":1558,"fill":134,"stroke":143,"style":144},"83.7",[1555,1575],{"cx":1524,"cy":1567,"r":1558,"fill":134,"stroke":143,"style":144},[1555,1577],{"cx":1528,"cy":1578,"r":1558,"fill":134,"stroke":143,"style":144},"56.4",[1555,1580],{"cx":1532,"cy":1581,"r":1558,"fill":134,"stroke":143,"style":144},"73.8",[1555,1583],{"cx":1536,"cy":1581,"r":1558,"fill":134,"stroke":143,"style":144},[1555,1585],{"cx":1540,"cy":1586,"r":1558,"fill":134,"stroke":143,"style":144},"77.1",[1555,1588],{"cx":1544,"cy":1589,"r":1558,"fill":134,"stroke":143,"style":144},"95.9",[1555,1591],{"cx":1548,"cy":1592,"r":1558,"fill":134,"stroke":143,"style":144},"94.2",[150,1594],{"d":1595,"fill":153,"stroke":233,"style":1553},"M88.5,61.5 L140.3,61.5 L192.1,61.5 L243.9,61.5 L295.7,61.5 L347.5,61.5 L399.3,61.5 L451.1,61.5 L502.9,61.5 L554.7,61.5 L606.5,61.5 L658.3,61.5 L710.1,61.5",[1555,1597],{"cx":1499,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},"61.5",[1555,1600],{"cx":1504,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1602],{"cx":1508,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1604],{"cx":1512,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1606],{"cx":1516,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1608],{"cx":1520,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1610],{"cx":1524,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1612],{"cx":1528,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1614],{"cx":1532,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1616],{"cx":1536,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1618],{"cx":1540,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1620],{"cx":1544,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[1555,1622],{"cx":1548,"cy":1598,"r":1558,"fill":134,"stroke":233,"style":144},[20,1624,1626],{"id":1625},"step-5-write-the-report","Step 5 — Write the report",[10,1628,1629],{},"The workbook needs the summary for the meeting and the list of failures for the people who follow up with carriers and customers. Rates are written as fractions with a percent format, and a conditional format marks every rate below the target:",[25,1631,1633],{"className":286,"code":1632,"language":288,"meta":30,"style":30},"TARGET = 0.95\nfailed = due[~due[\"OTIF\"]].copy()\nfailed[\"Reason\"] = np.select(\n    [~failed[\"InFull\"] & ~failed[\"OnTime\"], ~failed[\"InFull\"], ~failed[\"OnTime\"]],\n    [\"Late and short\", \"Short\", \"Late\"], default=\"\")\nfailed = failed.sort_values([\"Carrier\", \"PromisedDate\"])\n\nwith pd.ExcelWriter(\"otif_q3_2026.xlsx\", engine=\"xlsxwriter\") as xw:\n    book = xw.book\n    pct = book.add_format({\"num_format\": \"0.0%\"})\n    below = book.add_format({\"bg_color\": \"#FDE2E2\", \"font_color\": \"#9B1C1C\"})\n    for name, table in [(\"By carrier\", by_carrier), (\"By customer\", by_customer), (\"By week\", weekly)]:\n        out = table.copy()\n        for c in [\"On time %\", \"In full %\", \"OTIF %\"]:\n            out[c] = out[c] \u002F 100\n        out.index = out.index.astype(str)\n        out.to_excel(xw, sheet_name=name)\n        ws = xw.sheets[name]\n        ws.set_column(0, 0, 24)\n        ws.set_column(1, 1, 8)\n        ws.set_column(2, 4, 11, pct)\n        ws.conditional_format(1, 4, len(out), 4, {\"type\": \"cell\", \"criteria\": \"\u003C\", \"value\": TARGET, \"format\": below})\n    failed[[\"OrderID\", \"Line\", \"Customer\", \"Carrier\", \"SKU\", \"PromisedDate\", \"LastDelivery\", \"DaysLate\",\n            \"QtyOrdered\", \"QtyDelivered\", \"Shipments\", \"Reason\"]].to_excel(xw, sheet_name=\"Failed lines\", index=False)\n    fl = xw.sheets[\"Failed lines\"]\n    fl.set_column(0, 4, 14)\n    fl.set_column(5, 6, 12, book.add_format({\"num_format\": \"yyyy-mm-dd\"}))\n    fl.autofilter(0, 0, len(failed), 11)\n    fl.freeze_panes(1, 0)\n\n    ws = xw.sheets[\"By week\"]\n    chart = book.add_chart({\"type\": \"line\"})\n    n = len(weekly)\n    chart.add_series({\"name\": \"OTIF\", \"categories\": [\"By week\", 1, 0, n, 0], \"values\": [\"By week\", 1, 4, n, 4],\n                      \"marker\": {\"type\": \"circle\", \"size\": 4}})\n    chart.add_series({\"name\": \"Target\", \"categories\": [\"By week\", 1, 0, n, 0],\n                      \"values\": \"={\" + \",\".join([str(TARGET)] * n) + \"}\", \"line\": {\"dash_type\": \"dash\"}})\n    chart.set_y_axis({\"num_format\": \"0%\", \"min\": 0.7, \"max\": 1})\n    chart.set_title({\"name\": \"OTIF by week against target\"})\n    ws.insert_chart(\"G2\", chart, {\"x_scale\": 1.5})\nprint(failed[\"Reason\"].value_counts().to_string())\n",[32,1634,1635,1645,1664,1679,1720,1749,1766,1770,1799,1809,1829,1858,1890,1900,1928,1944,1960,1973,1984,2003,2021,2041,2100,2139,2179,2194,2213,2243,2266,2280,2285,2299,2319,2333,2393,2420,2454,2510,2543,2558,2580],{"__ignoreMap":30},[35,1636,1637,1640,1642],{"class":37,"line":38},[35,1638,1639],{"class":568},"TARGET",[35,1641,701],{"class":295},[35,1643,1644],{"class":568}," 0.95\n",[35,1646,1647,1650,1652,1655,1657,1659,1661],{"class":37,"line":309},[35,1648,1649],{"class":299},"failed ",[35,1651,335],{"class":295},[35,1653,1654],{"class":299}," due[",[35,1656,1091],{"class":295},[35,1658,1094],{"class":299},[35,1660,917],{"class":45},[35,1662,1663],{"class":299},"]].copy()\n",[35,1665,1666,1669,1672,1674,1676],{"class":37,"line":322},[35,1667,1668],{"class":299},"failed[",[35,1670,1671],{"class":45},"\"Reason\"",[35,1673,555],{"class":299},[35,1675,335],{"class":295},[35,1677,1678],{"class":299}," np.select(\n",[35,1680,1681,1684,1686,1688,1690,1692,1694,1697,1699,1701,1703,1705,1707,1709,1711,1713,1715,1717],{"class":37,"line":329},[35,1682,1683],{"class":299},"    [",[35,1685,1091],{"class":295},[35,1687,1668],{"class":299},[35,1689,889],{"class":45},[35,1691,555],{"class":299},[35,1693,866],{"class":295},[35,1695,1696],{"class":295}," ~",[35,1698,1668],{"class":299},[35,1700,856],{"class":45},[35,1702,523],{"class":299},[35,1704,1091],{"class":295},[35,1706,1668],{"class":299},[35,1708,889],{"class":45},[35,1710,523],{"class":299},[35,1712,1091],{"class":295},[35,1714,1668],{"class":299},[35,1716,856],{"class":45},[35,1718,1719],{"class":299},"]],\n",[35,1721,1722,1724,1727,1729,1732,1734,1737,1739,1742,1744,1747],{"class":37,"line":372},[35,1723,1683],{"class":299},[35,1725,1726],{"class":45},"\"Late and short\"",[35,1728,344],{"class":299},[35,1730,1731],{"class":45},"\"Short\"",[35,1733,344],{"class":299},[35,1735,1736],{"class":45},"\"Late\"",[35,1738,523],{"class":299},[35,1740,1741],{"class":347},"default",[35,1743,335],{"class":295},[35,1745,1746],{"class":45},"\"\"",[35,1748,544],{"class":299},[35,1750,1751,1753,1755,1758,1760,1762,1764],{"class":37,"line":406},[35,1752,1649],{"class":299},[35,1754,335],{"class":295},[35,1756,1757],{"class":299}," failed.sort_values([",[35,1759,1246],{"class":45},[35,1761,344],{"class":299},[35,1763,366],{"class":45},[35,1765,369],{"class":299},[35,1767,1768],{"class":37,"line":411},[35,1769,326],{"emptyLinePlaceholder":325},[35,1771,1772,1775,1778,1781,1783,1786,1788,1791,1794,1796],{"class":37,"line":433},[35,1773,1774],{"class":295},"with",[35,1776,1777],{"class":299}," pd.ExcelWriter(",[35,1779,1780],{"class":45},"\"otif_q3_2026.xlsx\"",[35,1782,344],{"class":299},[35,1784,1785],{"class":347},"engine",[35,1787,335],{"class":295},[35,1789,1790],{"class":45},"\"xlsxwriter\"",[35,1792,1793],{"class":299},") ",[35,1795,303],{"class":295},[35,1797,1798],{"class":299}," xw:\n",[35,1800,1801,1804,1806],{"class":37,"line":455},[35,1802,1803],{"class":299},"    book ",[35,1805,335],{"class":295},[35,1807,1808],{"class":299}," xw.book\n",[35,1810,1811,1814,1816,1819,1822,1824,1827],{"class":37,"line":474},[35,1812,1813],{"class":299},"    pct ",[35,1815,335],{"class":295},[35,1817,1818],{"class":299}," book.add_format({",[35,1820,1821],{"class":45},"\"num_format\"",[35,1823,1019],{"class":299},[35,1825,1826],{"class":45},"\"0.0%\"",[35,1828,1113],{"class":299},[35,1830,1831,1834,1836,1838,1841,1843,1846,1848,1851,1853,1856],{"class":37,"line":493},[35,1832,1833],{"class":299},"    below ",[35,1835,335],{"class":295},[35,1837,1818],{"class":299},[35,1839,1840],{"class":45},"\"bg_color\"",[35,1842,1019],{"class":299},[35,1844,1845],{"class":45},"\"#FDE2E2\"",[35,1847,344],{"class":299},[35,1849,1850],{"class":45},"\"font_color\"",[35,1852,1019],{"class":299},[35,1854,1855],{"class":45},"\"#9B1C1C\"",[35,1857,1113],{"class":299},[35,1859,1860,1863,1866,1869,1872,1875,1878,1881,1884,1887],{"class":37,"line":499},[35,1861,1862],{"class":295},"    for",[35,1864,1865],{"class":299}," name, table ",[35,1867,1868],{"class":295},"in",[35,1870,1871],{"class":299}," [(",[35,1873,1874],{"class":45},"\"By carrier\"",[35,1876,1877],{"class":299},", by_carrier), (",[35,1879,1880],{"class":45},"\"By customer\"",[35,1882,1883],{"class":299},", by_customer), (",[35,1885,1886],{"class":45},"\"By week\"",[35,1888,1889],{"class":299},", weekly)]:\n",[35,1891,1892,1895,1897],{"class":37,"line":547},[35,1893,1894],{"class":299},"        out ",[35,1896,335],{"class":295},[35,1898,1899],{"class":299}," table.copy()\n",[35,1901,1902,1905,1908,1910,1913,1916,1918,1921,1923,1925],{"class":37,"line":579},[35,1903,1904],{"class":295},"        for",[35,1906,1907],{"class":299}," c ",[35,1909,1868],{"class":295},[35,1911,1912],{"class":299}," [",[35,1914,1915],{"class":45},"\"On time %\"",[35,1917,344],{"class":299},[35,1919,1920],{"class":45},"\"In full %\"",[35,1922,344],{"class":299},[35,1924,1252],{"class":45},[35,1926,1927],{"class":299},"]:\n",[35,1929,1930,1933,1935,1938,1941],{"class":37,"line":635},[35,1931,1932],{"class":299},"            out[c] ",[35,1934,335],{"class":295},[35,1936,1937],{"class":299}," out[c] ",[35,1939,1940],{"class":295},"\u002F",[35,1942,1943],{"class":568}," 100\n",[35,1945,1947,1950,1952,1955,1958],{"class":37,"line":1946},16,[35,1948,1949],{"class":299},"        out.index ",[35,1951,335],{"class":295},[35,1953,1954],{"class":299}," out.index.astype(",[35,1956,1957],{"class":568},"str",[35,1959,544],{"class":299},[35,1961,1963,1966,1968,1970],{"class":37,"line":1962},17,[35,1964,1965],{"class":299},"        out.to_excel(xw, ",[35,1967,348],{"class":347},[35,1969,335],{"class":295},[35,1971,1972],{"class":299},"name)\n",[35,1974,1976,1979,1981],{"class":37,"line":1975},18,[35,1977,1978],{"class":299},"        ws ",[35,1980,335],{"class":295},[35,1982,1983],{"class":299}," xw.sheets[name]\n",[35,1985,1987,1990,1992,1994,1996,1998,2001],{"class":37,"line":1986},19,[35,1988,1989],{"class":299},"        ws.set_column(",[35,1991,131],{"class":568},[35,1993,344],{"class":299},[35,1995,131],{"class":568},[35,1997,344],{"class":299},[35,1999,2000],{"class":568},"24",[35,2002,544],{"class":299},[35,2004,2006,2008,2010,2012,2014,2016,2019],{"class":37,"line":2005},20,[35,2007,1989],{"class":299},[35,2009,1123],{"class":568},[35,2011,344],{"class":299},[35,2013,1123],{"class":568},[35,2015,344],{"class":299},[35,2017,2018],{"class":568},"8",[35,2020,544],{"class":299},[35,2022,2024,2026,2029,2031,2033,2035,2038],{"class":37,"line":2023},21,[35,2025,1989],{"class":299},[35,2027,2028],{"class":568},"2",[35,2030,344],{"class":299},[35,2032,130],{"class":568},[35,2034,344],{"class":299},[35,2036,2037],{"class":568},"11",[35,2039,2040],{"class":299},", pct)\n",[35,2042,2044,2047,2049,2051,2053,2055,2057,2060,2062,2065,2068,2070,2073,2075,2078,2080,2083,2085,2088,2090,2092,2094,2097],{"class":37,"line":2043},22,[35,2045,2046],{"class":299},"        ws.conditional_format(",[35,2048,1123],{"class":568},[35,2050,344],{"class":299},[35,2052,130],{"class":568},[35,2054,344],{"class":299},[35,2056,597],{"class":568},[35,2058,2059],{"class":299},"(out), ",[35,2061,130],{"class":568},[35,2063,2064],{"class":299},", {",[35,2066,2067],{"class":45},"\"type\"",[35,2069,1019],{"class":299},[35,2071,2072],{"class":45},"\"cell\"",[35,2074,344],{"class":299},[35,2076,2077],{"class":45},"\"criteria\"",[35,2079,1019],{"class":299},[35,2081,2082],{"class":45},"\"\u003C\"",[35,2084,344],{"class":299},[35,2086,2087],{"class":45},"\"value\"",[35,2089,1019],{"class":299},[35,2091,1639],{"class":568},[35,2093,344],{"class":299},[35,2095,2096],{"class":45},"\"format\"",[35,2098,2099],{"class":299},": below})\n",[35,2101,2103,2106,2108,2110,2112,2114,2116,2118,2120,2122,2125,2127,2129,2131,2133,2135,2137],{"class":37,"line":2102},23,[35,2104,2105],{"class":299},"    failed[[",[35,2107,422],{"class":45},[35,2109,344],{"class":299},[35,2111,427],{"class":45},[35,2113,344],{"class":299},[35,2115,1266],{"class":45},[35,2117,344],{"class":299},[35,2119,1246],{"class":45},[35,2121,344],{"class":299},[35,2123,2124],{"class":45},"\"SKU\"",[35,2126,344],{"class":299},[35,2128,366],{"class":45},[35,2130,344],{"class":299},[35,2132,740],{"class":45},[35,2134,344],{"class":299},[35,2136,816],{"class":45},[35,2138,1046],{"class":299},[35,2140,2142,2145,2147,2149,2151,2153,2155,2157,2160,2162,2164,2167,2169,2172,2174,2177],{"class":37,"line":2141},24,[35,2143,2144],{"class":45},"            \"QtyOrdered\"",[35,2146,344],{"class":299},[35,2148,444],{"class":45},[35,2150,344],{"class":299},[35,2152,390],{"class":45},[35,2154,344],{"class":299},[35,2156,1671],{"class":45},[35,2158,2159],{"class":299},"]].to_excel(xw, ",[35,2161,348],{"class":347},[35,2163,335],{"class":295},[35,2165,2166],{"class":45},"\"Failed lines\"",[35,2168,344],{"class":299},[35,2170,2171],{"class":347},"index",[35,2173,335],{"class":295},[35,2175,2176],{"class":568},"False",[35,2178,544],{"class":299},[35,2180,2182,2185,2187,2190,2192],{"class":37,"line":2181},25,[35,2183,2184],{"class":299},"    fl ",[35,2186,335],{"class":295},[35,2188,2189],{"class":299}," xw.sheets[",[35,2191,2166],{"class":45},[35,2193,910],{"class":299},[35,2195,2197,2200,2202,2204,2206,2208,2211],{"class":37,"line":2196},26,[35,2198,2199],{"class":299},"    fl.set_column(",[35,2201,131],{"class":568},[35,2203,344],{"class":299},[35,2205,130],{"class":568},[35,2207,344],{"class":299},[35,2209,2210],{"class":568},"14",[35,2212,544],{"class":299},[35,2214,2216,2218,2221,2223,2226,2228,2230,2233,2235,2237,2240],{"class":37,"line":2215},27,[35,2217,2199],{"class":299},[35,2219,2220],{"class":568},"5",[35,2222,344],{"class":299},[35,2224,2225],{"class":568},"6",[35,2227,344],{"class":299},[35,2229,141],{"class":568},[35,2231,2232],{"class":299},", book.add_format({",[35,2234,1821],{"class":45},[35,2236,1019],{"class":299},[35,2238,2239],{"class":45},"\"yyyy-mm-dd\"",[35,2241,2242],{"class":299},"}))\n",[35,2244,2246,2249,2251,2253,2255,2257,2259,2262,2264],{"class":37,"line":2245},28,[35,2247,2248],{"class":299},"    fl.autofilter(",[35,2250,131],{"class":568},[35,2252,344],{"class":299},[35,2254,131],{"class":568},[35,2256,344],{"class":299},[35,2258,597],{"class":568},[35,2260,2261],{"class":299},"(failed), ",[35,2263,2037],{"class":568},[35,2265,544],{"class":299},[35,2267,2269,2272,2274,2276,2278],{"class":37,"line":2268},29,[35,2270,2271],{"class":299},"    fl.freeze_panes(",[35,2273,1123],{"class":568},[35,2275,344],{"class":299},[35,2277,131],{"class":568},[35,2279,544],{"class":299},[35,2281,2283],{"class":37,"line":2282},30,[35,2284,326],{"emptyLinePlaceholder":325},[35,2286,2288,2291,2293,2295,2297],{"class":37,"line":2287},31,[35,2289,2290],{"class":299},"    ws ",[35,2292,335],{"class":295},[35,2294,2189],{"class":299},[35,2296,1886],{"class":45},[35,2298,910],{"class":299},[35,2300,2302,2305,2307,2310,2312,2314,2317],{"class":37,"line":2301},32,[35,2303,2304],{"class":299},"    chart ",[35,2306,335],{"class":295},[35,2308,2309],{"class":299}," book.add_chart({",[35,2311,2067],{"class":45},[35,2313,1019],{"class":299},[35,2315,2316],{"class":45},"\"line\"",[35,2318,1113],{"class":299},[35,2320,2322,2325,2327,2330],{"class":37,"line":2321},33,[35,2323,2324],{"class":299},"    n ",[35,2326,335],{"class":295},[35,2328,2329],{"class":568}," len",[35,2331,2332],{"class":299},"(weekly)\n",[35,2334,2336,2339,2342,2344,2346,2348,2351,2354,2356,2358,2360,2362,2364,2367,2369,2371,2374,2376,2378,2380,2382,2384,2386,2388,2390],{"class":37,"line":2335},34,[35,2337,2338],{"class":299},"    chart.add_series({",[35,2340,2341],{"class":45},"\"name\"",[35,2343,1019],{"class":299},[35,2345,917],{"class":45},[35,2347,344],{"class":299},[35,2349,2350],{"class":45},"\"categories\"",[35,2352,2353],{"class":299},": [",[35,2355,1886],{"class":45},[35,2357,344],{"class":299},[35,2359,1123],{"class":568},[35,2361,344],{"class":299},[35,2363,131],{"class":568},[35,2365,2366],{"class":299},", n, ",[35,2368,131],{"class":568},[35,2370,523],{"class":299},[35,2372,2373],{"class":45},"\"values\"",[35,2375,2353],{"class":299},[35,2377,1886],{"class":45},[35,2379,344],{"class":299},[35,2381,1123],{"class":568},[35,2383,344],{"class":299},[35,2385,130],{"class":568},[35,2387,2366],{"class":299},[35,2389,130],{"class":568},[35,2391,2392],{"class":299},"],\n",[35,2394,2396,2399,2402,2404,2406,2409,2411,2413,2415,2417],{"class":37,"line":2395},35,[35,2397,2398],{"class":45},"                      \"marker\"",[35,2400,2401],{"class":299},": {",[35,2403,2067],{"class":45},[35,2405,1019],{"class":299},[35,2407,2408],{"class":45},"\"circle\"",[35,2410,344],{"class":299},[35,2412,469],{"class":45},[35,2414,1019],{"class":299},[35,2416,130],{"class":568},[35,2418,2419],{"class":299},"}})\n",[35,2421,2423,2425,2427,2429,2432,2434,2436,2438,2440,2442,2444,2446,2448,2450,2452],{"class":37,"line":2422},36,[35,2424,2338],{"class":299},[35,2426,2341],{"class":45},[35,2428,1019],{"class":299},[35,2430,2431],{"class":45},"\"Target\"",[35,2433,344],{"class":299},[35,2435,2350],{"class":45},[35,2437,2353],{"class":299},[35,2439,1886],{"class":45},[35,2441,344],{"class":299},[35,2443,1123],{"class":568},[35,2445,344],{"class":299},[35,2447,131],{"class":568},[35,2449,2366],{"class":299},[35,2451,131],{"class":568},[35,2453,2392],{"class":299},[35,2455,2457,2460,2462,2465,2468,2471,2474,2476,2478,2480,2483,2485,2488,2491,2494,2496,2498,2500,2503,2505,2508],{"class":37,"line":2456},37,[35,2458,2459],{"class":45},"                      \"values\"",[35,2461,1019],{"class":299},[35,2463,2464],{"class":45},"\"={\"",[35,2466,2467],{"class":295}," +",[35,2469,2470],{"class":45}," \",\"",[35,2472,2473],{"class":299},".join([",[35,2475,1957],{"class":568},[35,2477,441],{"class":299},[35,2479,1639],{"class":568},[35,2481,2482],{"class":299},")] ",[35,2484,1040],{"class":295},[35,2486,2487],{"class":299}," n) ",[35,2489,2490],{"class":295},"+",[35,2492,2493],{"class":45}," \"}\"",[35,2495,344],{"class":299},[35,2497,2316],{"class":45},[35,2499,2401],{"class":299},[35,2501,2502],{"class":45},"\"dash_type\"",[35,2504,1019],{"class":299},[35,2506,2507],{"class":45},"\"dash\"",[35,2509,2419],{"class":299},[35,2511,2513,2516,2518,2520,2523,2525,2528,2530,2533,2535,2537,2539,2541],{"class":37,"line":2512},38,[35,2514,2515],{"class":299},"    chart.set_y_axis({",[35,2517,1821],{"class":45},[35,2519,1019],{"class":299},[35,2521,2522],{"class":45},"\"0%\"",[35,2524,344],{"class":299},[35,2526,2527],{"class":45},"\"min\"",[35,2529,1019],{"class":299},[35,2531,2532],{"class":568},"0.7",[35,2534,344],{"class":299},[35,2536,488],{"class":45},[35,2538,1019],{"class":299},[35,2540,1123],{"class":568},[35,2542,1113],{"class":299},[35,2544,2546,2549,2551,2553,2556],{"class":37,"line":2545},39,[35,2547,2548],{"class":299},"    chart.set_title({",[35,2550,2341],{"class":45},[35,2552,1019],{"class":299},[35,2554,2555],{"class":45},"\"OTIF by week against target\"",[35,2557,1113],{"class":299},[35,2559,2561,2564,2567,2570,2573,2575,2578],{"class":37,"line":2560},40,[35,2562,2563],{"class":299},"    ws.insert_chart(",[35,2565,2566],{"class":45},"\"G2\"",[35,2568,2569],{"class":299},", chart, {",[35,2571,2572],{"class":45},"\"x_scale\"",[35,2574,1019],{"class":299},[35,2576,2577],{"class":568},"1.5",[35,2579,1113],{"class":299},[35,2581,2583,2585,2588,2590],{"class":37,"line":2582},41,[35,2584,582],{"class":568},[35,2586,2587],{"class":299},"(failed[",[35,2589,1671],{"class":45},[35,2591,2592],{"class":299},"].value_counts().to_string())\n",[10,2594,2595,2596,2600],{},"The target line uses an array constant instead of a column of 0.95s, which keeps the week table clean. The failed-lines sheet has an autofilter, so the carrier manager can filter to their own lines; ",[14,2597,2599],{"href":2598},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-an-autofilter-to-an-excel-sheet-with-openpyxl\u002F","Add an Autofilter to an Excel Sheet with openpyxl"," covers filters in more depth.",[20,2602,2604],{"id":2603},"common-pitfalls","Common pitfalls",[75,2606,2607,2613,2619,2629,2635,2641],{},[78,2608,2609,2612],{},[81,2610,2611],{},"Scoring shipments instead of order lines."," A line delivered in two parts is one line; aggregate shipments first.",[78,2614,2615,2618],{},[81,2616,2617],{},"Using the first delivery date."," On time means the whole line arrived by the promise; use the last delivery.",[78,2620,2621,2624,2625,2628],{},[81,2622,2623],{},"Calendar-day lateness."," A Friday promise delivered Monday is one working day late; use ",[32,2626,2627],{},"busday_count"," with holidays.",[78,2630,2631,2634],{},[81,2632,2633],{},"Scoring lines not yet due."," Filter to lines promised on or before the period end.",[78,2636,2637,2640],{},[81,2638,2639],{},"Changed promise dates."," If the system overwrites the promised date when a delivery is rescheduled, every line looks on time. Report against the original promise, stored separately.",[78,2642,2643,2646,2647,2649],{},[81,2644,2645],{},"Over-deliveries."," Decide whether delivering more than ordered is in full; ",[32,2648,902],{}," treats it as in full.",[20,2651,2653],{"id":2652},"conclusion","Conclusion",[10,2655,2656,2657,2659],{},"Aggregate shipments to one row per order line — total delivered, number of shipments, last delivery date — and left-join to the order lines. Score on time against the last delivery, in full against total quantity, and OTIF as both, with lateness in working days from ",[32,2658,2627],{},". Report only lines that were due, break the rates down by carrier, customer and week against the target, and list the failed lines with a reason for follow-up.",[20,2661,2663],{"id":2662},"frequently-asked-questions","Frequently asked questions",[10,2665,2666,2669],{},[81,2667,2668],{},"What is OTIF?","\nOn time, in full: the share of order lines whose full quantity arrived by the promised date. It combines two measures that are worth reporting separately as well.",[10,2671,2672,2675,2676,2679],{},[81,2673,2674],{},"How do I count late days excluding weekends in pandas?","\nUse numpy.busday_count(promised, delivered, holidays=...) on datetime64",[35,2677,2678],{},"D"," arrays. It counts working days from the promised date up to the delivery date.",[10,2681,2682,2685],{},[81,2683,2684],{},"How should partial deliveries be counted?","\nSum the delivered quantity across all shipments for the line and use the last delivery date. The line is in full only if the total covers the order, and on time only if the last part arrived by the promise.",[10,2687,2688,2691],{},[81,2689,2690],{},"Should OTIF be measured per order or per line?","\nPer line is more common and more precise; per order is stricter, since one late line fails the whole order. Choose one, state it in the report, and keep it the same over time.",[20,2693,2695],{"id":2694},"related","Related",[75,2697,2698,2704,2711,2718,2725],{},[78,2699,2700,2701,2703],{},"Up one level: ",[14,2702,17],{"href":16}," — the rest of the finance and operations reports.",[78,2705,2706,2710],{},[14,2707,2709],{"href":2708},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-inventory-reorder-report\u002F","Build an Inventory Reorder Report"," — stock levels behind the in-full rate.",[78,2712,2713,2717],{},[14,2714,2716],{"href":2715},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-working-hours-between-two-timestamps\u002F","Calculate Working Hours Between Two Timestamps"," — business time at a finer grain.",[78,2719,2720,2724],{},[14,2721,2723],{"href":2722},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-weekly-sales-report\u002F","Build a Weekly Sales Report"," — another weekly operations report.",[78,2726,2727,2729],{},[14,2728,2599],{"href":2598}," — filtering the failed lines.",[2731,2732,2733],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":30,"searchDepth":309,"depth":309,"links":2735},[2736,2737,2738,2739,2740,2741,2742,2743,2744,2745,2746],{"id":22,"depth":309,"text":23},{"id":72,"depth":309,"text":73},{"id":279,"depth":309,"text":280},{"id":674,"depth":309,"text":675},{"id":952,"depth":309,"text":953},{"id":1132,"depth":309,"text":1133},{"id":1625,"depth":309,"text":1626},{"id":2603,"depth":309,"text":2604},{"id":2652,"depth":309,"text":2653},{"id":2662,"depth":309,"text":2663},{"id":2694,"depth":309,"text":2695},"2026-09-29","Measure delivery performance from order and shipment exports: combine split shipments per order line, count lateness in working days with holidays, score on time, in full and OTIF, break results down by carrier, customer and week against a target, and write an Excel report with the late orders listed for follow-up.","md",[2751,2753,2755,2757],{"q":2668,"a":2752},"On time, in full: the share of order lines whose full quantity arrived by the promised date. It combines two measures that are worth reporting separately as well.",{"q":2674,"a":2754},"Use numpy.busday_count(promised, delivered, holidays=...) on datetime64[D] arrays. It counts working days from the promised date up to the delivery date.",{"q":2684,"a":2756},"Sum the delivered quantity across all shipments for the line and use the last delivery date. The line is in full only if the total covers the order, and on time only if the last part arrived by the promise.",{"q":2690,"a":2758},"Per line is more common and more precise; per order is stricter, since one late line fails the whole order. Choose one, state it in the report, and keep it the same over time.","build an on-time delivery report",null,{"breadcrumb":2762},[2763,2765,2768],{"name":2764,"item":1940},"Home",{"name":2766,"item":2767},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2769,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report",{"title":2772,"description":2773},"On-Time In-Full (OTIF) Delivery Report in Excel with Python","Build an OTIF report with pandas: merge orders and shipments, handle partial deliveries, working-day lateness with numpy busday_count and holidays, on-time, in-full and OTIF rates by carrier, customer and week, Excel output.","build-an-on-time-delivery-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-on-time-delivery-report\u002Findex",[2777,2778],"report-layouts","dates-and-times","how-to","o-kTFPO-Zukbq_TOhgjTe8WRLvPznMxQy5YjPQjvjgw",[2782,2786],{"title":2783,"path":2784,"stem":2785,"children":-1},"Build an Inventory Turnover and Days on Hand Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-inventory-turnover-and-days-on-hand-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-inventory-turnover-and-days-on-hand-report\u002Findex",{"title":2787,"path":2788,"stem":2789,"children":-1},"Build an Overtime Report from Shift Data","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-an-overtime-report-from-shift-data\u002Findex",[2791,2793],{"path":2792,"title":2769},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2794,"title":2766},"\u002Fautomating-reporting-workflows",[],[2797,2801,2804,2807,2810,2813],{"path":2798,"title":2799,"kind":2800},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2802,"title":2803,"kind":2779},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-data-dictionary-sheet-to-an-excel-report","Add a Data Dictionary Sheet to an Excel Report",{"path":2805,"title":2806,"kind":2779},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard","Add a KPI Summary Block to an Excel Dashboard",{"path":2808,"title":2809,"kind":2779},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",{"path":2811,"title":2812,"kind":2779},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-slicer-style-filters-to-an-excel-dashboard","Add a Dropdown Filter That Drives an Excel Dashboard",{"path":2814,"title":2815,"kind":2779},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python","Add a Summary Sheet to an Excel Report with Python",1790972127797]