[{"data":1,"prerenderedAt":2121},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates":2088,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates":2097,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates":2102,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates":2103},{"id":4,"title":5,"body":6,"dateModified":2054,"datePublished":2054,"description":2055,"extension":2056,"faq":2057,"keyword":2066,"kind":2067,"meta":2068,"navigation":194,"path":2077,"seo":2078,"slug":2081,"stem":2082,"topics":2083,"type":2086,"__hash__":2087},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates\u002Findex.md","Update a Price List with Effective Dates",{"type":7,"value":8,"toc":2043},"minimark",[9,28,33,62,69,162,166,533,536,636,640,1006,1013,1017,1327,1330,1494,1498,1912,1919,1923,1958,1962,1965,1969,1986,1992,1998,2008,2012,2039],[10,11,12,13,17,18,21,22,27],"p",{},"A price list that only holds today's prices answers one question — what does this cost now? — and loses the answers to every other: what did it cost when this order was placed, what will it cost next month, when did the price last change? Price lists that keep history store each price with an ",[14,15,16],"em",{},"effective from"," and an ",[14,19,20],{},"effective to"," date, so every product has a series of rows covering the calendar without gaps or overlaps. Maintaining such a list by hand in Excel is error-prone: someone adds a new price but forgets to close the old one, and for a while the product has two prices; or a change is entered with a date in last month, quietly rewriting prices that invoices were already based on. This page applies a sheet of price changes to a dated price list with pandas, closing current prices, adding new rows, rejecting changes that would break history, validating the result, and producing a report of what changed. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[41,42,43],"code",{"__ignoreMap":39},[44,45,48,52,56,59],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," pandas",[44,60,61],{"class":54}," openpyxl\n",[10,63,64,65,68],{},"The price list holds one row per product per price period, with an open-ended ",[14,66,67],{},"To"," date of 31 December 9999 on each current price — a common convention that avoids blank end dates. The change sheet lists the new prices with the date each takes effect. Prices up to the end of October 2026 have already been invoiced, so that period is closed.",[70,71,79,80,79,84,79,88,79,96,79,106,79,113,79,119,79,124,79,128,79,132,79,136,79,141,79,146,79,150,79,153,79,156,79,159],"svg",{"viewBox":72,"role":73,"ariaLabelledBy":74,"xmlns":77,"style":78},"4 0 752 199","img",[75,76],"pl-history-t","pl-history-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  ",[81,82,83],"title",{"id":75},"Current prices only, or prices with history",[85,86,87],"desc",{"id":76},"A list of current prices is simple but cannot answer what something cost on a past date or will cost after a scheduled change. A dated list keeps every price with its period, so any date can be priced and changes can be entered in advance.",[89,90],"rect",{"x":91,"y":92,"width":93,"height":94,"fill":95},"4","0","752","199","#ffffff",[89,97],{"x":98,"y":99,"width":100,"height":101,"rx":102,"fill":103,"stroke":104,"style":105},"20.0","16.0","351.0","167.0","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[107,108,112],"text",{"x":109,"y":110,"style":111},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Current prices only",[46,114],{"x1":115,"y1":116,"x2":117,"y2":116,"stroke":104,"style":118},"36.0","57.0","355.0","stroke-width:1px",[107,120,123],{"x":109,"y":121,"style":122},"77.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","one row per product",[107,125,127],{"x":109,"y":126,"style":122},"103.0","overwritten on every change",[107,129,131],{"x":109,"y":130,"style":122},"129.0","past prices lost",[107,133,135],{"x":109,"y":134,"style":122},"155.0","changes entered on the day",[89,137],{"x":138,"y":99,"width":100,"height":101,"rx":102,"fill":139,"stroke":140,"style":105},"389.0","#d9f4f1","var(--teal,#0f9488)",[107,142,145],{"x":143,"y":110,"style":144},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Prices with effective dates",[46,147],{"x1":148,"y1":116,"x2":149,"y2":116,"stroke":140,"style":118},"405.0","724.0",[107,151,152],{"x":143,"y":121,"style":122},"one row per product and period",[107,154,155],{"x":143,"y":126,"style":122},"history kept",[107,157,158],{"x":143,"y":130,"style":122},"any date can be priced",[107,160,161],{"x":143,"y":134,"style":122},"future changes entered ahead",[29,163,165],{"id":164},"step-1-read-both-and-check-the-changes","Step 1 — Read both and check the changes",[34,167,171],{"className":168,"code":169,"language":170,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nOPEN = pd.Timestamp(\"9999-12-31\")\nCLOSED_UNTIL = pd.Timestamp(\"2026-10-31\")                    # invoiced periods may not change\n\nprices = pd.read_excel(\"price_list.xlsx\", parse_dates=[\"From\", \"To\"])\nchanges = pd.read_excel(\"price_changes.xlsx\", parse_dates=[\"Effective from\"]).sort_values([\"Product\", \"Effective from\"])\n\ndef change_problem(c):\n    if c[\"New price\"] \u003C= 0:\n        return \"price must be positive\"\n    if c[\"Effective from\"] \u003C= CLOSED_UNTIL:\n        return f\"backdated into a closed period (closed until {CLOSED_UNTIL:%d %b %Y})\"\n    return None\n\nchanges[\"Problem\"] = changes.apply(change_problem, axis=1)\nrejected = changes[changes[\"Problem\"].notna()]\naccepted = changes[changes[\"Problem\"].isna()]\nprint(rejected[[\"Product\", \"Effective from\", \"New price\", \"Problem\"]].to_string(index=False))\n","python",[41,172,173,189,196,215,235,240,278,314,319,332,356,365,383,425,434,439,465,481,496],{"__ignoreMap":39},[44,174,175,179,183,186],{"class":46,"line":47},[44,176,178],{"class":177},"s-kum","import",[44,180,182],{"class":181},"skGVy"," pandas ",[44,184,185],{"class":177},"as",[44,187,188],{"class":181}," pd\n",[44,190,192],{"class":46,"line":191},2,[44,193,195],{"emptyLinePlaceholder":194},true,"\n",[44,197,199,203,206,209,212],{"class":46,"line":198},3,[44,200,202],{"class":201},"sP0c6","OPEN",[44,204,205],{"class":177}," =",[44,207,208],{"class":181}," pd.Timestamp(",[44,210,211],{"class":54},"\"9999-12-31\"",[44,213,214],{"class":181},")\n",[44,216,218,221,223,225,228,231],{"class":46,"line":217},4,[44,219,220],{"class":201},"CLOSED_UNTIL",[44,222,205],{"class":177},[44,224,208],{"class":181},[44,226,227],{"class":54},"\"2026-10-31\"",[44,229,230],{"class":181},")                    ",[44,232,234],{"class":233},"s-wDw","# invoiced periods may not change\n",[44,236,238],{"class":46,"line":237},5,[44,239,195],{"emptyLinePlaceholder":194},[44,241,243,246,249,252,255,258,262,264,267,270,272,275],{"class":46,"line":242},6,[44,244,245],{"class":181},"prices ",[44,247,248],{"class":177},"=",[44,250,251],{"class":181}," pd.read_excel(",[44,253,254],{"class":54},"\"price_list.xlsx\"",[44,256,257],{"class":181},", ",[44,259,261],{"class":260},"sa561","parse_dates",[44,263,248],{"class":177},[44,265,266],{"class":181},"[",[44,268,269],{"class":54},"\"From\"",[44,271,257],{"class":181},[44,273,274],{"class":54},"\"To\"",[44,276,277],{"class":181},"])\n",[44,279,281,284,286,288,291,293,295,297,299,302,305,308,310,312],{"class":46,"line":280},7,[44,282,283],{"class":181},"changes ",[44,285,248],{"class":177},[44,287,251],{"class":181},[44,289,290],{"class":54},"\"price_changes.xlsx\"",[44,292,257],{"class":181},[44,294,261],{"class":260},[44,296,248],{"class":177},[44,298,266],{"class":181},[44,300,301],{"class":54},"\"Effective from\"",[44,303,304],{"class":181},"]).sort_values([",[44,306,307],{"class":54},"\"Product\"",[44,309,257],{"class":181},[44,311,301],{"class":54},[44,313,277],{"class":181},[44,315,317],{"class":46,"line":316},8,[44,318,195],{"emptyLinePlaceholder":194},[44,320,322,325,329],{"class":46,"line":321},9,[44,323,324],{"class":177},"def",[44,326,328],{"class":327},"s_Opv"," change_problem",[44,330,331],{"class":181},"(c):\n",[44,333,335,338,341,344,347,350,353],{"class":46,"line":334},10,[44,336,337],{"class":177},"    if",[44,339,340],{"class":181}," c[",[44,342,343],{"class":54},"\"New price\"",[44,345,346],{"class":181},"] ",[44,348,349],{"class":177},"\u003C=",[44,351,352],{"class":201}," 0",[44,354,355],{"class":181},":\n",[44,357,359,362],{"class":46,"line":358},11,[44,360,361],{"class":177},"        return",[44,363,364],{"class":54}," \"price must be positive\"\n",[44,366,368,370,372,374,376,378,381],{"class":46,"line":367},12,[44,369,337],{"class":177},[44,371,340],{"class":181},[44,373,301],{"class":54},[44,375,346],{"class":181},[44,377,349],{"class":177},[44,379,380],{"class":201}," CLOSED_UNTIL",[44,382,355],{"class":181},[44,384,386,388,391,394,398,400,403,406,409,411,414,416,419,422],{"class":46,"line":385},13,[44,387,361],{"class":177},[44,389,390],{"class":177}," f",[44,392,393],{"class":54},"\"backdated into a closed period (closed until ",[44,395,397],{"class":396},"sSjpA","{",[44,399,220],{"class":201},[44,401,402],{"class":181},":",[44,404,405],{"class":177},"%",[44,407,408],{"class":181},"d ",[44,410,405],{"class":177},[44,412,413],{"class":181},"b ",[44,415,405],{"class":177},[44,417,418],{"class":181},"Y",[44,420,421],{"class":396},"}",[44,423,424],{"class":54},")\"\n",[44,426,428,431],{"class":46,"line":427},14,[44,429,430],{"class":177},"    return",[44,432,433],{"class":201}," None\n",[44,435,437],{"class":46,"line":436},15,[44,438,195],{"emptyLinePlaceholder":194},[44,440,442,445,448,450,452,455,458,460,463],{"class":46,"line":441},16,[44,443,444],{"class":181},"changes[",[44,446,447],{"class":54},"\"Problem\"",[44,449,346],{"class":181},[44,451,248],{"class":177},[44,453,454],{"class":181}," changes.apply(change_problem, ",[44,456,457],{"class":260},"axis",[44,459,248],{"class":177},[44,461,462],{"class":201},"1",[44,464,214],{"class":181},[44,466,468,471,473,476,478],{"class":46,"line":467},17,[44,469,470],{"class":181},"rejected ",[44,472,248],{"class":177},[44,474,475],{"class":181}," changes[changes[",[44,477,447],{"class":54},[44,479,480],{"class":181},"].notna()]\n",[44,482,484,487,489,491,493],{"class":46,"line":483},18,[44,485,486],{"class":181},"accepted ",[44,488,248],{"class":177},[44,490,475],{"class":181},[44,492,447],{"class":54},[44,494,495],{"class":181},"].isna()]\n",[44,497,499,502,505,507,509,511,513,515,517,519,522,525,527,530],{"class":46,"line":498},19,[44,500,501],{"class":201},"print",[44,503,504],{"class":181},"(rejected[[",[44,506,307],{"class":54},[44,508,257],{"class":181},[44,510,301],{"class":54},[44,512,257],{"class":181},[44,514,343],{"class":54},[44,516,257],{"class":181},[44,518,447],{"class":54},[44,520,521],{"class":181},"]].to_string(",[44,523,524],{"class":260},"index",[44,526,248],{"class":177},[44,528,529],{"class":201},"False",[44,531,532],{"class":181},"))\n",[10,534,535],{},"Every change is checked before anything is applied. A price of zero or less is rejected — it is almost always a blank cell or a typing slip, and a genuinely free product needs a deliberate decision. A change effective on or before the end of the closed period is rejected because it would rewrite prices that have already been invoiced; a correction to a past price is a credit-note matter, not a price-list edit. Here the cable's price cut dated August and the gadget row with a zero price are rejected with their reasons, and the other five changes go ahead. Sorting the changes by product and date matters for the next step: two changes to the same product — the second widget gets one in November and another in December — must be applied in date order.",[70,537,79,542,79,545,79,548,79,551,79,556,79,562,79,567,79,571,79,577,79,582,79,587,79,593,79,597,79,600,79,603,79,607,79,612,79,615,79,619,79,622,79,625,79,630,79,633],{"viewBox":538,"role":73,"ariaLabelledBy":539,"xmlns":77,"style":78},"4 0 752 108",[540,541],"pl-apply-t","pl-apply-d",[81,543,544],{"id":540},"Applying one price change",[85,546,547],{"id":541},"A change is first checked for a positive price and a date after the closed period. The product's current price row is then closed the day before the change takes effect, and a new row is added from that date with an open end date. A product with no current price simply gets the new row.",[89,549],{"x":91,"y":92,"width":93,"height":550,"fill":95},"108",[89,552],{"x":98,"y":99,"width":553,"height":554,"rx":555,"fill":103,"stroke":104,"style":105},"153.0","76.5","12",[107,557,561],{"x":558,"y":559,"style":560},"96.5","41.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Check the change",[107,563,566],{"x":558,"y":564,"style":565},"61.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","price > 0, not",[107,568,570],{"x":558,"y":569,"style":565},"78.5","backdated",[46,572],{"x1":573,"y1":574,"x2":575,"y2":574,"stroke":576,"style":105},"178.0","54.2","200.0","var(--brand,#5b5cf0)",[578,579],"polygon",{"points":580,"fill":581},"204.0,54.2 195.0,59.2 195.0,49.2","#5b5cf0",[89,583],{"x":584,"y":99,"width":553,"height":554,"rx":555,"fill":585,"stroke":586,"style":105},"209.0","#e7ebef","var(--line,#cdd5e6)",[107,588,592],{"x":589,"y":590,"style":591},"285.5","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Find current row",[107,594,596],{"x":589,"y":595,"style":565},"69.8","To = 9999-12-31",[46,598],{"x1":599,"y1":574,"x2":138,"y2":574,"stroke":576,"style":105},"367.0",[578,601],{"points":602,"fill":581},"393.0,54.2 384.0,59.2 384.0,49.2",[89,604],{"x":605,"y":99,"width":553,"height":554,"rx":555,"fill":606,"stroke":576,"style":105},"398.0","#f0f4ff",[107,608,611],{"x":609,"y":590,"style":610},"474.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Close it",[107,613,614],{"x":609,"y":595,"style":565},"To = effective − 1 day",[46,616],{"x1":617,"y1":574,"x2":618,"y2":574,"stroke":576,"style":105},"556.0","578.0",[578,620],{"points":621,"fill":581},"582.0,54.2 573.0,59.2 573.0,49.2",[89,623],{"x":624,"y":99,"width":553,"height":554,"rx":555,"fill":139,"stroke":140,"style":105},"587.0",[107,626,629],{"x":627,"y":559,"style":628},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Add new row",[107,631,632],{"x":627,"y":564,"style":565},"From = effective, open",[107,634,635],{"x":627,"y":569,"style":565},"end",[29,637,639],{"id":638},"step-2-close-the-current-price-and-add-the-new-one","Step 2 — Close the current price and add the new one",[34,641,643],{"className":168,"code":642,"language":170,"meta":39,"style":39},"def apply_change(prices, product, effective, new_price):\n    prices = prices.copy()\n    current = (prices[\"Product\"] == product) & (prices[\"To\"] == OPEN)\n    if current.any():\n        start = prices.loc[current, \"From\"].iloc[0]\n        if effective \u003C= start:\n            raise ValueError(f\"{product}: new price from {effective:%d %b %Y} is not after the current price's start {start:%d %b %Y}\")\n        prices.loc[current, \"To\"] = effective - pd.Timedelta(days=1)\n    new_row = pd.DataFrame([{\"Product\": product, \"From\": effective, \"To\": OPEN, \"Price\": new_price}])\n    return pd.concat([prices, new_row], ignore_index=True)\n\nbefore = prices.copy()\nfor c in accepted.itertuples(index=False):\n    prices = apply_change(prices, c.Product, c._1, c._2)\nprices = prices.sort_values([\"Product\", \"From\"]).reset_index(drop=True)\nprint(prices.assign(From=prices[\"From\"].dt.date, To=prices[\"To\"].dt.date).to_string(index=False))\n",[41,644,645,655,665,701,708,728,741,815,843,878,895,899,908,931,940,967],{"__ignoreMap":39},[44,646,647,649,652],{"class":46,"line":47},[44,648,324],{"class":177},[44,650,651],{"class":327}," apply_change",[44,653,654],{"class":181},"(prices, product, effective, new_price):\n",[44,656,657,660,662],{"class":46,"line":191},[44,658,659],{"class":181},"    prices ",[44,661,248],{"class":177},[44,663,664],{"class":181}," prices.copy()\n",[44,666,667,670,672,675,677,679,682,685,688,690,692,694,696,699],{"class":46,"line":198},[44,668,669],{"class":181},"    current ",[44,671,248],{"class":177},[44,673,674],{"class":181}," (prices[",[44,676,307],{"class":54},[44,678,346],{"class":181},[44,680,681],{"class":177},"==",[44,683,684],{"class":181}," product) ",[44,686,687],{"class":177},"&",[44,689,674],{"class":181},[44,691,274],{"class":54},[44,693,346],{"class":181},[44,695,681],{"class":177},[44,697,698],{"class":201}," OPEN",[44,700,214],{"class":181},[44,702,703,705],{"class":46,"line":217},[44,704,337],{"class":177},[44,706,707],{"class":181}," current.any():\n",[44,709,710,713,715,718,720,723,725],{"class":46,"line":237},[44,711,712],{"class":181},"        start ",[44,714,248],{"class":177},[44,716,717],{"class":181}," prices.loc[current, ",[44,719,269],{"class":54},[44,721,722],{"class":181},"].iloc[",[44,724,92],{"class":201},[44,726,727],{"class":181},"]\n",[44,729,730,733,736,738],{"class":46,"line":242},[44,731,732],{"class":177},"        if",[44,734,735],{"class":181}," effective ",[44,737,349],{"class":177},[44,739,740],{"class":181}," start:\n",[44,742,743,746,749,752,755,758,760,763,765,768,770,773,775,777,779,781,783,785,787,789,792,794,797,799,801,803,805,807,809,811,813],{"class":46,"line":280},[44,744,745],{"class":177},"            raise",[44,747,748],{"class":201}," ValueError",[44,750,751],{"class":181},"(",[44,753,754],{"class":177},"f",[44,756,757],{"class":54},"\"",[44,759,397],{"class":396},[44,761,762],{"class":181},"product",[44,764,421],{"class":396},[44,766,767],{"class":54},": new price from ",[44,769,397],{"class":396},[44,771,772],{"class":181},"effective",[44,774,402],{"class":177},[44,776,405],{"class":181},[44,778,408],{"class":177},[44,780,405],{"class":181},[44,782,413],{"class":177},[44,784,405],{"class":181},[44,786,418],{"class":177},[44,788,421],{"class":396},[44,790,791],{"class":54}," is not after the current price's start ",[44,793,397],{"class":396},[44,795,796],{"class":181},"start:",[44,798,405],{"class":177},[44,800,408],{"class":181},[44,802,405],{"class":177},[44,804,413],{"class":181},[44,806,405],{"class":177},[44,808,418],{"class":181},[44,810,421],{"class":396},[44,812,757],{"class":54},[44,814,214],{"class":181},[44,816,817,820,822,824,826,828,831,834,837,839,841],{"class":46,"line":316},[44,818,819],{"class":181},"        prices.loc[current, ",[44,821,274],{"class":54},[44,823,346],{"class":181},[44,825,248],{"class":177},[44,827,735],{"class":181},[44,829,830],{"class":177},"-",[44,832,833],{"class":181}," pd.Timedelta(",[44,835,836],{"class":260},"days",[44,838,248],{"class":177},[44,840,462],{"class":201},[44,842,214],{"class":181},[44,844,845,848,850,853,855,858,860,863,865,868,870,872,875],{"class":46,"line":321},[44,846,847],{"class":181},"    new_row ",[44,849,248],{"class":177},[44,851,852],{"class":181}," pd.DataFrame([{",[44,854,307],{"class":54},[44,856,857],{"class":181},": product, ",[44,859,269],{"class":54},[44,861,862],{"class":181},": effective, ",[44,864,274],{"class":54},[44,866,867],{"class":181},": ",[44,869,202],{"class":201},[44,871,257],{"class":181},[44,873,874],{"class":54},"\"Price\"",[44,876,877],{"class":181},": new_price}])\n",[44,879,880,882,885,888,890,893],{"class":46,"line":334},[44,881,430],{"class":177},[44,883,884],{"class":181}," pd.concat([prices, new_row], ",[44,886,887],{"class":260},"ignore_index",[44,889,248],{"class":177},[44,891,892],{"class":201},"True",[44,894,214],{"class":181},[44,896,897],{"class":46,"line":358},[44,898,195],{"emptyLinePlaceholder":194},[44,900,901,904,906],{"class":46,"line":367},[44,902,903],{"class":181},"before ",[44,905,248],{"class":177},[44,907,664],{"class":181},[44,909,910,913,916,919,922,924,926,928],{"class":46,"line":385},[44,911,912],{"class":177},"for",[44,914,915],{"class":181}," c ",[44,917,918],{"class":177},"in",[44,920,921],{"class":181}," accepted.itertuples(",[44,923,524],{"class":260},[44,925,248],{"class":177},[44,927,529],{"class":201},[44,929,930],{"class":181},"):\n",[44,932,933,935,937],{"class":46,"line":427},[44,934,659],{"class":181},[44,936,248],{"class":177},[44,938,939],{"class":181}," apply_change(prices, c.Product, c._1, c._2)\n",[44,941,942,944,946,949,951,953,955,958,961,963,965],{"class":46,"line":436},[44,943,245],{"class":181},[44,945,248],{"class":177},[44,947,948],{"class":181}," prices.sort_values([",[44,950,307],{"class":54},[44,952,257],{"class":181},[44,954,269],{"class":54},[44,956,957],{"class":181},"]).reset_index(",[44,959,960],{"class":260},"drop",[44,962,248],{"class":177},[44,964,892],{"class":201},[44,966,214],{"class":181},[44,968,969,971,974,977,979,982,984,987,989,991,993,995,998,1000,1002,1004],{"class":46,"line":441},[44,970,501],{"class":201},[44,972,973],{"class":181},"(prices.assign(",[44,975,976],{"class":260},"From",[44,978,248],{"class":177},[44,980,981],{"class":181},"prices[",[44,983,269],{"class":54},[44,985,986],{"class":181},"].dt.date, ",[44,988,67],{"class":260},[44,990,248],{"class":177},[44,992,981],{"class":181},[44,994,274],{"class":54},[44,996,997],{"class":181},"].dt.date).to_string(",[44,999,524],{"class":260},[44,1001,248],{"class":177},[44,1003,529],{"class":201},[44,1005,532],{"class":181},[10,1007,1008,1009,1012],{},"Applying a change is two edits. The product's current row — the one with the open end date — is closed the day before the new price takes effect, and a new row is added starting on the effective date with an open end. A product that has no current price, such as the new product ",[14,1010,1011],{},"NEW-9",", simply gets the new row. One more check guards against a change dated before the start of the current price, which would leave the current row ending before it began. The second widget shows why order matters: the November change closes the 8.40 price on 31 October and opens 8.90; the December change then closes 8.90 on 30 November and opens 9.10, giving three consecutive periods.",[29,1014,1016],{"id":1015},"step-3-prove-the-list-is-still-consistent","Step 3 — Prove the list is still consistent",[34,1018,1020],{"className":168,"code":1019,"language":170,"meta":39,"style":39},"def check_periods(prices):\n    issues = []\n    for product, g in prices.groupby(\"Product\"):\n        g = g.sort_values(\"From\")\n        if (g[\"To\"] \u003C g[\"From\"]).any():\n            issues.append(f\"{product}: a row ends before it starts\")\n        gaps = g[\"From\"].iloc[1:].values - (g[\"To\"].iloc[:-1] + pd.Timedelta(days=1)).values\n        if (gaps > pd.Timedelta(0)).any():\n            issues.append(f\"{product}: gap between price periods\")\n        if (gaps \u003C pd.Timedelta(0)).any():\n            issues.append(f\"{product}: overlapping price periods\")\n        if (g[\"To\"] == OPEN).sum() != 1:\n            issues.append(f\"{product}: {(g['To'] == OPEN).sum()} open-ended prices\")\n    return issues\n\nprint(\"issues:\", check_periods(prices) or \"none\")\n",[41,1021,1022,1032,1042,1059,1073,1095,1115,1162,1179,1198,1212,1231,1256,1296,1303,1307],{"__ignoreMap":39},[44,1023,1024,1026,1029],{"class":46,"line":47},[44,1025,324],{"class":177},[44,1027,1028],{"class":327}," check_periods",[44,1030,1031],{"class":181},"(prices):\n",[44,1033,1034,1037,1039],{"class":46,"line":191},[44,1035,1036],{"class":181},"    issues ",[44,1038,248],{"class":177},[44,1040,1041],{"class":181}," []\n",[44,1043,1044,1047,1050,1052,1055,1057],{"class":46,"line":198},[44,1045,1046],{"class":177},"    for",[44,1048,1049],{"class":181}," product, g ",[44,1051,918],{"class":177},[44,1053,1054],{"class":181}," prices.groupby(",[44,1056,307],{"class":54},[44,1058,930],{"class":181},[44,1060,1061,1064,1066,1069,1071],{"class":46,"line":217},[44,1062,1063],{"class":181},"        g ",[44,1065,248],{"class":177},[44,1067,1068],{"class":181}," g.sort_values(",[44,1070,269],{"class":54},[44,1072,214],{"class":181},[44,1074,1075,1077,1080,1082,1084,1087,1090,1092],{"class":46,"line":237},[44,1076,732],{"class":177},[44,1078,1079],{"class":181}," (g[",[44,1081,274],{"class":54},[44,1083,346],{"class":181},[44,1085,1086],{"class":177},"\u003C",[44,1088,1089],{"class":181}," g[",[44,1091,269],{"class":54},[44,1093,1094],{"class":181},"]).any():\n",[44,1096,1097,1100,1102,1104,1106,1108,1110,1113],{"class":46,"line":242},[44,1098,1099],{"class":181},"            issues.append(",[44,1101,754],{"class":177},[44,1103,757],{"class":54},[44,1105,397],{"class":396},[44,1107,762],{"class":181},[44,1109,421],{"class":396},[44,1111,1112],{"class":54},": a row ends before it starts\"",[44,1114,214],{"class":181},[44,1116,1117,1120,1122,1124,1126,1128,1130,1133,1135,1137,1139,1142,1144,1146,1148,1151,1153,1155,1157,1159],{"class":46,"line":280},[44,1118,1119],{"class":181},"        gaps ",[44,1121,248],{"class":177},[44,1123,1089],{"class":181},[44,1125,269],{"class":54},[44,1127,722],{"class":181},[44,1129,462],{"class":201},[44,1131,1132],{"class":181},":].values ",[44,1134,830],{"class":177},[44,1136,1079],{"class":181},[44,1138,274],{"class":54},[44,1140,1141],{"class":181},"].iloc[:",[44,1143,830],{"class":177},[44,1145,462],{"class":201},[44,1147,346],{"class":181},[44,1149,1150],{"class":177},"+",[44,1152,833],{"class":181},[44,1154,836],{"class":260},[44,1156,248],{"class":177},[44,1158,462],{"class":201},[44,1160,1161],{"class":181},")).values\n",[44,1163,1164,1166,1169,1172,1174,1176],{"class":46,"line":316},[44,1165,732],{"class":177},[44,1167,1168],{"class":181}," (gaps ",[44,1170,1171],{"class":177},">",[44,1173,833],{"class":181},[44,1175,92],{"class":201},[44,1177,1178],{"class":181},")).any():\n",[44,1180,1181,1183,1185,1187,1189,1191,1193,1196],{"class":46,"line":321},[44,1182,1099],{"class":181},[44,1184,754],{"class":177},[44,1186,757],{"class":54},[44,1188,397],{"class":396},[44,1190,762],{"class":181},[44,1192,421],{"class":396},[44,1194,1195],{"class":54},": gap between price periods\"",[44,1197,214],{"class":181},[44,1199,1200,1202,1204,1206,1208,1210],{"class":46,"line":334},[44,1201,732],{"class":177},[44,1203,1168],{"class":181},[44,1205,1086],{"class":177},[44,1207,833],{"class":181},[44,1209,92],{"class":201},[44,1211,1178],{"class":181},[44,1213,1214,1216,1218,1220,1222,1224,1226,1229],{"class":46,"line":358},[44,1215,1099],{"class":181},[44,1217,754],{"class":177},[44,1219,757],{"class":54},[44,1221,397],{"class":396},[44,1223,762],{"class":181},[44,1225,421],{"class":396},[44,1227,1228],{"class":54},": overlapping price periods\"",[44,1230,214],{"class":181},[44,1232,1233,1235,1237,1239,1241,1243,1245,1248,1251,1254],{"class":46,"line":367},[44,1234,732],{"class":177},[44,1236,1079],{"class":181},[44,1238,274],{"class":54},[44,1240,346],{"class":181},[44,1242,681],{"class":177},[44,1244,698],{"class":201},[44,1246,1247],{"class":181},").sum() ",[44,1249,1250],{"class":177},"!=",[44,1252,1253],{"class":201}," 1",[44,1255,355],{"class":181},[44,1257,1258,1260,1262,1264,1266,1268,1270,1272,1274,1277,1280,1282,1284,1286,1289,1291,1294],{"class":46,"line":385},[44,1259,1099],{"class":181},[44,1261,754],{"class":177},[44,1263,757],{"class":54},[44,1265,397],{"class":396},[44,1267,762],{"class":181},[44,1269,421],{"class":396},[44,1271,867],{"class":54},[44,1273,397],{"class":396},[44,1275,1276],{"class":181},"(g[",[44,1278,1279],{"class":54},"'To'",[44,1281,346],{"class":181},[44,1283,681],{"class":177},[44,1285,698],{"class":201},[44,1287,1288],{"class":181},").sum()",[44,1290,421],{"class":396},[44,1292,1293],{"class":54}," open-ended prices\"",[44,1295,214],{"class":181},[44,1297,1298,1300],{"class":46,"line":427},[44,1299,430],{"class":177},[44,1301,1302],{"class":181}," issues\n",[44,1304,1305],{"class":46,"line":436},[44,1306,195],{"emptyLinePlaceholder":194},[44,1308,1309,1311,1313,1316,1319,1322,1325],{"class":46,"line":441},[44,1310,501],{"class":201},[44,1312,751],{"class":181},[44,1314,1315],{"class":54},"\"issues:\"",[44,1317,1318],{"class":181},", check_periods(prices) ",[44,1320,1321],{"class":177},"or",[44,1323,1324],{"class":54}," \"none\"",[44,1326,214],{"class":181},[10,1328,1329],{},"After the changes, the whole list is validated, not just the rows that were touched. For every product the periods, sorted by start date, must chain exactly: each period starts the day after the previous one ends, so there are no gaps — days with no price — and no overlaps — days with two. Every product must also have exactly one open-ended row, its current price. Running the same check on the original list before changes is worth doing once, because hand-maintained lists often have problems that predate any automation; this one passes before and after. If it had not, the issue list names the product and the kind of problem, which is enough to find the faulty row in a list of thousands.",[70,1331,79,1337,79,1340,79,1343,79,1348,79,1353,79,1357,79,1363,79,1367,79,1371,79,1375,79,1379,79,1383,79,1387,79,1391,79,1395,79,1398,79,1402,79,1404,79,1406,79,1408,79,1410,79,1412,79,1415,79,1418,79,1422,79,1424,79,1427,79,1429,79,1432,79,1434,79,1437,79,1439,79,1442,79,1445,79,1449,79,1451,79,1453,79,1455,79,1458,79,1460,79,1463,79,1465,79,1468,79,1471,79,1474,79,1476,79,1479,79,1481,79,1484,79,1486,79,1489,79,1491],{"viewBox":1332,"role":73,"ariaLabelledBy":1333,"xmlns":77,"style":1336},"181 0 398 168",[1334,1335],"pl-widget-b-t","pl-widget-b-d","width:100%;max-width:398px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[81,1338,1339],{"id":1334},"WID-B before and after the changes",[85,1341,1342],{"id":1335},"Before, WID-B had one open price of 8.40 from January 2025. After, that price ends on 31 October 2026, 8.90 runs through November and 9.10 applies from 1 December 2026 with an open end date.",[89,1344],{"x":1345,"y":92,"width":1346,"height":1347,"fill":95},"181","398","168",[89,1349],{"x":1350,"y":99,"width":1351,"height":1352,"rx":92,"fill":585,"stroke":586,"style":118},"197.1","32.0","24.0",[89,1354],{"x":1355,"y":99,"width":1356,"height":1352,"rx":92,"fill":585,"stroke":586,"style":118},"229.1","72.8",[107,1358,1362],{"x":1359,"y":1360,"style":1361},"265.5","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[89,1364],{"x":1365,"y":99,"width":1366,"height":1352,"rx":92,"fill":585,"stroke":586,"style":118},"301.9","101.3",[107,1368,1370],{"x":1369,"y":1360,"style":1361},"352.6","B",[89,1372],{"x":1373,"y":99,"width":1374,"height":1352,"rx":92,"fill":585,"stroke":586,"style":118},"403.2","104.0",[107,1376,1378],{"x":1377,"y":1360,"style":1361},"455.3","C",[89,1380],{"x":1381,"y":99,"width":1382,"height":1352,"rx":92,"fill":585,"stroke":586,"style":118},"507.3","55.6",[107,1384,1386],{"x":1385,"y":1360,"style":1361},"535.1","D",[89,1388],{"x":1350,"y":1389,"width":1351,"height":1390,"rx":92,"fill":585,"stroke":586,"style":118},"40.0","28.0",[107,1392,462],{"x":1393,"y":1394,"style":1361},"213.1","59.0",[89,1396],{"x":1355,"y":1389,"width":1356,"height":1390,"rx":92,"fill":606,"stroke":576,"style":1397},"stroke-width:1.5px",[107,1399,1401],{"x":1359,"y":1394,"style":1400},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Product",[89,1403],{"x":1365,"y":1389,"width":1366,"height":1390,"rx":92,"fill":606,"stroke":576,"style":1397},[107,1405,976],{"x":1369,"y":1394,"style":1400},[89,1407],{"x":1373,"y":1389,"width":1374,"height":1390,"rx":92,"fill":606,"stroke":576,"style":1397},[107,1409,67],{"x":1377,"y":1394,"style":1400},[89,1411],{"x":1381,"y":1389,"width":1382,"height":1390,"rx":92,"fill":606,"stroke":576,"style":1397},[107,1413,1414],{"x":1385,"y":1394,"style":1400},"Price",[89,1416],{"x":1350,"y":1417,"width":1351,"height":1390,"rx":92,"fill":585,"stroke":586,"style":118},"68.0",[107,1419,1421],{"x":1393,"y":1420,"style":1361},"87.0","2",[89,1423],{"x":1355,"y":1417,"width":1356,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1425,1426],{"x":1359,"y":1420,"style":122},"WID-B",[89,1428],{"x":1365,"y":1417,"width":1366,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1430,1431],{"x":1369,"y":1420,"style":122},"01 Jan 2025",[89,1433],{"x":1373,"y":1417,"width":1374,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1435,1436],{"x":1377,"y":1420,"style":122},"31 Oct 2026",[89,1438],{"x":1381,"y":1417,"width":1382,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1440,1441],{"x":1385,"y":1420,"style":122},"8.40",[89,1443],{"x":1350,"y":1444,"width":1351,"height":1390,"rx":92,"fill":585,"stroke":586,"style":118},"96.0",[107,1446,1448],{"x":1393,"y":1447,"style":1361},"115.0","3",[89,1450],{"x":1355,"y":1444,"width":1356,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1452,1426],{"x":1359,"y":1447,"style":122},[89,1454],{"x":1365,"y":1444,"width":1366,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1456,1457],{"x":1369,"y":1447,"style":122},"01 Nov 2026",[89,1459],{"x":1373,"y":1444,"width":1374,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1461,1462],{"x":1377,"y":1447,"style":122},"30 Nov 2026",[89,1464],{"x":1381,"y":1444,"width":1382,"height":1390,"rx":92,"fill":95,"stroke":586,"style":118},[107,1466,1467],{"x":1385,"y":1447,"style":122},"8.90",[89,1469],{"x":1350,"y":1470,"width":1351,"height":1390,"rx":92,"fill":585,"stroke":586,"style":118},"124.0",[107,1472,91],{"x":1393,"y":1473,"style":1361},"143.0",[89,1475],{"x":1355,"y":1470,"width":1356,"height":1390,"rx":92,"fill":139,"stroke":140,"style":1397},[107,1477,1426],{"x":1359,"y":1473,"style":1478},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[89,1480],{"x":1365,"y":1470,"width":1366,"height":1390,"rx":92,"fill":139,"stroke":140,"style":1397},[107,1482,1483],{"x":1369,"y":1473,"style":1478},"01 Dec 2026",[89,1485],{"x":1373,"y":1470,"width":1374,"height":1390,"rx":92,"fill":139,"stroke":140,"style":1397},[107,1487,1488],{"x":1377,"y":1473,"style":1478},"31 Dec 9999",[89,1490],{"x":1381,"y":1470,"width":1382,"height":1390,"rx":92,"fill":139,"stroke":140,"style":1397},[107,1492,1493],{"x":1385,"y":1473,"style":1478},"9.10",[29,1495,1497],{"id":1496},"step-4-prices-on-any-date-and-the-change-report","Step 4 — Prices on any date, and the change report",[34,1499,1501],{"className":168,"code":1500,"language":170,"meta":39,"style":39},"def price_on(prices, product, day):\n    day = pd.Timestamp(day)\n    hit = prices[(prices[\"Product\"] == product) & (prices[\"From\"] \u003C= day) & (prices[\"To\"] >= day)]\n    return hit[\"Price\"].iloc[0] if len(hit) else None\n\nfor day in [\"2026-10-15\", \"2026-11-15\", \"2026-12-15\", \"2027-01-15\"]:\n    print(day, {p: price_on(prices, p, day) for p in [\"WID-A\", \"WID-B\", \"GAD-X\", \"NEW-9\"]})\n\nreport = accepted.assign(**{\"Old price\": [price_on(prices, p, d - pd.Timedelta(days=1)) for p, d in zip(accepted[\"Product\"], accepted[\"Effective from\"])]})\nreport[\"Change %\"] = ((report[\"New price\"] \u002F report[\"Old price\"] - 1) * 100).round(1)\nwith pd.ExcelWriter(\"price_list_updated.xlsx\") as xw:\n    prices.to_excel(xw, sheet_name=\"Price list\", index=False)\n    report.drop(columns=\"Problem\").to_excel(xw, sheet_name=\"Changes applied\", index=False)\n    rejected.to_excel(xw, sheet_name=\"Changes rejected\", index=False)\nprint(report[[\"Product\", \"Effective from\", \"Old price\", \"New price\", \"Change %\"]].to_string(index=False))\n",[41,1502,1503,1513,1523,1568,1597,1601,1634,1672,1676,1733,1782,1800,1823,1855,1877],{"__ignoreMap":39},[44,1504,1505,1507,1510],{"class":46,"line":47},[44,1506,324],{"class":177},[44,1508,1509],{"class":327}," price_on",[44,1511,1512],{"class":181},"(prices, product, day):\n",[44,1514,1515,1518,1520],{"class":46,"line":191},[44,1516,1517],{"class":181},"    day ",[44,1519,248],{"class":177},[44,1521,1522],{"class":181}," pd.Timestamp(day)\n",[44,1524,1525,1528,1530,1533,1535,1537,1539,1541,1543,1545,1547,1549,1551,1554,1556,1558,1560,1562,1565],{"class":46,"line":198},[44,1526,1527],{"class":181},"    hit ",[44,1529,248],{"class":177},[44,1531,1532],{"class":181}," prices[(prices[",[44,1534,307],{"class":54},[44,1536,346],{"class":181},[44,1538,681],{"class":177},[44,1540,684],{"class":181},[44,1542,687],{"class":177},[44,1544,674],{"class":181},[44,1546,269],{"class":54},[44,1548,346],{"class":181},[44,1550,349],{"class":177},[44,1552,1553],{"class":181}," day) ",[44,1555,687],{"class":177},[44,1557,674],{"class":181},[44,1559,274],{"class":54},[44,1561,346],{"class":181},[44,1563,1564],{"class":177},">=",[44,1566,1567],{"class":181}," day)]\n",[44,1569,1570,1572,1575,1577,1579,1581,1583,1586,1589,1592,1595],{"class":46,"line":217},[44,1571,430],{"class":177},[44,1573,1574],{"class":181}," hit[",[44,1576,874],{"class":54},[44,1578,722],{"class":181},[44,1580,92],{"class":201},[44,1582,346],{"class":181},[44,1584,1585],{"class":177},"if",[44,1587,1588],{"class":201}," len",[44,1590,1591],{"class":181},"(hit) ",[44,1593,1594],{"class":177},"else",[44,1596,433],{"class":201},[44,1598,1599],{"class":46,"line":237},[44,1600,195],{"emptyLinePlaceholder":194},[44,1602,1603,1605,1608,1610,1613,1616,1618,1621,1623,1626,1628,1631],{"class":46,"line":242},[44,1604,912],{"class":177},[44,1606,1607],{"class":181}," day ",[44,1609,918],{"class":177},[44,1611,1612],{"class":181}," [",[44,1614,1615],{"class":54},"\"2026-10-15\"",[44,1617,257],{"class":181},[44,1619,1620],{"class":54},"\"2026-11-15\"",[44,1622,257],{"class":181},[44,1624,1625],{"class":54},"\"2026-12-15\"",[44,1627,257],{"class":181},[44,1629,1630],{"class":54},"\"2027-01-15\"",[44,1632,1633],{"class":181},"]:\n",[44,1635,1636,1639,1642,1644,1647,1649,1651,1654,1656,1659,1661,1664,1666,1669],{"class":46,"line":280},[44,1637,1638],{"class":201},"    print",[44,1640,1641],{"class":181},"(day, {p: price_on(prices, p, day) ",[44,1643,912],{"class":177},[44,1645,1646],{"class":181}," p ",[44,1648,918],{"class":177},[44,1650,1612],{"class":181},[44,1652,1653],{"class":54},"\"WID-A\"",[44,1655,257],{"class":181},[44,1657,1658],{"class":54},"\"WID-B\"",[44,1660,257],{"class":181},[44,1662,1663],{"class":54},"\"GAD-X\"",[44,1665,257],{"class":181},[44,1667,1668],{"class":54},"\"NEW-9\"",[44,1670,1671],{"class":181},"]})\n",[44,1673,1674],{"class":46,"line":316},[44,1675,195],{"emptyLinePlaceholder":194},[44,1677,1678,1681,1683,1686,1689,1691,1694,1697,1699,1701,1703,1705,1707,1710,1712,1715,1717,1720,1723,1725,1728,1730],{"class":46,"line":321},[44,1679,1680],{"class":181},"report ",[44,1682,248],{"class":177},[44,1684,1685],{"class":181}," accepted.assign(",[44,1687,1688],{"class":177},"**",[44,1690,397],{"class":181},[44,1692,1693],{"class":54},"\"Old price\"",[44,1695,1696],{"class":181},": [price_on(prices, p, d ",[44,1698,830],{"class":177},[44,1700,833],{"class":181},[44,1702,836],{"class":260},[44,1704,248],{"class":177},[44,1706,462],{"class":201},[44,1708,1709],{"class":181},")) ",[44,1711,912],{"class":177},[44,1713,1714],{"class":181}," p, d ",[44,1716,918],{"class":177},[44,1718,1719],{"class":201}," zip",[44,1721,1722],{"class":181},"(accepted[",[44,1724,307],{"class":54},[44,1726,1727],{"class":181},"], accepted[",[44,1729,301],{"class":54},[44,1731,1732],{"class":181},"])]})\n",[44,1734,1735,1738,1741,1743,1745,1748,1750,1752,1755,1758,1760,1762,1764,1766,1769,1772,1775,1778,1780],{"class":46,"line":334},[44,1736,1737],{"class":181},"report[",[44,1739,1740],{"class":54},"\"Change %\"",[44,1742,346],{"class":181},[44,1744,248],{"class":177},[44,1746,1747],{"class":181}," ((report[",[44,1749,343],{"class":54},[44,1751,346],{"class":181},[44,1753,1754],{"class":177},"\u002F",[44,1756,1757],{"class":181}," report[",[44,1759,1693],{"class":54},[44,1761,346],{"class":181},[44,1763,830],{"class":177},[44,1765,1253],{"class":201},[44,1767,1768],{"class":181},") ",[44,1770,1771],{"class":177},"*",[44,1773,1774],{"class":201}," 100",[44,1776,1777],{"class":181},").round(",[44,1779,462],{"class":201},[44,1781,214],{"class":181},[44,1783,1784,1787,1790,1793,1795,1797],{"class":46,"line":358},[44,1785,1786],{"class":177},"with",[44,1788,1789],{"class":181}," pd.ExcelWriter(",[44,1791,1792],{"class":54},"\"price_list_updated.xlsx\"",[44,1794,1768],{"class":181},[44,1796,185],{"class":177},[44,1798,1799],{"class":181}," xw:\n",[44,1801,1802,1805,1808,1810,1813,1815,1817,1819,1821],{"class":46,"line":367},[44,1803,1804],{"class":181},"    prices.to_excel(xw, ",[44,1806,1807],{"class":260},"sheet_name",[44,1809,248],{"class":177},[44,1811,1812],{"class":54},"\"Price list\"",[44,1814,257],{"class":181},[44,1816,524],{"class":260},[44,1818,248],{"class":177},[44,1820,529],{"class":201},[44,1822,214],{"class":181},[44,1824,1825,1828,1831,1833,1835,1838,1840,1842,1845,1847,1849,1851,1853],{"class":46,"line":385},[44,1826,1827],{"class":181},"    report.drop(",[44,1829,1830],{"class":260},"columns",[44,1832,248],{"class":177},[44,1834,447],{"class":54},[44,1836,1837],{"class":181},").to_excel(xw, ",[44,1839,1807],{"class":260},[44,1841,248],{"class":177},[44,1843,1844],{"class":54},"\"Changes applied\"",[44,1846,257],{"class":181},[44,1848,524],{"class":260},[44,1850,248],{"class":177},[44,1852,529],{"class":201},[44,1854,214],{"class":181},[44,1856,1857,1860,1862,1864,1867,1869,1871,1873,1875],{"class":46,"line":427},[44,1858,1859],{"class":181},"    rejected.to_excel(xw, ",[44,1861,1807],{"class":260},[44,1863,248],{"class":177},[44,1865,1866],{"class":54},"\"Changes rejected\"",[44,1868,257],{"class":181},[44,1870,524],{"class":260},[44,1872,248],{"class":177},[44,1874,529],{"class":201},[44,1876,214],{"class":181},[44,1878,1879,1881,1884,1886,1888,1890,1892,1894,1896,1898,1900,1902,1904,1906,1908,1910],{"class":46,"line":436},[44,1880,501],{"class":201},[44,1882,1883],{"class":181},"(report[[",[44,1885,307],{"class":54},[44,1887,257],{"class":181},[44,1889,301],{"class":54},[44,1891,257],{"class":181},[44,1893,1693],{"class":54},[44,1895,257],{"class":181},[44,1897,343],{"class":54},[44,1899,257],{"class":181},[44,1901,1740],{"class":54},[44,1903,521],{"class":181},[44,1905,524],{"class":260},[44,1907,248],{"class":177},[44,1909,529],{"class":201},[44,1911,532],{"class":181},[10,1913,1914,1915,27],{},"With history kept, the price of any product on any date is a filter for the row whose period contains the date — the lookup an order or invoice system makes, and the one used to re-price past orders correctly. Asking for four dates shows the changes taking effect: the widget at 12.60 until the end of October and 13.10 after, the second widget stepping from 8.40 to 8.90 to 9.10, the gadget unchanged until its January increase. The change report lists every accepted change with the price it replaces — read from the updated list the day before the change, so the December change to the second widget is shown against November's 8.90 rather than the original 8.40 — and the percentage increase, which is the summary sales and customer service need before the new prices go live, and the rejected changes go back to whoever submitted them. For joining whole tables of orders to dated prices, see ",[23,1916,1918],{"href":1917},"\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",[29,1920,1922],{"id":1921},"common-pitfalls","Common pitfalls",[1924,1925,1926,1934,1940,1946,1952],"ul",{},[1927,1928,1929,1933],"li",{},[1930,1931,1932],"strong",{},"Adding a new price without closing the old one."," The product then has two prices; close the current row first.",[1927,1935,1936,1939],{},[1930,1937,1938],{},"Backdated changes."," Reject changes inside invoiced periods; correct the past with credit notes.",[1927,1941,1942,1945],{},[1930,1943,1944],{},"Applying changes out of order."," Sort by product and date before applying.",[1927,1947,1948,1951],{},[1930,1949,1950],{},"Blank end dates."," Use a far-future date such as 9999-12-31 for the current price.",[1927,1953,1954,1957],{},[1930,1955,1956],{},"Checking only the edited rows."," Validate the whole list for gaps, overlaps and open rows.",[29,1959,1961],{"id":1960},"conclusion","Conclusion",[10,1963,1964],{},"To update a price list that keeps history, check each change — positive price, not backdated into a closed period — then, in date order, close the product's current row the day before the change takes effect and add a new open-ended row. Validate the whole list so every product's periods chain without gaps or overlaps and exactly one is open, look prices up by finding the period that contains a date, and report each change with the price it replaces.",[29,1966,1968],{"id":1967},"frequently-asked-questions","Frequently asked questions",[10,1970,1971,1974,1975,1978,1979,1982,1983,1985],{},[1930,1972,1973],{},"How do I store prices with effective dates?","\nOne row per product and period with ",[14,1976,1977],{},"from"," and ",[14,1980,1981],{},"to"," dates, using a far-future ",[14,1984,1981],{}," date such as 9999-12-31 for the current price.",[10,1987,1988,1991],{},[1930,1989,1990],{},"How do I add a new price without losing the old one?","\nSet the current row's end date to the day before the new price's effective date and add a new row from that date.",[10,1993,1994,1997],{},[1930,1995,1996],{},"How do I check a dated price list for overlaps?","\nSort each product's rows by start date and check that each period starts exactly one day after the previous one ends.",[10,1999,2000,2003,2004,1978,2006,27],{},[1930,2001,2002],{},"How do I find a product's price on a given date?","\nSelect the row where the product matches and the date lies between ",[14,2005,1977],{},[14,2007,1981],{},[29,2009,2011],{"id":2010},"related","Related",[1924,2013,2014,2020,2025,2032],{},[1927,2015,2016,2017,2019],{},"Up one level: ",[23,2018,26],{"href":25}," — finance and operations reports.",[1927,2021,2022,2024],{},[23,2023,1918],{"href":1917}," — pricing many orders at once.",[1927,2026,2027,2031],{},[23,2028,2030],{"href":2029},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fmodel-the-impact-of-a-price-increase\u002F","Model the Impact of a Price Increase"," — deciding the new prices.",[1927,2033,2034,2038],{},[23,2035,2037],{"href":2036},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fcheck-supplier-invoices-against-contract-prices\u002F","Check Supplier Invoices Against Contract Prices"," — dated prices on the buying side.",[2040,2041,2042],"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 .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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":39,"searchDepth":191,"depth":191,"links":2044},[2045,2046,2047,2048,2049,2050,2051,2052,2053],{"id":31,"depth":191,"text":32},{"id":164,"depth":191,"text":165},{"id":638,"depth":191,"text":639},{"id":1015,"depth":191,"text":1016},{"id":1496,"depth":191,"text":1497},{"id":1921,"depth":191,"text":1922},{"id":1960,"depth":191,"text":1961},{"id":1967,"depth":191,"text":1968},{"id":2010,"depth":191,"text":2011},"2026-10-02","Apply a sheet of price changes to a price list that keeps history: close each current price the day before its replacement takes effect, add the new row, reject changes that are backdated into a closed period or that would overlap, check that every product has exactly one price on every day, look up any product's price on any date, and produce a change report showing old and new prices side by side.","md",[2058,2060,2062,2064],{"q":1973,"a":2059},"One row per product and period with *from* and *to* dates, using a far-future *to* date such as 9999-12-31 for the current price.",{"q":1990,"a":2061},"Set the current row's end date to the day before the new price's effective date and add a new row from that date.",{"q":1996,"a":2063},"Sort each product's rows by start date and check that each period starts exactly one day after the previous one ends.",{"q":2002,"a":2065},"Select the row where the product matches and the date lies between *from* and *to*.","price list effective dates python",null,{"breadcrumb":2069},[2070,2072,2075],{"name":2071,"item":1754},"Home",{"name":2073,"item":2074},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2076,"item":25},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates",{"title":2079,"description":2080},"Update a Price List with Effective Dates in Python","Maintain a dated price list with pandas: apply price changes by closing current rows and adding new ones, reject backdated or overlapping changes, check for gaps and overlaps, look up prices as of a date and report the changes.","update-a-price-list-with-effective-dates","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fupdate-a-price-list-with-effective-dates\u002Findex",[2084,2085],"report-layouts","joins-and-lookups","how-to","GynLgGLMO4jNaMYB8O65Y8lkSx9wPFPd-h72kAbsj8o",[2089,2093],{"title":2090,"path":2091,"stem":2092,"children":-1},"Send Out Budget Templates and Consolidate the Returns","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fsend-out-budget-templates-and-consolidate-the-returns\u002Findex",{"title":2094,"path":2095,"stem":2096,"children":-1},"Building Multi-Sheet Excel Dashboards","\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","automating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Findex",[2098,2100],{"path":2099,"title":2076},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2101,"title":2073},"\u002Fautomating-reporting-workflows",[],[2104,2106,2109,2112,2115,2118],{"path":2095,"title":2094,"kind":2105},"guide",{"path":2107,"title":2108,"kind":2086},"\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":2110,"title":2111,"kind":2086},"\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":2113,"title":2114,"kind":2086},"\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":2116,"title":2117,"kind":2086},"\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":2119,"title":2120,"kind":2086},"\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",1790972128140]