[{"data":1,"prerenderedAt":2067},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices":2032,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices":2041,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices":2046,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices":2047},{"id":4,"title":5,"body":6,"dateModified":1997,"datePublished":1997,"description":1998,"extension":1999,"faq":2000,"keyword":2009,"kind":2010,"meta":2011,"navigation":88,"path":2021,"seo":2022,"slug":2025,"stem":2026,"topics":2027,"type":2030,"__hash__":2031},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices\u002Findex.md","Check Supplier Invoices Against Contract Prices",{"type":7,"value":8,"toc":1986},"minimark",[9,19,24,53,56,60,290,310,462,466,903,917,920,1113,1117,1287,1294,1298,1718,1721,1869,1873,1910,1914,1920,1924,1933,1939,1945,1951,1955,1982],[10,11,12,13,18],"p",{},"Suppliers make mistakes on invoices, and few of them make mistakes in the customer's favour. A price that should have dropped when a new contract started is still the old one; a quantity is invoiced that was never delivered; a line total does not equal quantity times price. Individually the amounts are small, but across thousands of invoice lines a year they add up, and once an invoice is paid the money is much harder to get back. Accounts payable teams check what they can by eye; a script can check every line. This page builds the check with pandas: the contract price in force on each invoice date, found with an as-of join on a dated price list, compared with the invoiced price within a tolerance; the quantity invoiced compared with the quantity received; the arithmetic of each line and each invoice total; and a list of invoices to hold, with the reason and the value to query, for the payables team. It belongs to ",[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 openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"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}," openpyxl\n",[10,54,55],{},"Four inputs: the contract price list with the date each price took effect; invoice lines with quantity, unit price and line total; invoice headers with the total the supplier charged; and goods received against each invoice. Two suppliers, five invoices, ten lines.",[20,57,59],{"id":58},"step-1-the-contract-price-on-the-invoice-date","Step 1 — The contract price on the invoice date",[25,61,65],{"className":62,"code":63,"language":64,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ncontract = pd.read_excel(\"contract_prices.xlsx\", parse_dates=[\"Effective from\"]).sort_values(\"Effective from\")\nlines = pd.read_excel(\"invoice_lines.xlsx\", parse_dates=[\"Invoice date\"]).sort_values(\"Invoice date\")\n\nchecked = pd.merge_asof(lines, contract, left_on=\"Invoice date\", right_on=\"Effective from\",\n                        by=[\"Supplier\", \"Item\"], direction=\"backward\")\nassert checked[\"Contract price\"].notna().all(), \"an invoiced item has no contract price\"\nprint(checked[[\"Invoice\", \"Invoice date\", \"Item\", \"Unit price\", \"Contract price\", \"Effective from\"]].to_string(index=False))\n","python",[32,66,67,83,90,128,158,163,193,224,242],{"__ignoreMap":30},[35,68,69,73,77,80],{"class":37,"line":38},[35,70,72],{"class":71},"s-kum","import",[35,74,76],{"class":75},"skGVy"," pandas ",[35,78,79],{"class":71},"as",[35,81,82],{"class":75}," pd\n",[35,84,86],{"class":37,"line":85},2,[35,87,89],{"emptyLinePlaceholder":88},true,"\n",[35,91,93,96,99,102,105,108,112,114,117,120,123,125],{"class":37,"line":92},3,[35,94,95],{"class":75},"contract ",[35,97,98],{"class":71},"=",[35,100,101],{"class":75}," pd.read_excel(",[35,103,104],{"class":45},"\"contract_prices.xlsx\"",[35,106,107],{"class":75},", ",[35,109,111],{"class":110},"sa561","parse_dates",[35,113,98],{"class":71},[35,115,116],{"class":75},"[",[35,118,119],{"class":45},"\"Effective from\"",[35,121,122],{"class":75},"]).sort_values(",[35,124,119],{"class":45},[35,126,127],{"class":75},")\n",[35,129,131,134,136,138,141,143,145,147,149,152,154,156],{"class":37,"line":130},4,[35,132,133],{"class":75},"lines ",[35,135,98],{"class":71},[35,137,101],{"class":75},[35,139,140],{"class":45},"\"invoice_lines.xlsx\"",[35,142,107],{"class":75},[35,144,111],{"class":110},[35,146,98],{"class":71},[35,148,116],{"class":75},[35,150,151],{"class":45},"\"Invoice date\"",[35,153,122],{"class":75},[35,155,151],{"class":45},[35,157,127],{"class":75},[35,159,161],{"class":37,"line":160},5,[35,162,89],{"emptyLinePlaceholder":88},[35,164,166,169,171,174,177,179,181,183,186,188,190],{"class":37,"line":165},6,[35,167,168],{"class":75},"checked ",[35,170,98],{"class":71},[35,172,173],{"class":75}," pd.merge_asof(lines, contract, ",[35,175,176],{"class":110},"left_on",[35,178,98],{"class":71},[35,180,151],{"class":45},[35,182,107],{"class":75},[35,184,185],{"class":110},"right_on",[35,187,98],{"class":71},[35,189,119],{"class":45},[35,191,192],{"class":75},",\n",[35,194,196,199,201,203,206,208,211,214,217,219,222],{"class":37,"line":195},7,[35,197,198],{"class":110},"                        by",[35,200,98],{"class":71},[35,202,116],{"class":75},[35,204,205],{"class":45},"\"Supplier\"",[35,207,107],{"class":75},[35,209,210],{"class":45},"\"Item\"",[35,212,213],{"class":75},"], ",[35,215,216],{"class":110},"direction",[35,218,98],{"class":71},[35,220,221],{"class":45},"\"backward\"",[35,223,127],{"class":75},[35,225,227,230,233,236,239],{"class":37,"line":226},8,[35,228,229],{"class":71},"assert",[35,231,232],{"class":75}," checked[",[35,234,235],{"class":45},"\"Contract price\"",[35,237,238],{"class":75},"].notna().all(), ",[35,240,241],{"class":45},"\"an invoiced item has no contract price\"\n",[35,243,245,249,252,255,257,259,261,263,265,268,270,272,274,276,279,282,284,287],{"class":37,"line":244},9,[35,246,248],{"class":247},"sP0c6","print",[35,250,251],{"class":75},"(checked[[",[35,253,254],{"class":45},"\"Invoice\"",[35,256,107],{"class":75},[35,258,151],{"class":45},[35,260,107],{"class":75},[35,262,210],{"class":45},[35,264,107],{"class":75},[35,266,267],{"class":45},"\"Unit price\"",[35,269,107],{"class":75},[35,271,235],{"class":45},[35,273,107],{"class":75},[35,275,119],{"class":45},[35,277,278],{"class":75},"]].to_string(",[35,280,281],{"class":110},"index",[35,283,98],{"class":71},[35,285,286],{"class":247},"False",[35,288,289],{"class":75},"))\n",[10,291,292,293,297,298,301,302,305,306,18],{},"Contract prices change over time — the cable ",[294,295,296],"em",{},"CB-10"," dropped from 4.20 to 3.95 on 1 July, the panel ",[294,299,300],{},"PL-05"," rose from 12.40 to 12.90 on 1 September — so the right price for an invoice line is the one in force on the invoice date. ",[32,303,304],{},"merge_asof"," does exactly that: for each invoice line it takes the most recent contract price, for the same supplier and item, whose effective date is on or before the invoice date. Both tables must be sorted by date for the join to work. An item invoiced without any contract price stops the check, because there is nothing to compare it with — either the item is not on contract, which payables should know about, or its code differs between the two systems. The general technique is covered in ",[14,307,309],{"href":308},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fjoin-rows-to-effective-date-ranges-with-pandas\u002F","Join Rows to Effective Date Ranges with pandas",[311,312,320,321,320,325,320,329,320,337,320,347,320,354,320,358,320,363,320,367,320,372,320,377,320,382,320,388,320,392,320,396,320,400,320,403,320,406,320,410,320,413,320,416,320,419,320,423,320,426,320,429,320,433,320,436,320,439,320,443,320,446,320,451,320,456,320,459],"svg",{"viewBox":313,"role":314,"ariaLabelledBy":315,"xmlns":318,"style":319},"4 0 752 126","img",[316,317],"inv-checks-t","inv-checks-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  ",[322,323,324],"title",{"id":316},"The checks on every invoice line",[326,327,328],"desc",{"id":317},"Each invoice line is joined to the contract price in force on the invoice date. The unit price is compared with the contract price within a tolerance, the quantity with the quantity received, and the line total with quantity times price. Invoice totals are compared with the sum of their lines, and any failure puts the invoice on hold.",[330,331],"rect",{"x":332,"y":333,"width":334,"height":335,"fill":336},"4","0","752","126","#ffffff",[330,338],{"x":339,"y":340,"width":341,"height":342,"rx":343,"fill":344,"stroke":345,"style":346},"20.0","16.0","115.2","94.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[348,349,353],"text",{"x":350,"y":351,"style":352},"77.6","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Contract",[348,355,357],{"x":350,"y":356,"style":352},"59.0","price",[348,359,362],{"x":350,"y":360,"style":361},"78.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","as of the",[348,364,366],{"x":350,"y":365,"style":361},"96.0","invoice date",[37,368],{"x1":369,"y1":370,"x2":371,"y2":370,"stroke":345,"style":346},"140.2","63.0","162.2",[373,374],"polygon",{"points":375,"fill":376},"166.2,63.0 157.2,68.0 157.2,58.0","#5b5cf0",[330,378],{"x":379,"y":340,"width":341,"height":342,"rx":343,"fill":380,"stroke":381,"style":346},"171.2","#fdefd8","var(--gold,#b4740a)",[348,383,387],{"x":384,"y":385,"style":386},"228.8","50.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Price check",[348,389,391],{"x":384,"y":390,"style":361},"69.8","within ½%",[348,393,395],{"x":384,"y":394,"style":361},"87.2","tolerance",[37,397],{"x1":398,"y1":370,"x2":399,"y2":370,"stroke":345,"style":346},"291.4","313.4",[373,401],{"points":402,"fill":376},"317.4,63.0 308.4,68.0 308.4,58.0",[330,404],{"x":405,"y":340,"width":341,"height":342,"rx":343,"fill":380,"stroke":381,"style":346},"322.4",[348,407,409],{"x":408,"y":351,"style":386},"380.0","Quantity",[348,411,412],{"x":408,"y":356,"style":386},"check",[348,414,415],{"x":408,"y":360,"style":361},"vs goods",[348,417,418],{"x":408,"y":365,"style":361},"received",[37,420],{"x1":421,"y1":370,"x2":422,"y2":370,"stroke":345,"style":346},"442.6","464.6",[373,424],{"points":425,"fill":376},"468.6,63.0 459.6,68.0 459.6,58.0",[330,427],{"x":428,"y":340,"width":341,"height":342,"rx":343,"fill":380,"stroke":381,"style":346},"473.6",[348,430,432],{"x":431,"y":385,"style":386},"531.2","Arithmetic",[348,434,435],{"x":431,"y":390,"style":361},"line and invoice",[348,437,438],{"x":431,"y":394,"style":361},"totals",[37,440],{"x1":441,"y1":370,"x2":442,"y2":370,"stroke":345,"style":346},"593.8","615.8",[373,444],{"points":445,"fill":376},"619.8,63.0 610.8,68.0 610.8,58.0",[330,447],{"x":448,"y":340,"width":341,"height":342,"rx":343,"fill":449,"stroke":450,"style":346},"624.8","#fce9e9","var(--danger,#dc2626)",[348,452,455],{"x":453,"y":385,"style":454},"682.4","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Hold list",[348,457,458],{"x":453,"y":390,"style":361},"reason +",[348,460,461],{"x":453,"y":394,"style":361},"amount",[20,463,465],{"id":464},"step-2-price-quantity-and-arithmetic-checks-on-each-line","Step 2 — Price, quantity and arithmetic checks on each line",[25,467,469],{"className":62,"code":468,"language":64,"meta":30,"style":30},"received = pd.read_excel(\"goods_received.xlsx\")\nchecked = checked.merge(received, on=[\"Supplier\", \"Invoice\", \"Item\"], how=\"left\")\n\nTOL = 0.005                                                  # accept prices within 0.5% of contract (rounding)\nchecked[\"Price diff\"] = checked[\"Unit price\"] - checked[\"Contract price\"]\nchecked[\"Price issue\"] = checked[\"Price diff\"] > checked[\"Contract price\"] * TOL\nchecked[\"Qty issue\"] = checked[\"Qty\"] > checked[\"Qty received\"].fillna(0)\nchecked[\"Maths issue\"] = (checked[\"Qty\"] * checked[\"Unit price\"] - checked[\"Line total\"]).abs() > 0.01\n\nchecked[\"Overcharge\"] = (\n    checked[\"Price diff\"].clip(lower=0) * checked[\"Qty\"] * checked[\"Price issue\"]\n    + (checked[\"Qty\"] - checked[\"Qty received\"].fillna(0)).clip(lower=0) * checked[\"Contract price\"]\n    + (checked[\"Line total\"] - checked[\"Qty\"] * checked[\"Unit price\"]).clip(lower=0)\n).round(2)\nissues = checked[checked[[\"Price issue\", \"Qty issue\", \"Maths issue\"]].any(axis=1)]\nprint(issues[[\"Invoice\", \"Item\", \"Qty\", \"Qty received\", \"Unit price\", \"Contract price\", \"Line total\", \"Overcharge\"]].to_string(index=False))\n",[32,470,471,485,523,527,542,571,603,635,676,680,695,732,773,809,820,855],{"__ignoreMap":30},[35,472,473,476,478,480,483],{"class":37,"line":38},[35,474,475],{"class":75},"received ",[35,477,98],{"class":71},[35,479,101],{"class":75},[35,481,482],{"class":45},"\"goods_received.xlsx\"",[35,484,127],{"class":75},[35,486,487,489,491,494,497,499,501,503,505,507,509,511,513,516,518,521],{"class":37,"line":85},[35,488,168],{"class":75},[35,490,98],{"class":71},[35,492,493],{"class":75}," checked.merge(received, ",[35,495,496],{"class":110},"on",[35,498,98],{"class":71},[35,500,116],{"class":75},[35,502,205],{"class":45},[35,504,107],{"class":75},[35,506,254],{"class":45},[35,508,107],{"class":75},[35,510,210],{"class":45},[35,512,213],{"class":75},[35,514,515],{"class":110},"how",[35,517,98],{"class":71},[35,519,520],{"class":45},"\"left\"",[35,522,127],{"class":75},[35,524,525],{"class":37,"line":92},[35,526,89],{"emptyLinePlaceholder":88},[35,528,529,532,535,538],{"class":37,"line":130},[35,530,531],{"class":247},"TOL",[35,533,534],{"class":71}," =",[35,536,537],{"class":247}," 0.005",[35,539,541],{"class":540},"s-wDw","                                                  # accept prices within 0.5% of contract (rounding)\n",[35,543,544,547,550,553,555,557,559,561,564,566,568],{"class":37,"line":160},[35,545,546],{"class":75},"checked[",[35,548,549],{"class":45},"\"Price diff\"",[35,551,552],{"class":75},"] ",[35,554,98],{"class":71},[35,556,232],{"class":75},[35,558,267],{"class":45},[35,560,552],{"class":75},[35,562,563],{"class":71},"-",[35,565,232],{"class":75},[35,567,235],{"class":45},[35,569,570],{"class":75},"]\n",[35,572,573,575,578,580,582,584,586,588,591,593,595,597,600],{"class":37,"line":165},[35,574,546],{"class":75},[35,576,577],{"class":45},"\"Price issue\"",[35,579,552],{"class":75},[35,581,98],{"class":71},[35,583,232],{"class":75},[35,585,549],{"class":45},[35,587,552],{"class":75},[35,589,590],{"class":71},">",[35,592,232],{"class":75},[35,594,235],{"class":45},[35,596,552],{"class":75},[35,598,599],{"class":71},"*",[35,601,602],{"class":247}," TOL\n",[35,604,605,607,610,612,614,616,619,621,623,625,628,631,633],{"class":37,"line":195},[35,606,546],{"class":75},[35,608,609],{"class":45},"\"Qty issue\"",[35,611,552],{"class":75},[35,613,98],{"class":71},[35,615,232],{"class":75},[35,617,618],{"class":45},"\"Qty\"",[35,620,552],{"class":75},[35,622,590],{"class":71},[35,624,232],{"class":75},[35,626,627],{"class":45},"\"Qty received\"",[35,629,630],{"class":75},"].fillna(",[35,632,333],{"class":247},[35,634,127],{"class":75},[35,636,637,639,642,644,646,649,651,653,655,657,659,661,663,665,668,671,673],{"class":37,"line":226},[35,638,546],{"class":75},[35,640,641],{"class":45},"\"Maths issue\"",[35,643,552],{"class":75},[35,645,98],{"class":71},[35,647,648],{"class":75}," (checked[",[35,650,618],{"class":45},[35,652,552],{"class":75},[35,654,599],{"class":71},[35,656,232],{"class":75},[35,658,267],{"class":45},[35,660,552],{"class":75},[35,662,563],{"class":71},[35,664,232],{"class":75},[35,666,667],{"class":45},"\"Line total\"",[35,669,670],{"class":75},"]).abs() ",[35,672,590],{"class":71},[35,674,675],{"class":247}," 0.01\n",[35,677,678],{"class":37,"line":244},[35,679,89],{"emptyLinePlaceholder":88},[35,681,683,685,688,690,692],{"class":37,"line":682},10,[35,684,546],{"class":75},[35,686,687],{"class":45},"\"Overcharge\"",[35,689,552],{"class":75},[35,691,98],{"class":71},[35,693,694],{"class":75}," (\n",[35,696,698,701,703,706,709,711,713,716,718,720,722,724,726,728,730],{"class":37,"line":697},11,[35,699,700],{"class":75},"    checked[",[35,702,549],{"class":45},[35,704,705],{"class":75},"].clip(",[35,707,708],{"class":110},"lower",[35,710,98],{"class":71},[35,712,333],{"class":247},[35,714,715],{"class":75},") ",[35,717,599],{"class":71},[35,719,232],{"class":75},[35,721,618],{"class":45},[35,723,552],{"class":75},[35,725,599],{"class":71},[35,727,232],{"class":75},[35,729,577],{"class":45},[35,731,570],{"class":75},[35,733,735,738,740,742,744,746,748,750,752,754,757,759,761,763,765,767,769,771],{"class":37,"line":734},12,[35,736,737],{"class":71},"    +",[35,739,648],{"class":75},[35,741,618],{"class":45},[35,743,552],{"class":75},[35,745,563],{"class":71},[35,747,232],{"class":75},[35,749,627],{"class":45},[35,751,630],{"class":75},[35,753,333],{"class":247},[35,755,756],{"class":75},")).clip(",[35,758,708],{"class":110},[35,760,98],{"class":71},[35,762,333],{"class":247},[35,764,715],{"class":75},[35,766,599],{"class":71},[35,768,232],{"class":75},[35,770,235],{"class":45},[35,772,570],{"class":75},[35,774,776,778,780,782,784,786,788,790,792,794,796,798,801,803,805,807],{"class":37,"line":775},13,[35,777,737],{"class":71},[35,779,648],{"class":75},[35,781,667],{"class":45},[35,783,552],{"class":75},[35,785,563],{"class":71},[35,787,232],{"class":75},[35,789,618],{"class":45},[35,791,552],{"class":75},[35,793,599],{"class":71},[35,795,232],{"class":75},[35,797,267],{"class":45},[35,799,800],{"class":75},"]).clip(",[35,802,708],{"class":110},[35,804,98],{"class":71},[35,806,333],{"class":247},[35,808,127],{"class":75},[35,810,812,815,818],{"class":37,"line":811},14,[35,813,814],{"class":75},").round(",[35,816,817],{"class":247},"2",[35,819,127],{"class":75},[35,821,823,826,828,831,833,835,837,839,841,844,847,849,852],{"class":37,"line":822},15,[35,824,825],{"class":75},"issues ",[35,827,98],{"class":71},[35,829,830],{"class":75}," checked[checked[[",[35,832,577],{"class":45},[35,834,107],{"class":75},[35,836,609],{"class":45},[35,838,107],{"class":75},[35,840,641],{"class":45},[35,842,843],{"class":75},"]].any(",[35,845,846],{"class":110},"axis",[35,848,98],{"class":71},[35,850,851],{"class":247},"1",[35,853,854],{"class":75},")]\n",[35,856,858,860,863,865,867,869,871,873,875,877,879,881,883,885,887,889,891,893,895,897,899,901],{"class":37,"line":857},16,[35,859,248],{"class":247},[35,861,862],{"class":75},"(issues[[",[35,864,254],{"class":45},[35,866,107],{"class":75},[35,868,210],{"class":45},[35,870,107],{"class":75},[35,872,618],{"class":45},[35,874,107],{"class":75},[35,876,627],{"class":45},[35,878,107],{"class":75},[35,880,267],{"class":45},[35,882,107],{"class":75},[35,884,235],{"class":45},[35,886,107],{"class":75},[35,888,667],{"class":45},[35,890,107],{"class":75},[35,892,687],{"class":45},[35,894,278],{"class":75},[35,896,281],{"class":110},[35,898,98],{"class":71},[35,900,286],{"class":247},[35,902,289],{"class":75},[10,904,905,906,908,909,912,913,916],{},"Three checks run on each line. The ",[294,907,357],{}," check compares the invoiced unit price with the contract price, accepting a small tolerance for rounding — half a per cent here — and ignoring prices below contract, which are in the buyer's favour. The ",[294,910,911],{},"quantity"," check is the second leg of a three-way match: the quantity invoiced must not exceed the quantity received against that invoice. The ",[294,914,915],{},"arithmetic"," check confirms that the line total equals quantity times unit price to the penny. Each failure is valued: the excess price times the quantity, the contract price of units invoiced but not received, and any amount by which the line total exceeds the arithmetic. That turns a list of exceptions into a figure the payables team can take to the supplier.",[10,918,919],{},"In this sample, invoice A-7802 charges the old cable price of 4.20 six weeks after it dropped to 3.95 — £100 over on 400 units. Invoice A-7840 has three problems: a price of 7.84 on an item contracted at 7.80, outside the tolerance; 1,500 gloves invoiced against 1,200 received; and a glove line total of £1,560 where 1,500 × £1.10 is £1,650 — an arithmetic slip in the buyer's favour, which is not valued as an overcharge but still means the invoice needs correcting. The NORD invoices look fine at first glance, because 12.90 is the current panel price — but the check shows otherwise: the August invoice uses 12.90, and that price only took effect on 1 September, so the August invoice is overcharged by 50p on each of its 60 panels. Spotting a price applied a week or two early is exactly what an eye scanning invoices against today's price list cannot do.",[311,921,320,927,320,930,320,933,320,938,320,946,320,950,320,956,320,960,320,964,320,968,320,972,320,976,320,980,320,984,320,987,320,990,320,994,320,996,320,999,320,1001,320,1004,320,1006,320,1009,320,1012,320,1015,320,1017,320,1021,320,1023,320,1025,320,1027,320,1030,320,1032,320,1035,320,1037,320,1041,320,1043,320,1046,320,1048,320,1051,320,1053,320,1056,320,1058,320,1061,320,1064,320,1067,320,1069,320,1071,320,1073,320,1076,320,1078,320,1081,320,1083,320,1087,320,1090,320,1094,320,1096,320,1099,320,1101,320,1103,320,1105,320,1108,320,1110],{"viewBox":922,"role":314,"ariaLabelledBy":923,"xmlns":318,"style":926},"125 0 510 196",[924,925],"inv-issues-t","inv-issues-d","width:100%;max-width:510px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[322,928,929],{"id":924},"Invoice lines that fail a check",[326,931,932],{"id":925},"Invoice A-7802 charges 4.20 for a cable contracted at 3.95 since July, 100 pounds over. A-7840 charges 7.84 for an item contracted at 7.80 and invoices 1,500 gloves against 1,200 received. N-3310 in August charges the September panel price of 12.90 instead of 12.40.",[330,934],{"x":935,"y":333,"width":936,"height":937,"fill":336},"125","510","196",[330,939],{"x":940,"y":340,"width":941,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"141.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[330,947],{"x":948,"y":340,"width":949,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"173.0","71.9",[348,951,955],{"x":952,"y":953,"style":954},"209.0","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[330,957],{"x":958,"y":340,"width":959,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"244.9","64.0",[348,961,963],{"x":962,"y":953,"style":954},"276.9","B",[330,965],{"x":966,"y":340,"width":967,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"308.9","213.0",[348,969,971],{"x":970,"y":953,"style":954},"415.4","C",[330,973],{"x":974,"y":340,"width":975,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"521.9","97.1",[348,977,979],{"x":978,"y":953,"style":954},"570.4","D",[330,981],{"x":940,"y":982,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},"40.0","28.0",[348,985,851],{"x":986,"y":356,"style":954},"157.0",[330,988],{"x":948,"y":982,"width":949,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},"stroke-width:1.5px",[348,991,993],{"x":952,"y":356,"style":992},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Invoice",[330,995],{"x":958,"y":982,"width":959,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,997,998],{"x":962,"y":356,"style":992},"Item",[330,1000],{"x":966,"y":982,"width":967,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,1002,1003],{"x":970,"y":356,"style":992},"Problem",[330,1005],{"x":974,"y":982,"width":975,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,1007,1008],{"x":978,"y":356,"style":992},"Overcharge",[330,1010],{"x":940,"y":1011,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},"68.0",[348,1013,817],{"x":986,"y":1014,"style":954},"87.0",[330,1016],{"x":948,"y":1011,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1018,1020],{"x":952,"y":1014,"style":1019},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","A-7802",[330,1022],{"x":958,"y":1011,"width":959,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1024,296],{"x":962,"y":1014,"style":1019},[330,1026],{"x":966,"y":1011,"width":967,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1028,1029],{"x":970,"y":1014,"style":1019},"4.20 vs 3.95 since 1 Jul",[330,1031],{"x":974,"y":1011,"width":975,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1033,1034],{"x":978,"y":1014,"style":1019},"£100.00",[330,1036],{"x":940,"y":365,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},[348,1038,1040],{"x":986,"y":1039,"style":954},"115.0","3",[330,1042],{"x":948,"y":365,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1044,1045],{"x":952,"y":1039,"style":1019},"A-7840",[330,1047],{"x":958,"y":365,"width":959,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1049,1050],{"x":962,"y":1039,"style":1019},"CB-20",[330,1052],{"x":966,"y":365,"width":967,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1054,1055],{"x":970,"y":1039,"style":1019},"7.84 vs 7.80",[330,1057],{"x":974,"y":365,"width":975,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1059,1060],{"x":978,"y":1039,"style":1019},"£10.00",[330,1062],{"x":940,"y":1063,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},"124.0",[348,1065,332],{"x":986,"y":1066,"style":954},"143.0",[330,1068],{"x":948,"y":1063,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1070,1045],{"x":952,"y":1066,"style":1019},[330,1072],{"x":958,"y":1063,"width":959,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1074,1075],{"x":962,"y":1066,"style":1019},"GL-01",[330,1077],{"x":966,"y":1063,"width":967,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1079,1080],{"x":970,"y":1066,"style":1019},"1,500 invoiced, 1,200 received",[330,1082],{"x":974,"y":1063,"width":975,"height":983,"rx":333,"fill":449,"stroke":450,"style":989},[348,1084,1086],{"x":978,"y":1066,"style":1085},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","£330.00",[330,1088],{"x":940,"y":1089,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},"152.0",[348,1091,1093],{"x":986,"y":1092,"style":954},"171.0","5",[330,1095],{"x":948,"y":1089,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1097,1098],{"x":952,"y":1092,"style":1019},"N-3310",[330,1100],{"x":958,"y":1089,"width":959,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1102,300],{"x":962,"y":1092,"style":1019},[330,1104],{"x":966,"y":1089,"width":967,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1106,1107],{"x":970,"y":1092,"style":1019},"12.90 before it took effect",[330,1109],{"x":974,"y":1089,"width":975,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1111,1112],{"x":978,"y":1092,"style":1019},"£30.00",[20,1114,1116],{"id":1115},"step-3-invoice-totals-and-duplicates","Step 3 — Invoice totals and duplicates",[25,1118,1120],{"className":62,"code":1119,"language":64,"meta":30,"style":30},"headers = pd.read_excel(\"invoice_headers.xlsx\")\nsums = checked.groupby(\"Invoice\")[\"Line total\"].sum().round(2).rename(\"Sum of lines\")\ntotals = headers.set_index(\"Invoice\").join(sums)\ntotals[\"Total issue\"] = (totals[\"Invoice total\"] - totals[\"Sum of lines\"]).abs() > 0.01\ndupes = lines.groupby([\"Supplier\", \"Invoice\"]).size().index.to_frame(index=False).duplicated(subset=[\"Invoice\"], keep=False)\nprint(totals.to_string())\nprint(\"duplicate invoice numbers across suppliers:\", int(dupes.sum()))\n",[32,1121,1122,1136,1166,1181,1214,1262,1269],{"__ignoreMap":30},[35,1123,1124,1127,1129,1131,1134],{"class":37,"line":38},[35,1125,1126],{"class":75},"headers ",[35,1128,98],{"class":71},[35,1130,101],{"class":75},[35,1132,1133],{"class":45},"\"invoice_headers.xlsx\"",[35,1135,127],{"class":75},[35,1137,1138,1141,1143,1146,1148,1151,1153,1156,1158,1161,1164],{"class":37,"line":85},[35,1139,1140],{"class":75},"sums ",[35,1142,98],{"class":71},[35,1144,1145],{"class":75}," checked.groupby(",[35,1147,254],{"class":45},[35,1149,1150],{"class":75},")[",[35,1152,667],{"class":45},[35,1154,1155],{"class":75},"].sum().round(",[35,1157,817],{"class":247},[35,1159,1160],{"class":75},").rename(",[35,1162,1163],{"class":45},"\"Sum of lines\"",[35,1165,127],{"class":75},[35,1167,1168,1171,1173,1176,1178],{"class":37,"line":92},[35,1169,1170],{"class":75},"totals ",[35,1172,98],{"class":71},[35,1174,1175],{"class":75}," headers.set_index(",[35,1177,254],{"class":45},[35,1179,1180],{"class":75},").join(sums)\n",[35,1182,1183,1186,1189,1191,1193,1196,1199,1201,1203,1206,1208,1210,1212],{"class":37,"line":130},[35,1184,1185],{"class":75},"totals[",[35,1187,1188],{"class":45},"\"Total issue\"",[35,1190,552],{"class":75},[35,1192,98],{"class":71},[35,1194,1195],{"class":75}," (totals[",[35,1197,1198],{"class":45},"\"Invoice total\"",[35,1200,552],{"class":75},[35,1202,563],{"class":71},[35,1204,1205],{"class":75}," totals[",[35,1207,1163],{"class":45},[35,1209,670],{"class":75},[35,1211,590],{"class":71},[35,1213,675],{"class":247},[35,1215,1216,1219,1221,1224,1226,1228,1230,1233,1235,1237,1239,1242,1245,1247,1249,1251,1253,1256,1258,1260],{"class":37,"line":160},[35,1217,1218],{"class":75},"dupes ",[35,1220,98],{"class":71},[35,1222,1223],{"class":75}," lines.groupby([",[35,1225,205],{"class":45},[35,1227,107],{"class":75},[35,1229,254],{"class":45},[35,1231,1232],{"class":75},"]).size().index.to_frame(",[35,1234,281],{"class":110},[35,1236,98],{"class":71},[35,1238,286],{"class":247},[35,1240,1241],{"class":75},").duplicated(",[35,1243,1244],{"class":110},"subset",[35,1246,98],{"class":71},[35,1248,116],{"class":75},[35,1250,254],{"class":45},[35,1252,213],{"class":75},[35,1254,1255],{"class":110},"keep",[35,1257,98],{"class":71},[35,1259,286],{"class":247},[35,1261,127],{"class":75},[35,1263,1264,1266],{"class":37,"line":165},[35,1265,248],{"class":247},[35,1267,1268],{"class":75},"(totals.to_string())\n",[35,1270,1271,1273,1276,1279,1281,1284],{"class":37,"line":195},[35,1272,248],{"class":247},[35,1274,1275],{"class":75},"(",[35,1277,1278],{"class":45},"\"duplicate invoice numbers across suppliers:\"",[35,1280,107],{"class":75},[35,1282,1283],{"class":247},"int",[35,1285,1286],{"class":75},"(dupes.sum()))\n",[10,1288,1289,1290,18],{},"The invoice total the supplier asks to be paid must equal the sum of its lines; a mismatch means a line was missing from the data, a charge was added at header level, or the total was simply typed wrong. Four of the five invoices add up. The fifth, N-3352, asks for £2,152 when its two lines come to £2,052 — every line on it passed the price and quantity checks, so without this check the extra £100 would have been paid. A duplicate invoice number from the same supplier is the classic way an invoice gets paid twice, and is worth checking against the whole payment history, not just this batch, as in ",[14,1291,1293],{"href":1292},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fdetect-possible-duplicate-invoices\u002F","Detect Possible Duplicate Invoices",[20,1295,1297],{"id":1296},"step-4-the-hold-list-for-payables","Step 4 — The hold list for payables",[25,1299,1301],{"className":62,"code":1300,"language":64,"meta":30,"style":30},"def reasons(g):\n    r = []\n    if g[\"Price issue\"].any(): r.append(\"price above contract\")\n    if g[\"Qty issue\"].any(): r.append(\"quantity above received\")\n    if g[\"Maths issue\"].any(): r.append(\"line arithmetic\")\n    return \"; \".join(r)\n\nhold = (checked.groupby([\"Supplier\", \"Invoice\"])\n        .apply(lambda g: pd.Series({\"Reason\": reasons(g), \"Amount to query\": g[\"Overcharge\"].sum()}), include_groups=False)\n        .reset_index().join(totals, on=\"Invoice\"))\nextra = (hold[\"Invoice total\"] - hold[\"Sum of lines\"]).clip(lower=0)\nhold.loc[hold[\"Total issue\"], \"Reason\"] = (hold.loc[hold[\"Total issue\"], \"Reason\"] + \"; invoice total ≠ lines\").str.strip(\"; \")\nhold[\"Amount to query\"] = (hold[\"Amount to query\"] + extra).round(2)\nhold = hold[hold[\"Reason\"] != \"\"][[\"Supplier\", \"Invoice\", \"Reason\", \"Amount to query\", \"Invoice total\"]]\nwith pd.ExcelWriter(\"invoice_checks_2026-09.xlsx\") as xw:\n    hold.to_excel(xw, sheet_name=\"Hold\", index=False)\n    issues.to_excel(xw, sheet_name=\"Failed lines\", index=False)\n    checked.to_excel(xw, sheet_name=\"All lines\", index=False)\nprint(hold.to_string(index=False))\nprint(\"total to query: £\", hold[\"Amount to query\"].sum())\n",[32,1302,1303,1315,1325,1343,1358,1373,1384,1388,1407,1444,1457,1488,1528,1554,1597,1615,1638,1661,1684,1700],{"__ignoreMap":30},[35,1304,1305,1308,1312],{"class":37,"line":38},[35,1306,1307],{"class":71},"def",[35,1309,1311],{"class":1310},"s_Opv"," reasons",[35,1313,1314],{"class":75},"(g):\n",[35,1316,1317,1320,1322],{"class":37,"line":85},[35,1318,1319],{"class":75},"    r ",[35,1321,98],{"class":71},[35,1323,1324],{"class":75}," []\n",[35,1326,1327,1330,1333,1335,1338,1341],{"class":37,"line":92},[35,1328,1329],{"class":71},"    if",[35,1331,1332],{"class":75}," g[",[35,1334,577],{"class":45},[35,1336,1337],{"class":75},"].any(): r.append(",[35,1339,1340],{"class":45},"\"price above contract\"",[35,1342,127],{"class":75},[35,1344,1345,1347,1349,1351,1353,1356],{"class":37,"line":130},[35,1346,1329],{"class":71},[35,1348,1332],{"class":75},[35,1350,609],{"class":45},[35,1352,1337],{"class":75},[35,1354,1355],{"class":45},"\"quantity above received\"",[35,1357,127],{"class":75},[35,1359,1360,1362,1364,1366,1368,1371],{"class":37,"line":160},[35,1361,1329],{"class":71},[35,1363,1332],{"class":75},[35,1365,641],{"class":45},[35,1367,1337],{"class":75},[35,1369,1370],{"class":45},"\"line arithmetic\"",[35,1372,127],{"class":75},[35,1374,1375,1378,1381],{"class":37,"line":165},[35,1376,1377],{"class":71},"    return",[35,1379,1380],{"class":45}," \"; \"",[35,1382,1383],{"class":75},".join(r)\n",[35,1385,1386],{"class":37,"line":195},[35,1387,89],{"emptyLinePlaceholder":88},[35,1389,1390,1393,1395,1398,1400,1402,1404],{"class":37,"line":226},[35,1391,1392],{"class":75},"hold ",[35,1394,98],{"class":71},[35,1396,1397],{"class":75}," (checked.groupby([",[35,1399,205],{"class":45},[35,1401,107],{"class":75},[35,1403,254],{"class":45},[35,1405,1406],{"class":75},"])\n",[35,1408,1409,1412,1415,1418,1421,1424,1427,1430,1432,1435,1438,1440,1442],{"class":37,"line":244},[35,1410,1411],{"class":75},"        .apply(",[35,1413,1414],{"class":71},"lambda",[35,1416,1417],{"class":75}," g: pd.Series({",[35,1419,1420],{"class":45},"\"Reason\"",[35,1422,1423],{"class":75},": reasons(g), ",[35,1425,1426],{"class":45},"\"Amount to query\"",[35,1428,1429],{"class":75},": g[",[35,1431,687],{"class":45},[35,1433,1434],{"class":75},"].sum()}), ",[35,1436,1437],{"class":110},"include_groups",[35,1439,98],{"class":71},[35,1441,286],{"class":247},[35,1443,127],{"class":75},[35,1445,1446,1449,1451,1453,1455],{"class":37,"line":682},[35,1447,1448],{"class":75},"        .reset_index().join(totals, ",[35,1450,496],{"class":110},[35,1452,98],{"class":71},[35,1454,254],{"class":45},[35,1456,289],{"class":75},[35,1458,1459,1462,1464,1467,1469,1471,1473,1476,1478,1480,1482,1484,1486],{"class":37,"line":697},[35,1460,1461],{"class":75},"extra ",[35,1463,98],{"class":71},[35,1465,1466],{"class":75}," (hold[",[35,1468,1198],{"class":45},[35,1470,552],{"class":75},[35,1472,563],{"class":71},[35,1474,1475],{"class":75}," hold[",[35,1477,1163],{"class":45},[35,1479,800],{"class":75},[35,1481,708],{"class":110},[35,1483,98],{"class":71},[35,1485,333],{"class":247},[35,1487,127],{"class":75},[35,1489,1490,1493,1495,1497,1499,1501,1503,1506,1508,1510,1512,1514,1517,1520,1523,1526],{"class":37,"line":734},[35,1491,1492],{"class":75},"hold.loc[hold[",[35,1494,1188],{"class":45},[35,1496,213],{"class":75},[35,1498,1420],{"class":45},[35,1500,552],{"class":75},[35,1502,98],{"class":71},[35,1504,1505],{"class":75}," (hold.loc[hold[",[35,1507,1188],{"class":45},[35,1509,213],{"class":75},[35,1511,1420],{"class":45},[35,1513,552],{"class":75},[35,1515,1516],{"class":71},"+",[35,1518,1519],{"class":45}," \"; invoice total ≠ lines\"",[35,1521,1522],{"class":75},").str.strip(",[35,1524,1525],{"class":45},"\"; \"",[35,1527,127],{"class":75},[35,1529,1530,1533,1535,1537,1539,1541,1543,1545,1547,1550,1552],{"class":37,"line":775},[35,1531,1532],{"class":75},"hold[",[35,1534,1426],{"class":45},[35,1536,552],{"class":75},[35,1538,98],{"class":71},[35,1540,1466],{"class":75},[35,1542,1426],{"class":45},[35,1544,552],{"class":75},[35,1546,1516],{"class":71},[35,1548,1549],{"class":75}," extra).round(",[35,1551,817],{"class":247},[35,1553,127],{"class":75},[35,1555,1556,1558,1560,1563,1565,1567,1570,1573,1576,1578,1580,1582,1584,1586,1588,1590,1592,1594],{"class":37,"line":811},[35,1557,1392],{"class":75},[35,1559,98],{"class":71},[35,1561,1562],{"class":75}," hold[hold[",[35,1564,1420],{"class":45},[35,1566,552],{"class":75},[35,1568,1569],{"class":71},"!=",[35,1571,1572],{"class":45}," \"\"",[35,1574,1575],{"class":75},"][[",[35,1577,205],{"class":45},[35,1579,107],{"class":75},[35,1581,254],{"class":45},[35,1583,107],{"class":75},[35,1585,1420],{"class":45},[35,1587,107],{"class":75},[35,1589,1426],{"class":45},[35,1591,107],{"class":75},[35,1593,1198],{"class":45},[35,1595,1596],{"class":75},"]]\n",[35,1598,1599,1602,1605,1608,1610,1612],{"class":37,"line":822},[35,1600,1601],{"class":71},"with",[35,1603,1604],{"class":75}," pd.ExcelWriter(",[35,1606,1607],{"class":45},"\"invoice_checks_2026-09.xlsx\"",[35,1609,715],{"class":75},[35,1611,79],{"class":71},[35,1613,1614],{"class":75}," xw:\n",[35,1616,1617,1620,1623,1625,1628,1630,1632,1634,1636],{"class":37,"line":857},[35,1618,1619],{"class":75},"    hold.to_excel(xw, ",[35,1621,1622],{"class":110},"sheet_name",[35,1624,98],{"class":71},[35,1626,1627],{"class":45},"\"Hold\"",[35,1629,107],{"class":75},[35,1631,281],{"class":110},[35,1633,98],{"class":71},[35,1635,286],{"class":247},[35,1637,127],{"class":75},[35,1639,1641,1644,1646,1648,1651,1653,1655,1657,1659],{"class":37,"line":1640},17,[35,1642,1643],{"class":75},"    issues.to_excel(xw, ",[35,1645,1622],{"class":110},[35,1647,98],{"class":71},[35,1649,1650],{"class":45},"\"Failed lines\"",[35,1652,107],{"class":75},[35,1654,281],{"class":110},[35,1656,98],{"class":71},[35,1658,286],{"class":247},[35,1660,127],{"class":75},[35,1662,1664,1667,1669,1671,1674,1676,1678,1680,1682],{"class":37,"line":1663},18,[35,1665,1666],{"class":75},"    checked.to_excel(xw, ",[35,1668,1622],{"class":110},[35,1670,98],{"class":71},[35,1672,1673],{"class":45},"\"All lines\"",[35,1675,107],{"class":75},[35,1677,281],{"class":110},[35,1679,98],{"class":71},[35,1681,286],{"class":247},[35,1683,127],{"class":75},[35,1685,1687,1689,1692,1694,1696,1698],{"class":37,"line":1686},19,[35,1688,248],{"class":247},[35,1690,1691],{"class":75},"(hold.to_string(",[35,1693,281],{"class":110},[35,1695,98],{"class":71},[35,1697,286],{"class":247},[35,1699,289],{"class":75},[35,1701,1703,1705,1707,1710,1713,1715],{"class":37,"line":1702},20,[35,1704,248],{"class":247},[35,1706,1275],{"class":75},[35,1708,1709],{"class":45},"\"total to query: £\"",[35,1711,1712],{"class":75},", hold[",[35,1714,1426],{"class":45},[35,1716,1717],{"class":75},"].sum())\n",[10,1719,1720],{},"The hold list has one row per invoice that failed any check: supplier, invoice number, the reasons in plain words, the amount to query and the invoice total. Payables can release everything not on the list for payment and send the rest back to the supplier with the detail from the failed-lines sheet, which shows each problem line next to the contract price and the quantity received. Here four of the five invoices are held, with £570 to query in total — small for one batch, but a steady stream of such errors across a year is real money, and suppliers that know every line is checked tend to make fewer of them.",[311,1722,320,1728,320,1731,320,1734,320,1739,320,1742,320,1745,320,1748,320,1752,320,1755,320,1759,320,1762,320,1764,320,1767,320,1769,320,1771,320,1773,320,1776,320,1778,320,1781,320,1783,320,1785,320,1787,320,1789,320,1791,320,1794,320,1796,320,1798,320,1800,320,1802,320,1804,320,1806,320,1808,320,1811,320,1813,320,1816,320,1818,320,1820,320,1822,320,1824,320,1826,320,1828,320,1830,320,1832,320,1834,320,1836,320,1838,320,1841,320,1843,320,1846,320,1848,320,1850,320,1853,320,1857,320,1859,320,1862,320,1864,320,1866],{"viewBox":1723,"role":314,"ariaLabelledBy":1724,"xmlns":318,"style":1727},"156 0 447 224",[1725,1726],"inv-hold-t","inv-hold-d","width:100%;max-width:447px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[322,1729,1730],{"id":1725},"The hold list",[326,1732,1733],{"id":1726},"Four of the five invoices are held. A-7802 for a price above contract, 100 pounds. A-7840 for price, quantity and arithmetic, 340 pounds. N-3310 for a price above contract, 30 pounds. N-3352 because its total is 100 pounds more than its lines. In all 570 pounds to query.",[330,1735],{"x":1736,"y":333,"width":1737,"height":1738,"fill":336},"156","447","224",[330,1740],{"x":1741,"y":340,"width":941,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"172.6",[330,1743],{"x":1744,"y":340,"width":949,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"204.6",[348,1746,955],{"x":1747,"y":953,"style":954},"240.5",[330,1749],{"x":1750,"y":340,"width":1751,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"276.4","183.7",[348,1753,963],{"x":1754,"y":953,"style":954},"368.3",[330,1756],{"x":1757,"y":340,"width":1758,"height":942,"rx":333,"fill":943,"stroke":944,"style":945},"460.2","127.3",[348,1760,971],{"x":1761,"y":953,"style":954},"523.8",[330,1763],{"x":1741,"y":982,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},[348,1765,851],{"x":1766,"y":356,"style":954},"188.6",[330,1768],{"x":1744,"y":982,"width":949,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,1770,993],{"x":1747,"y":356,"style":992},[330,1772],{"x":1750,"y":982,"width":1751,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,1774,1775],{"x":1754,"y":356,"style":992},"Reason",[330,1777],{"x":1757,"y":982,"width":1758,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,1779,1780],{"x":1761,"y":356,"style":992},"Amount to query",[330,1782],{"x":1741,"y":1011,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},[348,1784,817],{"x":1766,"y":1014,"style":954},[330,1786],{"x":1744,"y":1011,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1788,1020],{"x":1747,"y":1014,"style":1019},[330,1790],{"x":1750,"y":1011,"width":1751,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1792,1793],{"x":1754,"y":1014,"style":1019},"price above contract",[330,1795],{"x":1757,"y":1011,"width":1758,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1797,1034],{"x":1761,"y":1014,"style":1019},[330,1799],{"x":1741,"y":365,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},[348,1801,1040],{"x":1766,"y":1039,"style":954},[330,1803],{"x":1744,"y":365,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1805,1045],{"x":1747,"y":1039,"style":1019},[330,1807],{"x":1750,"y":365,"width":1751,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1809,1810],{"x":1754,"y":1039,"style":1019},"price, quantity, arithmetic",[330,1812],{"x":1757,"y":365,"width":1758,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1814,1815],{"x":1761,"y":1039,"style":1019},"£340.00",[330,1817],{"x":1741,"y":1063,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},[348,1819,332],{"x":1766,"y":1066,"style":954},[330,1821],{"x":1744,"y":1063,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1823,1098],{"x":1747,"y":1066,"style":1019},[330,1825],{"x":1750,"y":1063,"width":1751,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1827,1793],{"x":1754,"y":1066,"style":1019},[330,1829],{"x":1757,"y":1063,"width":1758,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1831,1112],{"x":1761,"y":1066,"style":1019},[330,1833],{"x":1741,"y":1089,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},[348,1835,1093],{"x":1766,"y":1092,"style":954},[330,1837],{"x":1744,"y":1089,"width":949,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1839,1840],{"x":1747,"y":1092,"style":1019},"N-3352",[330,1842],{"x":1750,"y":1089,"width":1751,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1844,1845],{"x":1754,"y":1092,"style":1019},"invoice total ≠ lines",[330,1847],{"x":1757,"y":1089,"width":1758,"height":983,"rx":333,"fill":336,"stroke":944,"style":945},[348,1849,1034],{"x":1761,"y":1092,"style":1019},[330,1851],{"x":1741,"y":1852,"width":941,"height":983,"rx":333,"fill":943,"stroke":944,"style":945},"180.0",[348,1854,1856],{"x":1766,"y":1855,"style":954},"199.0","6",[330,1858],{"x":1744,"y":1852,"width":949,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,1860,1861],{"x":1747,"y":1855,"style":992},"Total",[330,1863],{"x":1750,"y":1852,"width":1751,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[330,1865],{"x":1757,"y":1852,"width":1758,"height":983,"rx":333,"fill":344,"stroke":345,"style":989},[348,1867,1868],{"x":1761,"y":1855,"style":992},"£570.00",[20,1870,1872],{"id":1871},"common-pitfalls","Common pitfalls",[1874,1875,1876,1884,1892,1898,1904],"ul",{},[1877,1878,1879,1883],"li",{},[1880,1881,1882],"strong",{},"Comparing with today's contract price."," Use the price in force on the invoice date.",[1877,1885,1886,1891],{},[1880,1887,1888,1889,18],{},"Unsorted tables in ",[32,1890,304],{}," Both must be sorted by the date columns.",[1877,1893,1894,1897],{},[1880,1895,1896],{},"No tolerance."," Rounding differences of a fraction of a penny should not hold an invoice.",[1877,1899,1900,1903],{},[1880,1901,1902],{},"Price checks only."," Check quantity against goods received and the arithmetic too.",[1877,1905,1906,1909],{},[1880,1907,1908],{},"Exceptions without a value."," Put a figure on every failure so payables can prioritise.",[20,1911,1913],{"id":1912},"conclusion","Conclusion",[10,1915,1916,1917,1919],{},"To check supplier invoices before payment, join each line to the contract price in force on the invoice date with ",[32,1918,304],{},", compare the unit price with it within a small tolerance, compare the quantity with the quantity received, check line arithmetic and invoice totals, and value each failure. Grouping the failures by invoice gives a hold list with reasons and an amount to query, so payables can pay the clean invoices and challenge the rest.",[20,1921,1923],{"id":1922},"frequently-asked-questions","Frequently asked questions",[10,1925,1926,1929,1930,18],{},[1880,1927,1928],{},"How do I find the contract price on the invoice date in pandas?","\nSort both tables by date and use ",[32,1931,1932],{},"pd.merge_asof(lines, prices, left_on=\"Invoice date\", right_on=\"Effective from\", by=[\"Supplier\", \"Item\"])",[10,1934,1935,1938],{},[1880,1936,1937],{},"What is a three-way match?","\nChecking that the purchase order, the goods received and the invoice agree on item, quantity and price before paying.",[10,1940,1941,1944],{},[1880,1942,1943],{},"Why use a tolerance when comparing invoice prices?","\nUnit prices are often rounded differently by the supplier's system; a small tolerance avoids holding invoices for fractions of a penny.",[10,1946,1947,1950],{},[1880,1948,1949],{},"How do I value an invoice overcharge?","\nMultiply the excess unit price by the quantity, add the contract price of any units invoiced but not received, and add any excess in the line total.",[20,1952,1954],{"id":1953},"related","Related",[1874,1956,1957,1963,1968,1975],{},[1877,1958,1959,1960,1962],{},"Up one level: ",[14,1961,17],{"href":16}," — finance and operations reports.",[1877,1964,1965,1967],{},[14,1966,309],{"href":308}," — the as-of join.",[1877,1969,1970,1974],{},[14,1971,1973],{"href":1972},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates\u002F","Update a Price List with Effective Dates"," — keeping the contract prices.",[1877,1976,1977,1981],{},[14,1978,1980],{"href":1979},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-supplier-scorecard\u002F","Build a Supplier Scorecard"," — price variance as part of supplier performance.",[1983,1984,1985],"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 .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":85,"depth":85,"links":1987},[1988,1989,1990,1991,1992,1993,1994,1995,1996],{"id":22,"depth":85,"text":23},{"id":58,"depth":85,"text":59},{"id":464,"depth":85,"text":465},{"id":1115,"depth":85,"text":1116},{"id":1296,"depth":85,"text":1297},{"id":1871,"depth":85,"text":1872},{"id":1912,"depth":85,"text":1913},{"id":1922,"depth":85,"text":1923},{"id":1953,"depth":85,"text":1954},"2026-10-02","Check every supplier invoice line before payment with pandas: look up the contract price in force on the invoice date, allow a small tolerance, compare the quantity invoiced with the quantity received, check the line arithmetic and the invoice total, value every overcharge, and produce a list of invoices to hold with the reason and the amount to query — written to Excel for the accounts payable team.","md",[2001,2003,2005,2007],{"q":1928,"a":2002},"Sort both tables by date and use pd.merge_asof(lines, prices, left_on=\"Invoice date\", right_on=\"Effective from\", by=[\"Supplier\", \"Item\"]).",{"q":1937,"a":2004},"Checking that the purchase order, the goods received and the invoice agree on item, quantity and price before paying.",{"q":1943,"a":2006},"Unit prices are often rounded differently by the supplier's system; a small tolerance avoids holding invoices for fractions of a penny.",{"q":1949,"a":2008},"Multiply the excess unit price by the quantity, add the contract price of any units invoiced but not received, and add any excess in the line total.","check invoices against contract prices python",null,{"breadcrumb":2012},[2013,2016,2019],{"name":2014,"item":2015},"Home","\u002F",{"name":2017,"item":2018},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2020,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices",{"title":2023,"description":2024},"Check Supplier Invoices Against Contract Prices with Python","Invoice price checking with pandas: contract price on the invoice date with merge_asof, tolerance, quantity against goods received, line and total arithmetic, overcharge value and a hold list in Excel.","check-supplier-invoices-against-contract-prices","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices\u002Findex",[2028,2029],"report-layouts","pandas","how-to","guidRqx8gIIoNYLnBBdHj862m3Qgg6THOHuOeH5Xd0s",[2033,2037],{"title":2034,"path":2035,"stem":2036,"children":-1},"Calculate Weighted Average Cost Inventory Valuation","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-weighted-average-cost-inventory-valuation","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcalculate-weighted-average-cost-inventory-valuation\u002Findex",{"title":2038,"path":2039,"stem":2040,"children":-1},"Compare Supplier Quotes with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcompare-supplier-quotes-with-python","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcompare-supplier-quotes-with-python\u002Findex",[2042,2044],{"path":2043,"title":2020},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2045,"title":2017},"\u002Fautomating-reporting-workflows",[],[2048,2052,2055,2058,2061,2064],{"path":2049,"title":2050,"kind":2051},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2053,"title":2054,"kind":2030},"\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":2056,"title":2057,"kind":2030},"\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":2059,"title":2060,"kind":2030},"\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":2062,"title":2063,"kind":2030},"\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":2065,"title":2066,"kind":2030},"\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",1790972127887]