[{"data":1,"prerenderedAt":3918},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger":3883,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger":3892,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger":3897,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger":3898},{"id":4,"title":5,"body":6,"dateModified":3849,"datePublished":3849,"description":3850,"extension":3851,"faq":3852,"keyword":3861,"kind":3862,"meta":3863,"navigation":289,"path":3872,"seo":3873,"slug":3876,"stem":3877,"topics":3878,"type":3881,"__hash__":3882},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger\u002Findex.md","Build a Profit and Loss Statement from a General Ledger",{"type":7,"value":8,"toc":3836},"minimark",[9,19,24,56,70,254,258,261,587,590,594,601,939,947,951,959,1152,1299,1303,1306,1768,1895,1898,1985,1989,1996,3421,3433,3437,3440,3473,3527,3707,3711,3756,3760,3766,3770,3776,3782,3788,3794,3798,3832],[10,11,12,13,18],"p",{},"Every accounting system can print a profit and loss statement, but the one management wants rarely matches it: different line groupings, months side by side, margins, a layout that fits the board pack. So someone exports the general ledger to Excel each month and rebuilds the statement with SUMIFS. That rebuild is exactly the kind of work Python does well: read the journal lines, check they balance, map every account to a line, and lay out the statement the same way every month. This page is part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50,53],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl",[35,54,55],{"class":45}," xlsxwriter\n",[10,57,58,61,62,65,66,69],{},[32,59,60],{},"gl_q3.xlsx"," holds the export for July to September 2026: a ",[32,63,64],{},"GL"," sheet with one row per journal line (journal number, date, account, debit, credit) and an ",[32,67,68],{},"Accounts"," sheet with the chart of accounts. Accounts follow the common numbering: 1000–3999 are balance sheet accounts, 4000s revenue, 5000s cost of sales, 6000s operating expenses, 7000s other income and expense, 8000s tax.",[71,72,80,81,80,85,80,89,80,97,80,107,80,118,80,125,80,130,80,134,80,140,80,145,80,150,80,157,80,162,80,166,80,169,80,173,80,176,80,180,80,187,80,192,80,195,80,198,80,202,80,205,80,208,80,214,80,218,80,221,80,224,80,228,80,231,80,236,80,243,80,248,80,251],"svg",{"viewBox":73,"role":74,"ariaLabelledBy":75,"xmlns":78,"style":79},"4 0 752 142","img",[76,77],"pl-flow-t","pl-flow-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  ",[82,83,84],"title",{"id":76},"From journal lines to a statement",[86,87,88],"desc",{"id":77},"Journal lines are checked for balance, joined to the chart of accounts, mapped to P&L lines by account range, signed so that revenue is positive and costs are positive, summed by month, and laid out with subtotals and margins.",[90,91],"rect",{"x":92,"y":93,"width":94,"height":95,"fill":96},"4","0","752","142","#ffffff",[90,98],{"x":99,"y":100,"width":101,"height":102,"rx":103,"fill":104,"stroke":105,"style":106},"20.0","16.0","115.2","110.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[108,109,111],"g",{"transform":110},"translate(65.6,29.0) scale(1.000)",[112,113],"path",{"d":114,"fill":115,"stroke":116,"style":117},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[119,120,124],"text",{"x":121,"y":122,"style":123},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Journal lines",[119,126,129],{"x":121,"y":127,"style":128},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","debits and",[119,131,133],{"x":121,"y":132,"style":128},"112.5","credits",[37,135],{"x1":136,"y1":137,"x2":138,"y2":137,"stroke":139,"style":106},"140.2","71.2","162.2","var(--brand,#5b5cf0)",[141,142],"polygon",{"points":143,"fill":144},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[90,146],{"x":147,"y":100,"width":101,"height":102,"rx":103,"fill":148,"stroke":149,"style":106},"171.2","#d9f4f1","var(--teal,#0f9488)",[108,151,153],{"transform":152},"translate(216.8,29.0) scale(1.000)",[112,154],{"d":155,"fill":115,"stroke":156,"style":117},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[119,158,161],{"x":159,"y":122,"style":160},"228.8","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Balance",[119,163,165],{"x":159,"y":164,"style":160},"93.0","check",[119,167,168],{"x":159,"y":132,"style":128},"debits = credits",[37,170],{"x1":171,"y1":137,"x2":172,"y2":137,"stroke":139,"style":106},"291.4","313.4",[141,174],{"points":175,"fill":144},"317.4,71.2 308.4,76.2 308.4,66.2",[90,177],{"x":178,"y":100,"width":101,"height":102,"rx":103,"fill":179,"stroke":139,"style":106},"322.4","#f0f4ff",[108,181,183],{"transform":182},"translate(368.0,29.0) scale(1.000)",[112,184],{"d":185,"fill":115,"stroke":186,"style":117},"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1 M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1","var(--brand-strong,#4338ca)",[119,188,191],{"x":189,"y":122,"style":190},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Map",[119,193,194],{"x":189,"y":164,"style":190},"accounts",[119,196,197],{"x":189,"y":132,"style":128},"range to line",[37,199],{"x1":200,"y1":137,"x2":201,"y2":137,"stroke":139,"style":106},"442.6","464.6",[141,203],{"points":204,"fill":144},"468.6,71.2 459.6,76.2 459.6,66.2",[90,206],{"x":207,"y":100,"width":101,"height":102,"rx":103,"fill":179,"stroke":139,"style":106},"473.6",[108,209,211],{"transform":210},"translate(519.2,29.0) scale(1.000)",[112,212],{"d":213,"fill":115,"stroke":186,"style":117},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[119,215,217],{"x":216,"y":122,"style":190},"531.2","Sign and",[119,219,220],{"x":216,"y":164,"style":190},"sum",[119,222,223],{"x":216,"y":132,"style":128},"by month",[37,225],{"x1":226,"y1":137,"x2":227,"y2":137,"stroke":139,"style":106},"593.8","615.8",[141,229],{"points":230,"fill":144},"619.8,71.2 610.8,76.2 610.8,66.2",[90,232],{"x":233,"y":100,"width":101,"height":102,"rx":103,"fill":234,"stroke":235,"style":106},"624.8","#fdefd8","var(--gold,#b4740a)",[108,237,239],{"transform":238},"translate(670.4,29.0) scale(1.000)",[112,240],{"d":241,"fill":115,"stroke":242,"style":117},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--gold-ink,#7a4e06)",[119,244,247],{"x":245,"y":122,"style":246},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Statement",[119,249,250],{"x":245,"y":127,"style":128},"subtotals and",[119,252,253],{"x":245,"y":132,"style":128},"margins",[20,255,257],{"id":256},"step-1-check-the-ledger-balances","Step 1 — Check the ledger balances",[10,259,260],{},"A statement built on an unbalanced or partial export is wrong before it starts. Check that debits equal credits overall and for every journal, and that every account in the ledger exists in the chart of accounts:",[25,262,266],{"className":263,"code":264,"language":265,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ngl = pd.read_excel(\"gl_q3.xlsx\", sheet_name=\"GL\", parse_dates=[\"Date\"])\ncoa = pd.read_excel(\"gl_q3.xlsx\", sheet_name=\"Accounts\")\n\ntotal = (gl[\"Debit\"] - gl[\"Credit\"]).sum().round(2)\nby_journal = gl.groupby(\"Journal\")[[\"Debit\", \"Credit\"]].sum().round(2)\nunbalanced = by_journal[by_journal[\"Debit\"] != by_journal[\"Credit\"]]\nunknown = set(gl[\"Account\"]) - set(coa[\"Account\"])\nprint(f\"{len(gl)} lines, {gl['Journal'].nunique()} journals; debits - credits = {total}\")\nprint(f\"unbalanced journals: {len(unbalanced)}; accounts not in the chart: {sorted(unknown) or 'none'}\")\n","python",[32,267,268,284,291,334,358,363,398,428,454,485,543],{"__ignoreMap":30},[35,269,270,274,278,281],{"class":37,"line":38},[35,271,273],{"class":272},"s-kum","import",[35,275,277],{"class":276},"skGVy"," pandas ",[35,279,280],{"class":272},"as",[35,282,283],{"class":276}," pd\n",[35,285,287],{"class":37,"line":286},2,[35,288,290],{"emptyLinePlaceholder":289},true,"\n",[35,292,294,297,300,303,306,309,313,315,318,320,323,325,328,331],{"class":37,"line":293},3,[35,295,296],{"class":276},"gl ",[35,298,299],{"class":272},"=",[35,301,302],{"class":276}," pd.read_excel(",[35,304,305],{"class":45},"\"gl_q3.xlsx\"",[35,307,308],{"class":276},", ",[35,310,312],{"class":311},"sa561","sheet_name",[35,314,299],{"class":272},[35,316,317],{"class":45},"\"GL\"",[35,319,308],{"class":276},[35,321,322],{"class":311},"parse_dates",[35,324,299],{"class":272},[35,326,327],{"class":276},"[",[35,329,330],{"class":45},"\"Date\"",[35,332,333],{"class":276},"])\n",[35,335,337,340,342,344,346,348,350,352,355],{"class":37,"line":336},4,[35,338,339],{"class":276},"coa ",[35,341,299],{"class":272},[35,343,302],{"class":276},[35,345,305],{"class":45},[35,347,308],{"class":276},[35,349,312],{"class":311},[35,351,299],{"class":272},[35,353,354],{"class":45},"\"Accounts\"",[35,356,357],{"class":276},")\n",[35,359,361],{"class":37,"line":360},5,[35,362,290],{"emptyLinePlaceholder":289},[35,364,366,369,371,374,377,380,383,386,389,392,396],{"class":37,"line":365},6,[35,367,368],{"class":276},"total ",[35,370,299],{"class":272},[35,372,373],{"class":276}," (gl[",[35,375,376],{"class":45},"\"Debit\"",[35,378,379],{"class":276},"] ",[35,381,382],{"class":272},"-",[35,384,385],{"class":276}," gl[",[35,387,388],{"class":45},"\"Credit\"",[35,390,391],{"class":276},"]).sum().round(",[35,393,395],{"class":394},"sP0c6","2",[35,397,357],{"class":276},[35,399,401,404,406,409,412,415,417,419,421,424,426],{"class":37,"line":400},7,[35,402,403],{"class":276},"by_journal ",[35,405,299],{"class":272},[35,407,408],{"class":276}," gl.groupby(",[35,410,411],{"class":45},"\"Journal\"",[35,413,414],{"class":276},")[[",[35,416,376],{"class":45},[35,418,308],{"class":276},[35,420,388],{"class":45},[35,422,423],{"class":276},"]].sum().round(",[35,425,395],{"class":394},[35,427,357],{"class":276},[35,429,431,434,436,439,441,443,446,449,451],{"class":37,"line":430},8,[35,432,433],{"class":276},"unbalanced ",[35,435,299],{"class":272},[35,437,438],{"class":276}," by_journal[by_journal[",[35,440,376],{"class":45},[35,442,379],{"class":276},[35,444,445],{"class":272},"!=",[35,447,448],{"class":276}," by_journal[",[35,450,388],{"class":45},[35,452,453],{"class":276},"]]\n",[35,455,457,460,462,465,468,471,474,476,478,481,483],{"class":37,"line":456},9,[35,458,459],{"class":276},"unknown ",[35,461,299],{"class":272},[35,463,464],{"class":394}," set",[35,466,467],{"class":276},"(gl[",[35,469,470],{"class":45},"\"Account\"",[35,472,473],{"class":276},"]) ",[35,475,382],{"class":272},[35,477,464],{"class":394},[35,479,480],{"class":276},"(coa[",[35,482,470],{"class":45},[35,484,333],{"class":276},[35,486,488,491,494,497,500,504,507,510,513,516,518,521,524,527,529,532,534,537,539,541],{"class":37,"line":487},10,[35,489,490],{"class":394},"print",[35,492,493],{"class":276},"(",[35,495,496],{"class":272},"f",[35,498,499],{"class":45},"\"",[35,501,503],{"class":502},"sSjpA","{",[35,505,506],{"class":394},"len",[35,508,509],{"class":276},"(gl)",[35,511,512],{"class":502},"}",[35,514,515],{"class":45}," lines, ",[35,517,503],{"class":502},[35,519,520],{"class":276},"gl[",[35,522,523],{"class":45},"'Journal'",[35,525,526],{"class":276},"].nunique()",[35,528,512],{"class":502},[35,530,531],{"class":45}," journals; debits - credits = ",[35,533,503],{"class":502},[35,535,536],{"class":276},"total",[35,538,512],{"class":502},[35,540,499],{"class":45},[35,542,357],{"class":276},[35,544,546,548,550,552,555,557,559,562,564,567,569,572,575,578,581,583,585],{"class":37,"line":545},11,[35,547,490],{"class":394},[35,549,493],{"class":276},[35,551,496],{"class":272},[35,553,554],{"class":45},"\"unbalanced journals: ",[35,556,503],{"class":502},[35,558,506],{"class":394},[35,560,561],{"class":276},"(unbalanced)",[35,563,512],{"class":502},[35,565,566],{"class":45},"; accounts not in the chart: ",[35,568,503],{"class":502},[35,570,571],{"class":394},"sorted",[35,573,574],{"class":276},"(unknown) ",[35,576,577],{"class":272},"or",[35,579,580],{"class":45}," 'none'",[35,582,512],{"class":502},[35,584,499],{"class":45},[35,586,357],{"class":276},[10,588,589],{},"Stop the script if either check fails. An unknown account usually means someone opened a new account; it needs a line in the mapping before the numbers can be trusted, not a silent exclusion.",[20,591,593],{"id":592},"step-2-map-accounts-to-statement-lines","Step 2 — Map accounts to statement lines",[10,595,596,597,600],{},"The mapping is the heart of the report and should be data, not code: a table of account ranges and the line each belongs to. ",[32,598,599],{},"pd.cut"," assigns every account to its range in one call:",[25,602,604],{"className":263,"code":603,"language":265,"meta":30,"style":30},"bounds = [4000, 4100, 4200, 5000, 6000, 6100, 6300, 7000, 7100, 7200, 8000, 9000]\nlines = [\"Revenue\", \"Discounts\", None, \"Cost of sales\", \"People\", \"Premises and software\",\n         \"Marketing\", \"Other income\", \"Other expense\", None, \"Tax\"]\nline_map = pd.DataFrame({\"From\": bounds[:-1], \"To\": bounds[1:], \"Line\": lines}).dropna()\nprint(line_map.to_string(index=False))\n\npl = gl[gl[\"Account\"].between(4000, 8999)].merge(coa, on=\"Account\", how=\"left\")\npl[\"Line\"] = pd.cut(pl[\"Account\"], bins=bounds, right=False, labels=False).map(dict(enumerate(lines)))\nprint(\"unmapped P&L accounts:\", sorted(pl.loc[pl[\"Line\"].isna(), \"Account\"].unique()) or \"none\")\n",[32,605,606,677,717,743,784,802,806,850,906],{"__ignoreMap":30},[35,607,608,611,613,616,619,621,624,626,629,631,634,636,639,641,644,646,649,651,654,656,659,661,664,666,669,671,674],{"class":37,"line":38},[35,609,610],{"class":276},"bounds ",[35,612,299],{"class":272},[35,614,615],{"class":276}," [",[35,617,618],{"class":394},"4000",[35,620,308],{"class":276},[35,622,623],{"class":394},"4100",[35,625,308],{"class":276},[35,627,628],{"class":394},"4200",[35,630,308],{"class":276},[35,632,633],{"class":394},"5000",[35,635,308],{"class":276},[35,637,638],{"class":394},"6000",[35,640,308],{"class":276},[35,642,643],{"class":394},"6100",[35,645,308],{"class":276},[35,647,648],{"class":394},"6300",[35,650,308],{"class":276},[35,652,653],{"class":394},"7000",[35,655,308],{"class":276},[35,657,658],{"class":394},"7100",[35,660,308],{"class":276},[35,662,663],{"class":394},"7200",[35,665,308],{"class":276},[35,667,668],{"class":394},"8000",[35,670,308],{"class":276},[35,672,673],{"class":394},"9000",[35,675,676],{"class":276},"]\n",[35,678,679,682,684,686,689,691,694,696,699,701,704,706,709,711,714],{"class":37,"line":286},[35,680,681],{"class":276},"lines ",[35,683,299],{"class":272},[35,685,615],{"class":276},[35,687,688],{"class":45},"\"Revenue\"",[35,690,308],{"class":276},[35,692,693],{"class":45},"\"Discounts\"",[35,695,308],{"class":276},[35,697,698],{"class":394},"None",[35,700,308],{"class":276},[35,702,703],{"class":45},"\"Cost of sales\"",[35,705,308],{"class":276},[35,707,708],{"class":45},"\"People\"",[35,710,308],{"class":276},[35,712,713],{"class":45},"\"Premises and software\"",[35,715,716],{"class":276},",\n",[35,718,719,722,724,727,729,732,734,736,738,741],{"class":37,"line":293},[35,720,721],{"class":45},"         \"Marketing\"",[35,723,308],{"class":276},[35,725,726],{"class":45},"\"Other income\"",[35,728,308],{"class":276},[35,730,731],{"class":45},"\"Other expense\"",[35,733,308],{"class":276},[35,735,698],{"class":394},[35,737,308],{"class":276},[35,739,740],{"class":45},"\"Tax\"",[35,742,676],{"class":276},[35,744,745,748,750,753,756,759,761,764,767,770,773,775,778,781],{"class":37,"line":336},[35,746,747],{"class":276},"line_map ",[35,749,299],{"class":272},[35,751,752],{"class":276}," pd.DataFrame({",[35,754,755],{"class":45},"\"From\"",[35,757,758],{"class":276},": bounds[:",[35,760,382],{"class":272},[35,762,763],{"class":394},"1",[35,765,766],{"class":276},"], ",[35,768,769],{"class":45},"\"To\"",[35,771,772],{"class":276},": bounds[",[35,774,763],{"class":394},[35,776,777],{"class":276},":], ",[35,779,780],{"class":45},"\"Line\"",[35,782,783],{"class":276},": lines}).dropna()\n",[35,785,786,788,791,794,796,799],{"class":37,"line":360},[35,787,490],{"class":394},[35,789,790],{"class":276},"(line_map.to_string(",[35,792,793],{"class":311},"index",[35,795,299],{"class":272},[35,797,798],{"class":394},"False",[35,800,801],{"class":276},"))\n",[35,803,804],{"class":37,"line":365},[35,805,290],{"emptyLinePlaceholder":289},[35,807,808,811,813,816,818,821,823,825,828,831,834,836,838,840,843,845,848],{"class":37,"line":400},[35,809,810],{"class":276},"pl ",[35,812,299],{"class":272},[35,814,815],{"class":276}," gl[gl[",[35,817,470],{"class":45},[35,819,820],{"class":276},"].between(",[35,822,618],{"class":394},[35,824,308],{"class":276},[35,826,827],{"class":394},"8999",[35,829,830],{"class":276},")].merge(coa, ",[35,832,833],{"class":311},"on",[35,835,299],{"class":272},[35,837,470],{"class":45},[35,839,308],{"class":276},[35,841,842],{"class":311},"how",[35,844,299],{"class":272},[35,846,847],{"class":45},"\"left\"",[35,849,357],{"class":276},[35,851,852,855,857,859,861,864,866,868,871,873,876,879,881,883,885,888,890,892,895,898,900,903],{"class":37,"line":430},[35,853,854],{"class":276},"pl[",[35,856,780],{"class":45},[35,858,379],{"class":276},[35,860,299],{"class":272},[35,862,863],{"class":276}," pd.cut(pl[",[35,865,470],{"class":45},[35,867,766],{"class":276},[35,869,870],{"class":311},"bins",[35,872,299],{"class":272},[35,874,875],{"class":276},"bounds, ",[35,877,878],{"class":311},"right",[35,880,299],{"class":272},[35,882,798],{"class":394},[35,884,308],{"class":276},[35,886,887],{"class":311},"labels",[35,889,299],{"class":272},[35,891,798],{"class":394},[35,893,894],{"class":276},").map(",[35,896,897],{"class":394},"dict",[35,899,493],{"class":276},[35,901,902],{"class":394},"enumerate",[35,904,905],{"class":276},"(lines)))\n",[35,907,908,910,912,915,917,919,922,924,927,929,932,934,937],{"class":37,"line":456},[35,909,490],{"class":394},[35,911,493],{"class":276},[35,913,914],{"class":45},"\"unmapped P&L accounts:\"",[35,916,308],{"class":276},[35,918,571],{"class":394},[35,920,921],{"class":276},"(pl.loc[pl[",[35,923,780],{"class":45},[35,925,926],{"class":276},"].isna(), ",[35,928,470],{"class":45},[35,930,931],{"class":276},"].unique()) ",[35,933,577],{"class":272},[35,935,936],{"class":45}," \"none\"",[35,938,357],{"class":276},[10,940,941,942,946],{},"Keep the range table in its own sheet or file, owned by finance, so a new account is a one-row change. When groupings do not follow number ranges, map account by account with a merge instead; ",[14,943,945],{"href":944},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files\u002F","The VLOOKUP Equivalent in pandas for Excel Files"," shows that pattern.",[20,948,950],{"id":949},"step-3-sign-conventions","Step 3 — Sign conventions",[10,952,953,954,958],{},"In a ledger, revenue is a credit and costs are debits. A statement reads better with both positive, then revenue minus costs. Compute each line's ",[955,956,957],"em",{},"natural"," amount: credit minus debit for revenue and other income, debit minus credit for everything else. Discounts are a contra-revenue account — a debit that reduces revenue — so they stay on the revenue side with a negative value:",[25,960,962],{"className":263,"code":961,"language":265,"meta":30,"style":30},"credit_lines = {\"Revenue\", \"Discounts\", \"Other income\"}\nnatural = pl[\"Credit\"] - pl[\"Debit\"]\npl[\"Amount\"] = natural.where(pl[\"Line\"].isin(credit_lines), -natural)\npl[\"Month\"] = pl[\"Date\"].dt.to_period(\"M\").astype(str)\n\ndetail = pl.pivot_table(index=[\"Line\", \"Account\", \"Name\"], columns=\"Month\",\n                        values=\"Amount\", aggfunc=\"sum\", fill_value=0).round(2)\nprint(detail.head(6).to_string())\n",[32,963,964,987,1009,1033,1062,1066,1104,1139],{"__ignoreMap":30},[35,965,966,969,971,974,976,978,980,982,984],{"class":37,"line":38},[35,967,968],{"class":276},"credit_lines ",[35,970,299],{"class":272},[35,972,973],{"class":276}," {",[35,975,688],{"class":45},[35,977,308],{"class":276},[35,979,693],{"class":45},[35,981,308],{"class":276},[35,983,726],{"class":45},[35,985,986],{"class":276},"}\n",[35,988,989,992,994,997,999,1001,1003,1005,1007],{"class":37,"line":286},[35,990,991],{"class":276},"natural ",[35,993,299],{"class":272},[35,995,996],{"class":276}," pl[",[35,998,388],{"class":45},[35,1000,379],{"class":276},[35,1002,382],{"class":272},[35,1004,996],{"class":276},[35,1006,376],{"class":45},[35,1008,676],{"class":276},[35,1010,1011,1013,1016,1018,1020,1023,1025,1028,1030],{"class":37,"line":293},[35,1012,854],{"class":276},[35,1014,1015],{"class":45},"\"Amount\"",[35,1017,379],{"class":276},[35,1019,299],{"class":272},[35,1021,1022],{"class":276}," natural.where(pl[",[35,1024,780],{"class":45},[35,1026,1027],{"class":276},"].isin(credit_lines), ",[35,1029,382],{"class":272},[35,1031,1032],{"class":276},"natural)\n",[35,1034,1035,1037,1040,1042,1044,1046,1048,1051,1054,1057,1060],{"class":37,"line":336},[35,1036,854],{"class":276},[35,1038,1039],{"class":45},"\"Month\"",[35,1041,379],{"class":276},[35,1043,299],{"class":272},[35,1045,996],{"class":276},[35,1047,330],{"class":45},[35,1049,1050],{"class":276},"].dt.to_period(",[35,1052,1053],{"class":45},"\"M\"",[35,1055,1056],{"class":276},").astype(",[35,1058,1059],{"class":394},"str",[35,1061,357],{"class":276},[35,1063,1064],{"class":37,"line":360},[35,1065,290],{"emptyLinePlaceholder":289},[35,1067,1068,1071,1073,1076,1078,1080,1082,1084,1086,1088,1090,1093,1095,1098,1100,1102],{"class":37,"line":365},[35,1069,1070],{"class":276},"detail ",[35,1072,299],{"class":272},[35,1074,1075],{"class":276}," pl.pivot_table(",[35,1077,793],{"class":311},[35,1079,299],{"class":272},[35,1081,327],{"class":276},[35,1083,780],{"class":45},[35,1085,308],{"class":276},[35,1087,470],{"class":45},[35,1089,308],{"class":276},[35,1091,1092],{"class":45},"\"Name\"",[35,1094,766],{"class":276},[35,1096,1097],{"class":311},"columns",[35,1099,299],{"class":272},[35,1101,1039],{"class":45},[35,1103,716],{"class":276},[35,1105,1106,1109,1111,1113,1115,1118,1120,1123,1125,1128,1130,1132,1135,1137],{"class":37,"line":400},[35,1107,1108],{"class":311},"                        values",[35,1110,299],{"class":272},[35,1112,1015],{"class":45},[35,1114,308],{"class":276},[35,1116,1117],{"class":311},"aggfunc",[35,1119,299],{"class":272},[35,1121,1122],{"class":45},"\"sum\"",[35,1124,308],{"class":276},[35,1126,1127],{"class":311},"fill_value",[35,1129,299],{"class":272},[35,1131,93],{"class":394},[35,1133,1134],{"class":276},").round(",[35,1136,395],{"class":394},[35,1138,357],{"class":276},[35,1140,1141,1143,1146,1149],{"class":37,"line":430},[35,1142,490],{"class":394},[35,1144,1145],{"class":276},"(detail.head(",[35,1147,1148],{"class":394},"6",[35,1150,1151],{"class":276},").to_string())\n",[71,1153,80,1159,80,1162,80,1165,80,1170,80,1176,80,1182,80,1186,80,1190,80,1194,80,1198,80,1202,80,1206,80,1210,80,1215,80,1217,80,1220,80,1222,80,1225,80,1227,80,1230,80,1233,80,1237,80,1239,80,1242,80,1244,80,1247,80,1251,80,1255,80,1258,80,1262,80,1264,80,1267,80,1269,80,1272,80,1274,80,1277,80,1280,80,1284,80,1286,80,1289,80,1291,80,1294,80,1296],{"viewBox":1154,"role":74,"ariaLabelledBy":1155,"xmlns":78,"style":1158},"163 0 434 172",[1156,1157],"pl-signs-t","pl-signs-d","width:100%;max-width:434px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[82,1160,1161],{"id":1156},"The same entries, before and after signing",[86,1163,1164],{"id":1157},"A table of quarter totals in the ledger and on the statement. Product sales, a credit of 569,139 in the ledger, shows as positive revenue. Discounts, a debit of 19,977, shows as negative revenue. Materials, a debit, shows as a positive cost. Interest income, a credit, shows as positive other income.",[90,1166],{"x":1167,"y":93,"width":1168,"height":1169,"fill":96},"163","434","172",[90,1171],{"x":1172,"y":100,"width":1173,"height":1174,"rx":93,"fill":179,"stroke":139,"style":1175},"179.4","115.1","28.0","stroke-width:1.5px",[119,1177,1181],{"x":1178,"y":1179,"style":1180},"237.0","35.0","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Account",[90,1183],{"x":1184,"y":100,"width":1185,"height":1174,"rx":93,"fill":179,"stroke":139,"style":1175},"294.5","107.5",[119,1187,1189],{"x":1188,"y":1179,"style":1180},"348.2","Ledger",[90,1191],{"x":1192,"y":100,"width":1193,"height":1174,"rx":93,"fill":179,"stroke":139,"style":1175},"402.0","105.5",[119,1195,1197],{"x":1196,"y":1179,"style":1180},"454.8","Statement line",[90,1199],{"x":1200,"y":100,"width":1201,"height":1174,"rx":93,"fill":179,"stroke":139,"style":1175},"507.5","73.0",[119,1203,1205],{"x":1204,"y":1179,"style":1180},"544.0","Amount",[90,1207],{"x":1172,"y":1208,"width":1173,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},"44.0","stroke-width:1px",[119,1211,1214],{"x":1178,"y":1212,"style":1213},"63.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Product sales",[90,1216],{"x":1184,"y":1208,"width":1185,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1218,1219],{"x":1188,"y":1212,"style":1213},"Credit 569,139",[90,1221],{"x":1192,"y":1208,"width":1193,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1223,1224],{"x":1196,"y":1212,"style":1213},"Revenue",[90,1226],{"x":1200,"y":1208,"width":1201,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1228,1229],{"x":1204,"y":1212,"style":1213},"569,139",[90,1231],{"x":1172,"y":1232,"width":1173,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},"72.0",[119,1234,1236],{"x":1178,"y":1235,"style":1213},"91.0","Discounts given",[90,1238],{"x":1184,"y":1232,"width":1185,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1240,1241],{"x":1188,"y":1235,"style":1213},"Debit 19,977",[90,1243],{"x":1192,"y":1232,"width":1193,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1245,1246],{"x":1196,"y":1235,"style":1213},"Discounts",[90,1248],{"x":1200,"y":1232,"width":1201,"height":1174,"rx":93,"fill":1249,"stroke":1250,"style":1175},"#fce9e9","var(--danger,#dc2626)",[119,1252,1254],{"x":1204,"y":1235,"style":1253},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","-19,977",[90,1256],{"x":1172,"y":1257,"width":1173,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},"100.0",[119,1259,1261],{"x":1178,"y":1260,"style":1213},"119.0","Materials",[90,1263],{"x":1184,"y":1257,"width":1185,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1265,1266],{"x":1188,"y":1260,"style":1213},"Debit 224,948",[90,1268],{"x":1192,"y":1257,"width":1193,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1270,1271],{"x":1196,"y":1260,"style":1213},"Cost of sales",[90,1273],{"x":1200,"y":1257,"width":1201,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1275,1276],{"x":1204,"y":1260,"style":1213},"224,948",[90,1278],{"x":1172,"y":1279,"width":1173,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},"128.0",[119,1281,1283],{"x":1178,"y":1282,"style":1213},"147.0","Interest income",[90,1285],{"x":1184,"y":1279,"width":1185,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1287,1288],{"x":1188,"y":1282,"style":1213},"Credit 1,103",[90,1290],{"x":1192,"y":1279,"width":1193,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1292,1293],{"x":1196,"y":1282,"style":1213},"Other income",[90,1295],{"x":1200,"y":1279,"width":1201,"height":1174,"rx":93,"fill":96,"stroke":105,"style":1209},[119,1297,1298],{"x":1204,"y":1282,"style":1213},"1,103",[20,1300,1302],{"id":1301},"step-4-subtotals-and-margins","Step 4 — Subtotals and margins",[10,1304,1305],{},"A P&L is a sequence of subtracted lines with subtotals: revenue less discounts is net revenue, less cost of sales is gross profit, less operating expenses is operating profit, then other items and tax to net profit. Compute them from the line totals and add margins as a share of net revenue:",[25,1307,1309],{"className":263,"code":1308,"language":265,"meta":30,"style":30},"by_line = detail.groupby(level=\"Line\").sum()\nL = lambda name: by_line.loc[name] if name in by_line.index else 0\n\nstatement = pd.DataFrame({\n    \"Revenue\": L(\"Revenue\"),\n    \"Discounts\": L(\"Discounts\"),\n    \"Net revenue\": L(\"Revenue\") + L(\"Discounts\"),\n    \"Cost of sales\": L(\"Cost of sales\"),\n})\nstatement[\"Gross profit\"] = statement[\"Net revenue\"] - statement[\"Cost of sales\"]\nfor line in [\"People\", \"Premises and software\", \"Marketing\"]:\n    statement[line] = L(line)\nstatement[\"Operating profit\"] = statement[\"Gross profit\"] - statement[[\"People\", \"Premises and software\", \"Marketing\"]].sum(axis=1)\nstatement[\"Other income\"] = L(\"Other income\")\nstatement[\"Other expense\"] = L(\"Other expense\")\nstatement[\"Profit before tax\"] = statement[\"Operating profit\"] + statement[\"Other income\"] - statement[\"Other expense\"]\nstatement[\"Tax\"] = L(\"Tax\")\nstatement[\"Net profit\"] = statement[\"Profit before tax\"] - statement[\"Tax\"]\nstatement = statement.T\nstatement[\"Q3\"] = statement.sum(axis=1)\n\nmargins = (statement.loc[[\"Gross profit\", \"Operating profit\", \"Net profit\"]] \u002F statement.loc[\"Net revenue\"] * 100).round(1)\nprint(statement.round(0).astype(int).to_string())\nprint(margins.to_string())\n",[32,1310,1311,1331,1362,1366,1376,1389,1400,1422,1433,1438,1466,1492,1503,1548,1565,1582,1616,1633,1659,1669,1692,1697,1743,1760],{"__ignoreMap":30},[35,1312,1313,1316,1318,1321,1324,1326,1328],{"class":37,"line":38},[35,1314,1315],{"class":276},"by_line ",[35,1317,299],{"class":272},[35,1319,1320],{"class":276}," detail.groupby(",[35,1322,1323],{"class":311},"level",[35,1325,299],{"class":272},[35,1327,780],{"class":45},[35,1329,1330],{"class":276},").sum()\n",[35,1332,1333,1336,1338,1341,1344,1347,1350,1353,1356,1359],{"class":37,"line":286},[35,1334,1335],{"class":276},"L ",[35,1337,299],{"class":272},[35,1339,1340],{"class":272}," lambda",[35,1342,1343],{"class":276}," name: by_line.loc[name] ",[35,1345,1346],{"class":272},"if",[35,1348,1349],{"class":276}," name ",[35,1351,1352],{"class":272},"in",[35,1354,1355],{"class":276}," by_line.index ",[35,1357,1358],{"class":272},"else",[35,1360,1361],{"class":394}," 0\n",[35,1363,1364],{"class":37,"line":293},[35,1365,290],{"emptyLinePlaceholder":289},[35,1367,1368,1371,1373],{"class":37,"line":336},[35,1369,1370],{"class":276},"statement ",[35,1372,299],{"class":272},[35,1374,1375],{"class":276}," pd.DataFrame({\n",[35,1377,1378,1381,1384,1386],{"class":37,"line":360},[35,1379,1380],{"class":45},"    \"Revenue\"",[35,1382,1383],{"class":276},": L(",[35,1385,688],{"class":45},[35,1387,1388],{"class":276},"),\n",[35,1390,1391,1394,1396,1398],{"class":37,"line":365},[35,1392,1393],{"class":45},"    \"Discounts\"",[35,1395,1383],{"class":276},[35,1397,693],{"class":45},[35,1399,1388],{"class":276},[35,1401,1402,1405,1407,1409,1412,1415,1418,1420],{"class":37,"line":400},[35,1403,1404],{"class":45},"    \"Net revenue\"",[35,1406,1383],{"class":276},[35,1408,688],{"class":45},[35,1410,1411],{"class":276},") ",[35,1413,1414],{"class":272},"+",[35,1416,1417],{"class":276}," L(",[35,1419,693],{"class":45},[35,1421,1388],{"class":276},[35,1423,1424,1427,1429,1431],{"class":37,"line":430},[35,1425,1426],{"class":45},"    \"Cost of sales\"",[35,1428,1383],{"class":276},[35,1430,703],{"class":45},[35,1432,1388],{"class":276},[35,1434,1435],{"class":37,"line":456},[35,1436,1437],{"class":276},"})\n",[35,1439,1440,1443,1446,1448,1450,1453,1456,1458,1460,1462,1464],{"class":37,"line":487},[35,1441,1442],{"class":276},"statement[",[35,1444,1445],{"class":45},"\"Gross profit\"",[35,1447,379],{"class":276},[35,1449,299],{"class":272},[35,1451,1452],{"class":276}," statement[",[35,1454,1455],{"class":45},"\"Net revenue\"",[35,1457,379],{"class":276},[35,1459,382],{"class":272},[35,1461,1452],{"class":276},[35,1463,703],{"class":45},[35,1465,676],{"class":276},[35,1467,1468,1471,1474,1476,1478,1480,1482,1484,1486,1489],{"class":37,"line":545},[35,1469,1470],{"class":272},"for",[35,1472,1473],{"class":276}," line ",[35,1475,1352],{"class":272},[35,1477,615],{"class":276},[35,1479,708],{"class":45},[35,1481,308],{"class":276},[35,1483,713],{"class":45},[35,1485,308],{"class":276},[35,1487,1488],{"class":45},"\"Marketing\"",[35,1490,1491],{"class":276},"]:\n",[35,1493,1495,1498,1500],{"class":37,"line":1494},12,[35,1496,1497],{"class":276},"    statement[line] ",[35,1499,299],{"class":272},[35,1501,1502],{"class":276}," L(line)\n",[35,1504,1506,1508,1511,1513,1515,1517,1519,1521,1523,1526,1528,1530,1532,1534,1536,1539,1542,1544,1546],{"class":37,"line":1505},13,[35,1507,1442],{"class":276},[35,1509,1510],{"class":45},"\"Operating profit\"",[35,1512,379],{"class":276},[35,1514,299],{"class":272},[35,1516,1452],{"class":276},[35,1518,1445],{"class":45},[35,1520,379],{"class":276},[35,1522,382],{"class":272},[35,1524,1525],{"class":276}," statement[[",[35,1527,708],{"class":45},[35,1529,308],{"class":276},[35,1531,713],{"class":45},[35,1533,308],{"class":276},[35,1535,1488],{"class":45},[35,1537,1538],{"class":276},"]].sum(",[35,1540,1541],{"class":311},"axis",[35,1543,299],{"class":272},[35,1545,763],{"class":394},[35,1547,357],{"class":276},[35,1549,1551,1553,1555,1557,1559,1561,1563],{"class":37,"line":1550},14,[35,1552,1442],{"class":276},[35,1554,726],{"class":45},[35,1556,379],{"class":276},[35,1558,299],{"class":272},[35,1560,1417],{"class":276},[35,1562,726],{"class":45},[35,1564,357],{"class":276},[35,1566,1568,1570,1572,1574,1576,1578,1580],{"class":37,"line":1567},15,[35,1569,1442],{"class":276},[35,1571,731],{"class":45},[35,1573,379],{"class":276},[35,1575,299],{"class":272},[35,1577,1417],{"class":276},[35,1579,731],{"class":45},[35,1581,357],{"class":276},[35,1583,1585,1587,1590,1592,1594,1596,1598,1600,1602,1604,1606,1608,1610,1612,1614],{"class":37,"line":1584},16,[35,1586,1442],{"class":276},[35,1588,1589],{"class":45},"\"Profit before tax\"",[35,1591,379],{"class":276},[35,1593,299],{"class":272},[35,1595,1452],{"class":276},[35,1597,1510],{"class":45},[35,1599,379],{"class":276},[35,1601,1414],{"class":272},[35,1603,1452],{"class":276},[35,1605,726],{"class":45},[35,1607,379],{"class":276},[35,1609,382],{"class":272},[35,1611,1452],{"class":276},[35,1613,731],{"class":45},[35,1615,676],{"class":276},[35,1617,1619,1621,1623,1625,1627,1629,1631],{"class":37,"line":1618},17,[35,1620,1442],{"class":276},[35,1622,740],{"class":45},[35,1624,379],{"class":276},[35,1626,299],{"class":272},[35,1628,1417],{"class":276},[35,1630,740],{"class":45},[35,1632,357],{"class":276},[35,1634,1636,1638,1641,1643,1645,1647,1649,1651,1653,1655,1657],{"class":37,"line":1635},18,[35,1637,1442],{"class":276},[35,1639,1640],{"class":45},"\"Net profit\"",[35,1642,379],{"class":276},[35,1644,299],{"class":272},[35,1646,1452],{"class":276},[35,1648,1589],{"class":45},[35,1650,379],{"class":276},[35,1652,382],{"class":272},[35,1654,1452],{"class":276},[35,1656,740],{"class":45},[35,1658,676],{"class":276},[35,1660,1662,1664,1666],{"class":37,"line":1661},19,[35,1663,1370],{"class":276},[35,1665,299],{"class":272},[35,1667,1668],{"class":276}," statement.T\n",[35,1670,1672,1674,1677,1679,1681,1684,1686,1688,1690],{"class":37,"line":1671},20,[35,1673,1442],{"class":276},[35,1675,1676],{"class":45},"\"Q3\"",[35,1678,379],{"class":276},[35,1680,299],{"class":272},[35,1682,1683],{"class":276}," statement.sum(",[35,1685,1541],{"class":311},[35,1687,299],{"class":272},[35,1689,763],{"class":394},[35,1691,357],{"class":276},[35,1693,1695],{"class":37,"line":1694},21,[35,1696,290],{"emptyLinePlaceholder":289},[35,1698,1700,1703,1705,1708,1710,1712,1714,1716,1718,1721,1724,1727,1729,1731,1734,1737,1739,1741],{"class":37,"line":1699},22,[35,1701,1702],{"class":276},"margins ",[35,1704,299],{"class":272},[35,1706,1707],{"class":276}," (statement.loc[[",[35,1709,1445],{"class":45},[35,1711,308],{"class":276},[35,1713,1510],{"class":45},[35,1715,308],{"class":276},[35,1717,1640],{"class":45},[35,1719,1720],{"class":276},"]] ",[35,1722,1723],{"class":272},"\u002F",[35,1725,1726],{"class":276}," statement.loc[",[35,1728,1455],{"class":45},[35,1730,379],{"class":276},[35,1732,1733],{"class":272},"*",[35,1735,1736],{"class":394}," 100",[35,1738,1134],{"class":276},[35,1740,763],{"class":394},[35,1742,357],{"class":276},[35,1744,1746,1748,1751,1753,1755,1758],{"class":37,"line":1745},23,[35,1747,490],{"class":394},[35,1749,1750],{"class":276},"(statement.round(",[35,1752,93],{"class":394},[35,1754,1056],{"class":276},[35,1756,1757],{"class":394},"int",[35,1759,1151],{"class":276},[35,1761,1763,1765],{"class":37,"line":1762},24,[35,1764,490],{"class":394},[35,1766,1767],{"class":276},"(margins.to_string())\n",[71,1769,80,1775,80,1778,80,1781,80,1786,80,1790,80,1795,80,1798,80,1801,80,1805,80,1808,80,1811,80,1816,80,1820,80,1824,80,1828,80,1831,80,1834,80,1840,80,1844,80,1848,80,1852,80,1855,80,1858,80,1864,80,1868,80,1872,80,1876,80,1879,80,1882,80,1887,80,1891],{"viewBox":1770,"role":74,"ariaLabelledBy":1771,"xmlns":78,"style":1774},"64 0 632 641",[1772,1773],"pl-cascade-t","pl-cascade-d","width:100%;max-width:632px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[82,1776,1777],{"id":1772},"How the statement steps down to net profit",[86,1779,1780],{"id":1773},"Five layers from top to bottom. Net revenue is revenue less discounts. Gross profit is net revenue less cost of sales. Operating profit is gross profit less people, premises and software, and marketing. Profit before tax adds other income and subtracts other expense. Net profit subtracts tax.",[90,1782],{"x":1783,"y":93,"width":1784,"height":1785,"fill":96},"64","632","641",[90,1787],{"x":1788,"y":100,"width":1789,"height":164,"rx":103,"fill":179,"stroke":139,"style":106},"80.0","600.0",[108,1791,1792],{"transform":182},[112,1793],{"d":1794,"fill":115,"stroke":186,"style":117},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8",[119,1796,1797],{"x":189,"y":122,"style":190},"Net revenue",[119,1799,1800],{"x":189,"y":127,"style":128},"revenue less discounts",[37,1802],{"x1":189,"y1":1803,"x2":189,"y2":1804,"stroke":139,"style":106},"114.0","136.0",[141,1806],{"points":1807,"fill":144},"380.0,140.0 375.0,131.0 385.0,131.0",[90,1809],{"x":1788,"y":1810,"width":1789,"height":164,"rx":103,"fill":148,"stroke":149,"style":106},"145.0",[108,1812,1814],{"transform":1813},"translate(368.0,158.0) scale(1.000)",[112,1815],{"d":213,"fill":115,"stroke":156,"style":117},[119,1817,1819],{"x":189,"y":1818,"style":160},"204.5","Gross profit",[119,1821,1823],{"x":189,"y":1822,"style":128},"224.0","less cost of sales",[37,1825],{"x1":189,"y1":1826,"x2":189,"y2":1827,"stroke":139,"style":106},"243.0","265.0",[141,1829],{"points":1830,"fill":144},"380.0,269.0 375.0,260.0 385.0,260.0",[90,1832],{"x":1788,"y":1833,"width":1789,"height":164,"rx":103,"fill":148,"stroke":149,"style":106},"274.0",[108,1835,1837],{"transform":1836},"translate(368.0,287.0) scale(1.000)",[112,1838],{"d":1839,"fill":115,"stroke":156,"style":117},"M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6 M16 4.5a3.3 3.3 0 0 1 0 6.3 M18 14.3c2.2.7 3.5 2.8 3.5 5.7",[119,1841,1843],{"x":189,"y":1842,"style":160},"333.5","Operating profit",[119,1845,1847],{"x":189,"y":1846,"style":128},"353.0","less people, premises, marketing",[37,1849],{"x1":189,"y1":1850,"x2":189,"y2":1851,"stroke":139,"style":106},"372.0","394.0",[141,1853],{"points":1854,"fill":144},"380.0,398.0 375.0,389.0 385.0,389.0",[90,1856],{"x":1788,"y":1857,"width":1789,"height":164,"rx":103,"fill":234,"stroke":235,"style":106},"403.0",[108,1859,1861],{"transform":1860},"translate(368.0,416.0) scale(1.000)",[112,1862],{"d":1863,"fill":115,"stroke":242,"style":117},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16",[119,1865,1867],{"x":189,"y":1866,"style":246},"462.5","Profit before tax",[119,1869,1871],{"x":189,"y":1870,"style":128},"482.0","plus other income, less other expense",[37,1873],{"x1":189,"y1":1874,"x2":189,"y2":1875,"stroke":139,"style":106},"501.0","523.0",[141,1877],{"points":1878,"fill":144},"380.0,527.0 375.0,518.0 385.0,518.0",[90,1880],{"x":1788,"y":1881,"width":1789,"height":164,"rx":103,"fill":234,"stroke":235,"style":106},"532.0",[108,1883,1885],{"transform":1884},"translate(368.0,545.0) scale(1.000)",[112,1886],{"d":155,"fill":115,"stroke":242,"style":117},[119,1888,1890],{"x":189,"y":1889,"style":246},"591.5","Net profit",[119,1892,1894],{"x":189,"y":1893,"style":128},"611.0","less tax",[10,1896,1897],{},"Always reconcile the bottom line independently: net profit must equal the sum of every P&L account's credit minus debit, whatever the grouping. If a mapping change drops an account, this check catches it. It cannot catch an account filed under the wrong line, such as software landing in marketing because a range boundary is off by one; only the account detail under each line shows that, which is why the report keeps it:",[25,1899,1901],{"className":263,"code":1900,"language":265,"meta":30,"style":30},"independent = (pl[\"Credit\"] - pl[\"Debit\"]).sum().round(2)\nprint(f\"net profit from the statement {statement.loc['Net profit', 'Q3']:.2f}; \"\n      f\"from the ledger {independent:.2f}\")\n",[32,1902,1903,1929,1964],{"__ignoreMap":30},[35,1904,1905,1908,1910,1913,1915,1917,1919,1921,1923,1925,1927],{"class":37,"line":38},[35,1906,1907],{"class":276},"independent ",[35,1909,299],{"class":272},[35,1911,1912],{"class":276}," (pl[",[35,1914,388],{"class":45},[35,1916,379],{"class":276},[35,1918,382],{"class":272},[35,1920,996],{"class":276},[35,1922,376],{"class":45},[35,1924,391],{"class":276},[35,1926,395],{"class":394},[35,1928,357],{"class":276},[35,1930,1931,1933,1935,1937,1940,1942,1945,1948,1950,1953,1956,1959,1961],{"class":37,"line":286},[35,1932,490],{"class":394},[35,1934,493],{"class":276},[35,1936,496],{"class":272},[35,1938,1939],{"class":45},"\"net profit from the statement ",[35,1941,503],{"class":502},[35,1943,1944],{"class":276},"statement.loc[",[35,1946,1947],{"class":45},"'Net profit'",[35,1949,308],{"class":276},[35,1951,1952],{"class":45},"'Q3'",[35,1954,1955],{"class":276},"]",[35,1957,1958],{"class":272},":.2f",[35,1960,512],{"class":502},[35,1962,1963],{"class":45},"; \"\n",[35,1965,1966,1969,1972,1974,1977,1979,1981,1983],{"class":37,"line":293},[35,1967,1968],{"class":272},"      f",[35,1970,1971],{"class":45},"\"from the ledger ",[35,1973,503],{"class":502},[35,1975,1976],{"class":276},"independent",[35,1978,1958],{"class":272},[35,1980,512],{"class":502},[35,1982,499],{"class":45},[35,1984,357],{"class":276},[20,1986,1988],{"id":1987},"step-5-write-the-statement-with-account-detail","Step 5 — Write the statement with account detail",[10,1990,1991,1992,1995],{},"The reader wants the summary lines, with the option to open a line and see the accounts behind it. Excel's outline grouping does that: account rows sit at level 1 under each line and collapse with the minus buttons in the margin. Subtotals are ",[32,1993,1994],{},"SUM"," formulas over the line rows, so the workbook stays consistent if someone adjusts a figure:",[25,1997,1999],{"className":263,"code":1998,"language":265,"meta":30,"style":30},"import xlsxwriter\n\nmonths = list(statement.columns)\nwb = xlsxwriter.Workbook(\"pl_q3_2026.xlsx\")\nws = wb.add_worksheet(\"P&L\")\nnum = wb.add_format({\"num_format\": \"#,##0;(#,##0)\"})\nbold = wb.add_format({\"num_format\": \"#,##0;(#,##0)\", \"bold\": True})\nsub = wb.add_format({\"num_format\": \"#,##0;(#,##0)\", \"bold\": True, \"top\": 1})\npct = wb.add_format({\"num_format\": \"0.0%\", \"italic\": True, \"font_color\": \"#555555\"})\nhead = wb.add_format({\"bold\": True, \"bottom\": 1, \"align\": \"right\"})\nacct_fmt = wb.add_format({\"num_format\": \"#,##0;(#,##0)\", \"font_color\": \"#555555\"})\n\nws.write(0, 0, \"Profit and loss, Q3 2026\", wb.add_format({\"bold\": True, \"font_size\": 13}))\nws.write_row(2, 1, [pd.Period(m).strftime(\"%b %Y\") if m != \"Q3\" else \"Q3 total\" for m in months], head)\nrow = 3\ncell = {}          # statement line -> worksheet row, for formulas\n\ndef col(c):\n    return xlsxwriter.utility.xl_col_to_name(c)\n\ndef write_line(name, fmt=bold):\n    global row\n    ws.write(row, 0, name, fmt)\n    for c, m in enumerate(months, start=1):\n        ws.write_number(row, c, float(statement.loc[name, m]), fmt)\n    cell[name] = row\n    row += 1\n    accounts = detail.loc[name] if name in detail.index.get_level_values(0) else None\n    if accounts is not None and len(accounts) > 1:\n        for (acct, acct_name), values in accounts.iterrows():\n            ws.write(row, 0, f\"   {acct} {acct_name}\")\n            for c, m in enumerate(months[:-1], start=1):\n                ws.write_number(row, c, float(values[m]), acct_fmt)\n            ws.write_formula(row, len(months), f\"=SUM(B{row + 1}:{col(len(months) - 1)}{row + 1})\", acct_fmt)\n            ws.set_row(row, None, None, {\"level\": 1, \"hidden\": True})\n            row += 1\n        ws.set_row(cell[name], None, None, {\"collapsed\": True})\n\ndef write_total(name, terms):\n    \"\"\"terms: (sign, line) pairs, e.g. [(\"+\", \"Revenue\"), (\"+\", \"Discounts\")].\"\"\"\n    global row\n    ws.write(row, 0, name, sub)\n    for c in range(1, len(months) + 1):\n        formula = \"\".join(f\"{sign}{col(c)}{cell[line] + 1}\" for sign, line in terms)\n        ws.write_formula(row, c, \"=\" + formula.lstrip(\"+\"), sub)\n    cell[name] = row\n    row += 2\n\nwrite_line(\"Revenue\"); write_line(\"Discounts\")\nwrite_total(\"Net revenue\", [(\"+\", \"Revenue\"), (\"+\", \"Discounts\")])\nwrite_line(\"Cost of sales\")\nwrite_total(\"Gross profit\", [(\"+\", \"Net revenue\"), (\"-\", \"Cost of sales\")])\nopex = [\"People\", \"Premises and software\", \"Marketing\"]\nfor line in opex:\n    write_line(line)\nwrite_total(\"Operating profit\", [(\"+\", \"Gross profit\")] + [(\"-\", line) for line in opex])\nwrite_line(\"Other income\"); write_line(\"Other expense\")\nwrite_total(\"Profit before tax\", [(\"+\", \"Operating profit\"), (\"+\", \"Other income\"), (\"-\", \"Other expense\")])\nwrite_line(\"Tax\")\nwrite_total(\"Net profit\", [(\"+\", \"Profit before tax\"), (\"-\", \"Tax\")])\n\nfor name in [\"Gross profit\", \"Operating profit\", \"Net profit\"]:\n    ws.write(row, 0, f\"{name} margin\", pct)\n    for c in range(1, len(months) + 1):\n        ws.write_formula(row, c, f\"={col(c)}{cell[name] + 1}\u002F{col(c)}{cell['Net revenue'] + 1}\", pct)\n    row += 1\n\nws.set_column(0, 0, 30)\nws.set_column(1, len(months), 13)\nws.outline_settings(True, False, True, False)   # summary rows above the detail\nws.freeze_panes(3, 1)\nwb.close()\nprint(f\"wrote {row} rows; net profit on row {cell['Net profit'] + 1}\")\n",[32,2000,2001,2007,2011,2024,2039,2054,2075,2102,2136,2173,2209,2234,2238,2276,2326,2336,2350,2354,2366,2374,2378,2393,2401,2411,2437,2449,2459,2471,2500,2536,2550,2583,2612,2623,2686,2719,2729,2752,2757,2768,2774,2781,2791,2820,2871,2892,2901,2911,2916,2931,2960,2969,2995,3017,3029,3035,3072,3085,3118,3127,3152,3157,3180,3206,3233,3284,3293,3298,3317,3334,3360,3375,3381],{"__ignoreMap":30},[35,2002,2003,2005],{"class":37,"line":38},[35,2004,273],{"class":272},[35,2006,55],{"class":276},[35,2008,2009],{"class":37,"line":286},[35,2010,290],{"emptyLinePlaceholder":289},[35,2012,2013,2016,2018,2021],{"class":37,"line":293},[35,2014,2015],{"class":276},"months ",[35,2017,299],{"class":272},[35,2019,2020],{"class":394}," list",[35,2022,2023],{"class":276},"(statement.columns)\n",[35,2025,2026,2029,2031,2034,2037],{"class":37,"line":336},[35,2027,2028],{"class":276},"wb ",[35,2030,299],{"class":272},[35,2032,2033],{"class":276}," xlsxwriter.Workbook(",[35,2035,2036],{"class":45},"\"pl_q3_2026.xlsx\"",[35,2038,357],{"class":276},[35,2040,2041,2044,2046,2049,2052],{"class":37,"line":360},[35,2042,2043],{"class":276},"ws ",[35,2045,299],{"class":272},[35,2047,2048],{"class":276}," wb.add_worksheet(",[35,2050,2051],{"class":45},"\"P&L\"",[35,2053,357],{"class":276},[35,2055,2056,2059,2061,2064,2067,2070,2073],{"class":37,"line":365},[35,2057,2058],{"class":276},"num ",[35,2060,299],{"class":272},[35,2062,2063],{"class":276}," wb.add_format({",[35,2065,2066],{"class":45},"\"num_format\"",[35,2068,2069],{"class":276},": ",[35,2071,2072],{"class":45},"\"#,##0;(#,##0)\"",[35,2074,1437],{"class":276},[35,2076,2077,2080,2082,2084,2086,2088,2090,2092,2095,2097,2100],{"class":37,"line":400},[35,2078,2079],{"class":276},"bold ",[35,2081,299],{"class":272},[35,2083,2063],{"class":276},[35,2085,2066],{"class":45},[35,2087,2069],{"class":276},[35,2089,2072],{"class":45},[35,2091,308],{"class":276},[35,2093,2094],{"class":45},"\"bold\"",[35,2096,2069],{"class":276},[35,2098,2099],{"class":394},"True",[35,2101,1437],{"class":276},[35,2103,2104,2107,2109,2111,2113,2115,2117,2119,2121,2123,2125,2127,2130,2132,2134],{"class":37,"line":430},[35,2105,2106],{"class":276},"sub ",[35,2108,299],{"class":272},[35,2110,2063],{"class":276},[35,2112,2066],{"class":45},[35,2114,2069],{"class":276},[35,2116,2072],{"class":45},[35,2118,308],{"class":276},[35,2120,2094],{"class":45},[35,2122,2069],{"class":276},[35,2124,2099],{"class":394},[35,2126,308],{"class":276},[35,2128,2129],{"class":45},"\"top\"",[35,2131,2069],{"class":276},[35,2133,763],{"class":394},[35,2135,1437],{"class":276},[35,2137,2138,2141,2143,2145,2147,2149,2152,2154,2157,2159,2161,2163,2166,2168,2171],{"class":37,"line":456},[35,2139,2140],{"class":276},"pct ",[35,2142,299],{"class":272},[35,2144,2063],{"class":276},[35,2146,2066],{"class":45},[35,2148,2069],{"class":276},[35,2150,2151],{"class":45},"\"0.0%\"",[35,2153,308],{"class":276},[35,2155,2156],{"class":45},"\"italic\"",[35,2158,2069],{"class":276},[35,2160,2099],{"class":394},[35,2162,308],{"class":276},[35,2164,2165],{"class":45},"\"font_color\"",[35,2167,2069],{"class":276},[35,2169,2170],{"class":45},"\"#555555\"",[35,2172,1437],{"class":276},[35,2174,2175,2178,2180,2182,2184,2186,2188,2190,2193,2195,2197,2199,2202,2204,2207],{"class":37,"line":487},[35,2176,2177],{"class":276},"head ",[35,2179,299],{"class":272},[35,2181,2063],{"class":276},[35,2183,2094],{"class":45},[35,2185,2069],{"class":276},[35,2187,2099],{"class":394},[35,2189,308],{"class":276},[35,2191,2192],{"class":45},"\"bottom\"",[35,2194,2069],{"class":276},[35,2196,763],{"class":394},[35,2198,308],{"class":276},[35,2200,2201],{"class":45},"\"align\"",[35,2203,2069],{"class":276},[35,2205,2206],{"class":45},"\"right\"",[35,2208,1437],{"class":276},[35,2210,2211,2214,2216,2218,2220,2222,2224,2226,2228,2230,2232],{"class":37,"line":545},[35,2212,2213],{"class":276},"acct_fmt ",[35,2215,299],{"class":272},[35,2217,2063],{"class":276},[35,2219,2066],{"class":45},[35,2221,2069],{"class":276},[35,2223,2072],{"class":45},[35,2225,308],{"class":276},[35,2227,2165],{"class":45},[35,2229,2069],{"class":276},[35,2231,2170],{"class":45},[35,2233,1437],{"class":276},[35,2235,2236],{"class":37,"line":1494},[35,2237,290],{"emptyLinePlaceholder":289},[35,2239,2240,2243,2245,2247,2249,2251,2254,2257,2259,2261,2263,2265,2268,2270,2273],{"class":37,"line":1505},[35,2241,2242],{"class":276},"ws.write(",[35,2244,93],{"class":394},[35,2246,308],{"class":276},[35,2248,93],{"class":394},[35,2250,308],{"class":276},[35,2252,2253],{"class":45},"\"Profit and loss, Q3 2026\"",[35,2255,2256],{"class":276},", wb.add_format({",[35,2258,2094],{"class":45},[35,2260,2069],{"class":276},[35,2262,2099],{"class":394},[35,2264,308],{"class":276},[35,2266,2267],{"class":45},"\"font_size\"",[35,2269,2069],{"class":276},[35,2271,2272],{"class":394},"13",[35,2274,2275],{"class":276},"}))\n",[35,2277,2278,2281,2283,2285,2287,2290,2292,2295,2298,2300,2302,2305,2307,2310,2313,2316,2319,2321,2323],{"class":37,"line":1550},[35,2279,2280],{"class":276},"ws.write_row(",[35,2282,395],{"class":394},[35,2284,308],{"class":276},[35,2286,763],{"class":394},[35,2288,2289],{"class":276},", [pd.Period(m).strftime(",[35,2291,499],{"class":45},[35,2293,2294],{"class":502},"%b",[35,2296,2297],{"class":45}," %Y\"",[35,2299,1411],{"class":276},[35,2301,1346],{"class":272},[35,2303,2304],{"class":276}," m ",[35,2306,445],{"class":272},[35,2308,2309],{"class":45}," \"Q3\"",[35,2311,2312],{"class":272}," else",[35,2314,2315],{"class":45}," \"Q3 total\"",[35,2317,2318],{"class":272}," for",[35,2320,2304],{"class":276},[35,2322,1352],{"class":272},[35,2324,2325],{"class":276}," months], head)\n",[35,2327,2328,2331,2333],{"class":37,"line":1567},[35,2329,2330],{"class":276},"row ",[35,2332,299],{"class":272},[35,2334,2335],{"class":394}," 3\n",[35,2337,2338,2341,2343,2346],{"class":37,"line":1584},[35,2339,2340],{"class":276},"cell ",[35,2342,299],{"class":272},[35,2344,2345],{"class":276}," {}          ",[35,2347,2349],{"class":2348},"s-wDw","# statement line -> worksheet row, for formulas\n",[35,2351,2352],{"class":37,"line":1618},[35,2353,290],{"emptyLinePlaceholder":289},[35,2355,2356,2359,2363],{"class":37,"line":1635},[35,2357,2358],{"class":272},"def",[35,2360,2362],{"class":2361},"s_Opv"," col",[35,2364,2365],{"class":276},"(c):\n",[35,2367,2368,2371],{"class":37,"line":1661},[35,2369,2370],{"class":272},"    return",[35,2372,2373],{"class":276}," xlsxwriter.utility.xl_col_to_name(c)\n",[35,2375,2376],{"class":37,"line":1671},[35,2377,290],{"emptyLinePlaceholder":289},[35,2379,2380,2382,2385,2388,2390],{"class":37,"line":1694},[35,2381,2358],{"class":272},[35,2383,2384],{"class":2361}," write_line",[35,2386,2387],{"class":276},"(name, fmt",[35,2389,299],{"class":272},[35,2391,2392],{"class":276},"bold):\n",[35,2394,2395,2398],{"class":37,"line":1699},[35,2396,2397],{"class":272},"    global",[35,2399,2400],{"class":276}," row\n",[35,2402,2403,2406,2408],{"class":37,"line":1745},[35,2404,2405],{"class":276},"    ws.write(row, ",[35,2407,93],{"class":394},[35,2409,2410],{"class":276},", name, fmt)\n",[35,2412,2413,2416,2419,2421,2424,2427,2430,2432,2434],{"class":37,"line":1762},[35,2414,2415],{"class":272},"    for",[35,2417,2418],{"class":276}," c, m ",[35,2420,1352],{"class":272},[35,2422,2423],{"class":394}," enumerate",[35,2425,2426],{"class":276},"(months, ",[35,2428,2429],{"class":311},"start",[35,2431,299],{"class":272},[35,2433,763],{"class":394},[35,2435,2436],{"class":276},"):\n",[35,2438,2440,2443,2446],{"class":37,"line":2439},25,[35,2441,2442],{"class":276},"        ws.write_number(row, c, ",[35,2444,2445],{"class":394},"float",[35,2447,2448],{"class":276},"(statement.loc[name, m]), fmt)\n",[35,2450,2452,2455,2457],{"class":37,"line":2451},26,[35,2453,2454],{"class":276},"    cell[name] ",[35,2456,299],{"class":272},[35,2458,2400],{"class":276},[35,2460,2462,2465,2468],{"class":37,"line":2461},27,[35,2463,2464],{"class":276},"    row ",[35,2466,2467],{"class":272},"+=",[35,2469,2470],{"class":394}," 1\n",[35,2472,2474,2477,2479,2482,2484,2486,2488,2491,2493,2495,2497],{"class":37,"line":2473},28,[35,2475,2476],{"class":276},"    accounts ",[35,2478,299],{"class":272},[35,2480,2481],{"class":276}," detail.loc[name] ",[35,2483,1346],{"class":272},[35,2485,1349],{"class":276},[35,2487,1352],{"class":272},[35,2489,2490],{"class":276}," detail.index.get_level_values(",[35,2492,93],{"class":394},[35,2494,1411],{"class":276},[35,2496,1358],{"class":272},[35,2498,2499],{"class":394}," None\n",[35,2501,2503,2506,2509,2512,2515,2518,2521,2524,2527,2530,2533],{"class":37,"line":2502},29,[35,2504,2505],{"class":272},"    if",[35,2507,2508],{"class":276}," accounts ",[35,2510,2511],{"class":272},"is",[35,2513,2514],{"class":272}," not",[35,2516,2517],{"class":394}," None",[35,2519,2520],{"class":272}," and",[35,2522,2523],{"class":394}," len",[35,2525,2526],{"class":276},"(accounts) ",[35,2528,2529],{"class":272},">",[35,2531,2532],{"class":394}," 1",[35,2534,2535],{"class":276},":\n",[35,2537,2539,2542,2545,2547],{"class":37,"line":2538},30,[35,2540,2541],{"class":272},"        for",[35,2543,2544],{"class":276}," (acct, acct_name), values ",[35,2546,1352],{"class":272},[35,2548,2549],{"class":276}," accounts.iterrows():\n",[35,2551,2553,2556,2558,2560,2562,2565,2567,2570,2572,2574,2577,2579,2581],{"class":37,"line":2552},31,[35,2554,2555],{"class":276},"            ws.write(row, ",[35,2557,93],{"class":394},[35,2559,308],{"class":276},[35,2561,496],{"class":272},[35,2563,2564],{"class":45},"\"   ",[35,2566,503],{"class":502},[35,2568,2569],{"class":276},"acct",[35,2571,512],{"class":502},[35,2573,973],{"class":502},[35,2575,2576],{"class":276},"acct_name",[35,2578,512],{"class":502},[35,2580,499],{"class":45},[35,2582,357],{"class":276},[35,2584,2586,2589,2591,2593,2595,2598,2600,2602,2604,2606,2608,2610],{"class":37,"line":2585},32,[35,2587,2588],{"class":272},"            for",[35,2590,2418],{"class":276},[35,2592,1352],{"class":272},[35,2594,2423],{"class":394},[35,2596,2597],{"class":276},"(months[:",[35,2599,382],{"class":272},[35,2601,763],{"class":394},[35,2603,766],{"class":276},[35,2605,2429],{"class":311},[35,2607,299],{"class":272},[35,2609,763],{"class":394},[35,2611,2436],{"class":276},[35,2613,2615,2618,2620],{"class":37,"line":2614},33,[35,2616,2617],{"class":276},"                ws.write_number(row, c, ",[35,2619,2445],{"class":394},[35,2621,2622],{"class":276},"(values[m]), acct_fmt)\n",[35,2624,2626,2629,2631,2634,2636,2639,2641,2643,2645,2647,2649,2652,2654,2657,2659,2662,2664,2666,2669,2672,2674,2676,2678,2680,2683],{"class":37,"line":2625},34,[35,2627,2628],{"class":276},"            ws.write_formula(row, ",[35,2630,506],{"class":394},[35,2632,2633],{"class":276},"(months), ",[35,2635,496],{"class":272},[35,2637,2638],{"class":45},"\"=SUM(B",[35,2640,503],{"class":502},[35,2642,2330],{"class":276},[35,2644,1414],{"class":272},[35,2646,2532],{"class":394},[35,2648,512],{"class":502},[35,2650,2651],{"class":45},":",[35,2653,503],{"class":502},[35,2655,2656],{"class":276},"col(",[35,2658,506],{"class":394},[35,2660,2661],{"class":276},"(months) ",[35,2663,382],{"class":272},[35,2665,2532],{"class":394},[35,2667,2668],{"class":276},")",[35,2670,2671],{"class":502},"}{",[35,2673,2330],{"class":276},[35,2675,1414],{"class":272},[35,2677,2532],{"class":394},[35,2679,512],{"class":502},[35,2681,2682],{"class":45},")\"",[35,2684,2685],{"class":276},", acct_fmt)\n",[35,2687,2689,2692,2694,2696,2698,2701,2704,2706,2708,2710,2713,2715,2717],{"class":37,"line":2688},35,[35,2690,2691],{"class":276},"            ws.set_row(row, ",[35,2693,698],{"class":394},[35,2695,308],{"class":276},[35,2697,698],{"class":394},[35,2699,2700],{"class":276},", {",[35,2702,2703],{"class":45},"\"level\"",[35,2705,2069],{"class":276},[35,2707,763],{"class":394},[35,2709,308],{"class":276},[35,2711,2712],{"class":45},"\"hidden\"",[35,2714,2069],{"class":276},[35,2716,2099],{"class":394},[35,2718,1437],{"class":276},[35,2720,2722,2725,2727],{"class":37,"line":2721},36,[35,2723,2724],{"class":276},"            row ",[35,2726,2467],{"class":272},[35,2728,2470],{"class":394},[35,2730,2732,2735,2737,2739,2741,2743,2746,2748,2750],{"class":37,"line":2731},37,[35,2733,2734],{"class":276},"        ws.set_row(cell[name], ",[35,2736,698],{"class":394},[35,2738,308],{"class":276},[35,2740,698],{"class":394},[35,2742,2700],{"class":276},[35,2744,2745],{"class":45},"\"collapsed\"",[35,2747,2069],{"class":276},[35,2749,2099],{"class":394},[35,2751,1437],{"class":276},[35,2753,2755],{"class":37,"line":2754},38,[35,2756,290],{"emptyLinePlaceholder":289},[35,2758,2760,2762,2765],{"class":37,"line":2759},39,[35,2761,2358],{"class":272},[35,2763,2764],{"class":2361}," write_total",[35,2766,2767],{"class":276},"(name, terms):\n",[35,2769,2771],{"class":37,"line":2770},40,[35,2772,2773],{"class":45},"    \"\"\"terms: (sign, line) pairs, e.g. [(\"+\", \"Revenue\"), (\"+\", \"Discounts\")].\"\"\"\n",[35,2775,2777,2779],{"class":37,"line":2776},41,[35,2778,2397],{"class":272},[35,2780,2400],{"class":276},[35,2782,2784,2786,2788],{"class":37,"line":2783},42,[35,2785,2405],{"class":276},[35,2787,93],{"class":394},[35,2789,2790],{"class":276},", name, sub)\n",[35,2792,2794,2796,2799,2801,2804,2806,2808,2810,2812,2814,2816,2818],{"class":37,"line":2793},43,[35,2795,2415],{"class":272},[35,2797,2798],{"class":276}," c ",[35,2800,1352],{"class":272},[35,2802,2803],{"class":394}," range",[35,2805,493],{"class":276},[35,2807,763],{"class":394},[35,2809,308],{"class":276},[35,2811,506],{"class":394},[35,2813,2661],{"class":276},[35,2815,1414],{"class":272},[35,2817,2532],{"class":394},[35,2819,2436],{"class":276},[35,2821,2823,2826,2828,2831,2834,2836,2838,2840,2843,2845,2848,2850,2853,2855,2857,2859,2861,2863,2866,2868],{"class":37,"line":2822},44,[35,2824,2825],{"class":276},"        formula ",[35,2827,299],{"class":272},[35,2829,2830],{"class":45}," \"\"",[35,2832,2833],{"class":276},".join(",[35,2835,496],{"class":272},[35,2837,499],{"class":45},[35,2839,503],{"class":502},[35,2841,2842],{"class":276},"sign",[35,2844,2671],{"class":502},[35,2846,2847],{"class":276},"col(c)",[35,2849,2671],{"class":502},[35,2851,2852],{"class":276},"cell[line] ",[35,2854,1414],{"class":272},[35,2856,2532],{"class":394},[35,2858,512],{"class":502},[35,2860,499],{"class":45},[35,2862,2318],{"class":272},[35,2864,2865],{"class":276}," sign, line ",[35,2867,1352],{"class":272},[35,2869,2870],{"class":276}," terms)\n",[35,2872,2874,2877,2880,2883,2886,2889],{"class":37,"line":2873},45,[35,2875,2876],{"class":276},"        ws.write_formula(row, c, ",[35,2878,2879],{"class":45},"\"=\"",[35,2881,2882],{"class":272}," +",[35,2884,2885],{"class":276}," formula.lstrip(",[35,2887,2888],{"class":45},"\"+\"",[35,2890,2891],{"class":276},"), sub)\n",[35,2893,2895,2897,2899],{"class":37,"line":2894},46,[35,2896,2454],{"class":276},[35,2898,299],{"class":272},[35,2900,2400],{"class":276},[35,2902,2904,2906,2908],{"class":37,"line":2903},47,[35,2905,2464],{"class":276},[35,2907,2467],{"class":272},[35,2909,2910],{"class":394}," 2\n",[35,2912,2914],{"class":37,"line":2913},48,[35,2915,290],{"emptyLinePlaceholder":289},[35,2917,2919,2922,2924,2927,2929],{"class":37,"line":2918},49,[35,2920,2921],{"class":276},"write_line(",[35,2923,688],{"class":45},[35,2925,2926],{"class":276},"); write_line(",[35,2928,693],{"class":45},[35,2930,357],{"class":276},[35,2932,2934,2937,2939,2942,2944,2946,2948,2951,2953,2955,2957],{"class":37,"line":2933},50,[35,2935,2936],{"class":276},"write_total(",[35,2938,1455],{"class":45},[35,2940,2941],{"class":276},", [(",[35,2943,2888],{"class":45},[35,2945,308],{"class":276},[35,2947,688],{"class":45},[35,2949,2950],{"class":276},"), (",[35,2952,2888],{"class":45},[35,2954,308],{"class":276},[35,2956,693],{"class":45},[35,2958,2959],{"class":276},")])\n",[35,2961,2963,2965,2967],{"class":37,"line":2962},51,[35,2964,2921],{"class":276},[35,2966,703],{"class":45},[35,2968,357],{"class":276},[35,2970,2972,2974,2976,2978,2980,2982,2984,2986,2989,2991,2993],{"class":37,"line":2971},52,[35,2973,2936],{"class":276},[35,2975,1445],{"class":45},[35,2977,2941],{"class":276},[35,2979,2888],{"class":45},[35,2981,308],{"class":276},[35,2983,1455],{"class":45},[35,2985,2950],{"class":276},[35,2987,2988],{"class":45},"\"-\"",[35,2990,308],{"class":276},[35,2992,703],{"class":45},[35,2994,2959],{"class":276},[35,2996,2998,3001,3003,3005,3007,3009,3011,3013,3015],{"class":37,"line":2997},53,[35,2999,3000],{"class":276},"opex ",[35,3002,299],{"class":272},[35,3004,615],{"class":276},[35,3006,708],{"class":45},[35,3008,308],{"class":276},[35,3010,713],{"class":45},[35,3012,308],{"class":276},[35,3014,1488],{"class":45},[35,3016,676],{"class":276},[35,3018,3020,3022,3024,3026],{"class":37,"line":3019},54,[35,3021,1470],{"class":272},[35,3023,1473],{"class":276},[35,3025,1352],{"class":272},[35,3027,3028],{"class":276}," opex:\n",[35,3030,3032],{"class":37,"line":3031},55,[35,3033,3034],{"class":276},"    write_line(line)\n",[35,3036,3038,3040,3042,3044,3046,3048,3050,3053,3055,3058,3060,3063,3065,3067,3069],{"class":37,"line":3037},56,[35,3039,2936],{"class":276},[35,3041,1510],{"class":45},[35,3043,2941],{"class":276},[35,3045,2888],{"class":45},[35,3047,308],{"class":276},[35,3049,1445],{"class":45},[35,3051,3052],{"class":276},")] ",[35,3054,1414],{"class":272},[35,3056,3057],{"class":276}," [(",[35,3059,2988],{"class":45},[35,3061,3062],{"class":276},", line) ",[35,3064,1470],{"class":272},[35,3066,1473],{"class":276},[35,3068,1352],{"class":272},[35,3070,3071],{"class":276}," opex])\n",[35,3073,3075,3077,3079,3081,3083],{"class":37,"line":3074},57,[35,3076,2921],{"class":276},[35,3078,726],{"class":45},[35,3080,2926],{"class":276},[35,3082,731],{"class":45},[35,3084,357],{"class":276},[35,3086,3088,3090,3092,3094,3096,3098,3100,3102,3104,3106,3108,3110,3112,3114,3116],{"class":37,"line":3087},58,[35,3089,2936],{"class":276},[35,3091,1589],{"class":45},[35,3093,2941],{"class":276},[35,3095,2888],{"class":45},[35,3097,308],{"class":276},[35,3099,1510],{"class":45},[35,3101,2950],{"class":276},[35,3103,2888],{"class":45},[35,3105,308],{"class":276},[35,3107,726],{"class":45},[35,3109,2950],{"class":276},[35,3111,2988],{"class":45},[35,3113,308],{"class":276},[35,3115,731],{"class":45},[35,3117,2959],{"class":276},[35,3119,3121,3123,3125],{"class":37,"line":3120},59,[35,3122,2921],{"class":276},[35,3124,740],{"class":45},[35,3126,357],{"class":276},[35,3128,3130,3132,3134,3136,3138,3140,3142,3144,3146,3148,3150],{"class":37,"line":3129},60,[35,3131,2936],{"class":276},[35,3133,1640],{"class":45},[35,3135,2941],{"class":276},[35,3137,2888],{"class":45},[35,3139,308],{"class":276},[35,3141,1589],{"class":45},[35,3143,2950],{"class":276},[35,3145,2988],{"class":45},[35,3147,308],{"class":276},[35,3149,740],{"class":45},[35,3151,2959],{"class":276},[35,3153,3155],{"class":37,"line":3154},61,[35,3156,290],{"emptyLinePlaceholder":289},[35,3158,3160,3162,3164,3166,3168,3170,3172,3174,3176,3178],{"class":37,"line":3159},62,[35,3161,1470],{"class":272},[35,3163,1349],{"class":276},[35,3165,1352],{"class":272},[35,3167,615],{"class":276},[35,3169,1445],{"class":45},[35,3171,308],{"class":276},[35,3173,1510],{"class":45},[35,3175,308],{"class":276},[35,3177,1640],{"class":45},[35,3179,1491],{"class":276},[35,3181,3183,3185,3187,3189,3191,3193,3195,3198,3200,3203],{"class":37,"line":3182},63,[35,3184,2405],{"class":276},[35,3186,93],{"class":394},[35,3188,308],{"class":276},[35,3190,496],{"class":272},[35,3192,499],{"class":45},[35,3194,503],{"class":502},[35,3196,3197],{"class":276},"name",[35,3199,512],{"class":502},[35,3201,3202],{"class":45}," margin\"",[35,3204,3205],{"class":276},", pct)\n",[35,3207,3209,3211,3213,3215,3217,3219,3221,3223,3225,3227,3229,3231],{"class":37,"line":3208},64,[35,3210,2415],{"class":272},[35,3212,2798],{"class":276},[35,3214,1352],{"class":272},[35,3216,2803],{"class":394},[35,3218,493],{"class":276},[35,3220,763],{"class":394},[35,3222,308],{"class":276},[35,3224,506],{"class":394},[35,3226,2661],{"class":276},[35,3228,1414],{"class":272},[35,3230,2532],{"class":394},[35,3232,2436],{"class":276},[35,3234,3236,3238,3240,3243,3245,3247,3249,3252,3254,3256,3258,3260,3262,3264,3266,3269,3272,3274,3276,3278,3280,3282],{"class":37,"line":3235},65,[35,3237,2876],{"class":276},[35,3239,496],{"class":272},[35,3241,3242],{"class":45},"\"=",[35,3244,503],{"class":502},[35,3246,2847],{"class":276},[35,3248,2671],{"class":502},[35,3250,3251],{"class":276},"cell[name] ",[35,3253,1414],{"class":272},[35,3255,2532],{"class":394},[35,3257,512],{"class":502},[35,3259,1723],{"class":45},[35,3261,503],{"class":502},[35,3263,2847],{"class":276},[35,3265,2671],{"class":502},[35,3267,3268],{"class":276},"cell[",[35,3270,3271],{"class":45},"'Net revenue'",[35,3273,379],{"class":276},[35,3275,1414],{"class":272},[35,3277,2532],{"class":394},[35,3279,512],{"class":502},[35,3281,499],{"class":45},[35,3283,3205],{"class":276},[35,3285,3287,3289,3291],{"class":37,"line":3286},66,[35,3288,2464],{"class":276},[35,3290,2467],{"class":272},[35,3292,2470],{"class":394},[35,3294,3296],{"class":37,"line":3295},67,[35,3297,290],{"emptyLinePlaceholder":289},[35,3299,3301,3304,3306,3308,3310,3312,3315],{"class":37,"line":3300},68,[35,3302,3303],{"class":276},"ws.set_column(",[35,3305,93],{"class":394},[35,3307,308],{"class":276},[35,3309,93],{"class":394},[35,3311,308],{"class":276},[35,3313,3314],{"class":394},"30",[35,3316,357],{"class":276},[35,3318,3320,3322,3324,3326,3328,3330,3332],{"class":37,"line":3319},69,[35,3321,3303],{"class":276},[35,3323,763],{"class":394},[35,3325,308],{"class":276},[35,3327,506],{"class":394},[35,3329,2633],{"class":276},[35,3331,2272],{"class":394},[35,3333,357],{"class":276},[35,3335,3337,3340,3342,3344,3346,3348,3350,3352,3354,3357],{"class":37,"line":3336},70,[35,3338,3339],{"class":276},"ws.outline_settings(",[35,3341,2099],{"class":394},[35,3343,308],{"class":276},[35,3345,798],{"class":394},[35,3347,308],{"class":276},[35,3349,2099],{"class":394},[35,3351,308],{"class":276},[35,3353,798],{"class":394},[35,3355,3356],{"class":276},")   ",[35,3358,3359],{"class":2348},"# summary rows above the detail\n",[35,3361,3363,3366,3369,3371,3373],{"class":37,"line":3362},71,[35,3364,3365],{"class":276},"ws.freeze_panes(",[35,3367,3368],{"class":394},"3",[35,3370,308],{"class":276},[35,3372,763],{"class":394},[35,3374,357],{"class":276},[35,3376,3378],{"class":37,"line":3377},72,[35,3379,3380],{"class":276},"wb.close()\n",[35,3382,3384,3386,3388,3390,3393,3395,3398,3400,3403,3405,3407,3409,3411,3413,3415,3417,3419],{"class":37,"line":3383},73,[35,3385,490],{"class":394},[35,3387,493],{"class":276},[35,3389,496],{"class":272},[35,3391,3392],{"class":45},"\"wrote ",[35,3394,503],{"class":502},[35,3396,3397],{"class":276},"row",[35,3399,512],{"class":502},[35,3401,3402],{"class":45}," rows; net profit on row ",[35,3404,503],{"class":502},[35,3406,3268],{"class":276},[35,3408,1947],{"class":45},[35,3410,379],{"class":276},[35,3412,1414],{"class":272},[35,3414,2532],{"class":394},[35,3416,512],{"class":502},[35,3418,499],{"class":45},[35,3420,357],{"class":276},[10,3422,3423,3424,3427,3428,3432],{},"The account rows are written hidden at outline level 1 and the line above them is marked collapsed, so the statement opens showing only its summary lines; the plus buttons in the left margin open each line. ",[32,3425,3426],{},"outline_settings"," puts the buttons on the line above the detail, which is where a reader of a P&L expects them. ",[14,3429,3431],{"href":3430},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fgroup-and-outline-rows-in-excel-with-openpyxl\u002F","Group and Outline Rows in Excel with openpyxl"," covers outlines in more depth.",[20,3434,3436],{"id":3435},"step-6-recalculate-and-compare","Step 6 — Recalculate and compare",[10,3438,3439],{},"The subtotal formulas must agree with the pandas statement. Drop xlsxwriter's cached zeros with an openpyxl save, recalculate with LibreOffice, and compare the net profit row:",[25,3441,3443],{"className":263,"code":3442,"language":265,"meta":30,"style":30},"from openpyxl import load_workbook\nload_workbook(\"pl_q3_2026.xlsx\").save(\"pl_nocache.xlsx\")\n",[32,3444,3445,3458],{"__ignoreMap":30},[35,3446,3447,3450,3453,3455],{"class":37,"line":38},[35,3448,3449],{"class":272},"from",[35,3451,3452],{"class":276}," openpyxl ",[35,3454,273],{"class":272},[35,3456,3457],{"class":276}," load_workbook\n",[35,3459,3460,3463,3465,3468,3471],{"class":37,"line":286},[35,3461,3462],{"class":276},"load_workbook(",[35,3464,2036],{"class":45},[35,3466,3467],{"class":276},").save(",[35,3469,3470],{"class":45},"\"pl_nocache.xlsx\"",[35,3472,357],{"class":276},[25,3474,3476],{"className":27,"code":3475,"language":29,"meta":30,"style":30},"soffice -env:UserInstallation=file:\u002F\u002F$PWD\u002Flo_profile --headless --convert-to xlsx --outdir recalculated pl_nocache.xlsx > \u002Fdev\u002Fnull 2>&1\nls recalculated\n",[32,3477,3478,3519],{"__ignoreMap":30},[35,3479,3480,3483,3486,3489,3492,3495,3498,3501,3504,3507,3510,3513,3516],{"class":37,"line":38},[35,3481,3482],{"class":41},"soffice",[35,3484,3485],{"class":394}," -env:UserInstallation=file:\u002F\u002F",[35,3487,3488],{"class":276},"$PWD",[35,3490,3491],{"class":394},"\u002Flo_profile",[35,3493,3494],{"class":394}," --headless",[35,3496,3497],{"class":394}," --convert-to",[35,3499,3500],{"class":45}," xlsx",[35,3502,3503],{"class":394}," --outdir",[35,3505,3506],{"class":45}," recalculated",[35,3508,3509],{"class":45}," pl_nocache.xlsx",[35,3511,3512],{"class":272}," >",[35,3514,3515],{"class":45}," \u002Fdev\u002Fnull",[35,3517,3518],{"class":272}," 2>&1\n",[35,3520,3521,3524],{"class":37,"line":286},[35,3522,3523],{"class":41},"ls",[35,3525,3526],{"class":45}," recalculated\n",[25,3528,3530],{"className":263,"code":3529,"language":265,"meta":30,"style":30},"ws = load_workbook(\"recalculated\u002Fpl_nocache.xlsx\", data_only=True)[\"P&L\"]\nr = cell[\"Net profit\"] + 1\nsheet_values = [ws.cell(row=r, column=c).value for c in range(2, len(months) + 2)]\nprint([round(v, 2) for v in sheet_values])\nprint(\"matches pandas:\", all(abs(a - b) \u003C 0.005 for a, b in zip(sheet_values, statement.loc[\"Net profit\"])))\n",[32,3531,3532,3560,3578,3629,3656],{"__ignoreMap":30},[35,3533,3534,3536,3538,3541,3544,3546,3549,3551,3553,3556,3558],{"class":37,"line":38},[35,3535,2043],{"class":276},[35,3537,299],{"class":272},[35,3539,3540],{"class":276}," load_workbook(",[35,3542,3543],{"class":45},"\"recalculated\u002Fpl_nocache.xlsx\"",[35,3545,308],{"class":276},[35,3547,3548],{"class":311},"data_only",[35,3550,299],{"class":272},[35,3552,2099],{"class":394},[35,3554,3555],{"class":276},")[",[35,3557,2051],{"class":45},[35,3559,676],{"class":276},[35,3561,3562,3565,3567,3570,3572,3574,3576],{"class":37,"line":286},[35,3563,3564],{"class":276},"r ",[35,3566,299],{"class":272},[35,3568,3569],{"class":276}," cell[",[35,3571,1640],{"class":45},[35,3573,379],{"class":276},[35,3575,1414],{"class":272},[35,3577,2470],{"class":394},[35,3579,3580,3583,3585,3588,3590,3592,3595,3598,3600,3603,3605,3607,3609,3611,3613,3615,3617,3619,3621,3623,3626],{"class":37,"line":293},[35,3581,3582],{"class":276},"sheet_values ",[35,3584,299],{"class":272},[35,3586,3587],{"class":276}," [ws.cell(",[35,3589,3397],{"class":311},[35,3591,299],{"class":272},[35,3593,3594],{"class":276},"r, ",[35,3596,3597],{"class":311},"column",[35,3599,299],{"class":272},[35,3601,3602],{"class":276},"c).value ",[35,3604,1470],{"class":272},[35,3606,2798],{"class":276},[35,3608,1352],{"class":272},[35,3610,2803],{"class":394},[35,3612,493],{"class":276},[35,3614,395],{"class":394},[35,3616,308],{"class":276},[35,3618,506],{"class":394},[35,3620,2661],{"class":276},[35,3622,1414],{"class":272},[35,3624,3625],{"class":394}," 2",[35,3627,3628],{"class":276},")]\n",[35,3630,3631,3633,3636,3639,3642,3644,3646,3648,3651,3653],{"class":37,"line":336},[35,3632,490],{"class":394},[35,3634,3635],{"class":276},"([",[35,3637,3638],{"class":394},"round",[35,3640,3641],{"class":276},"(v, ",[35,3643,395],{"class":394},[35,3645,1411],{"class":276},[35,3647,1470],{"class":272},[35,3649,3650],{"class":276}," v ",[35,3652,1352],{"class":272},[35,3654,3655],{"class":276}," sheet_values])\n",[35,3657,3658,3660,3662,3665,3667,3670,3672,3675,3678,3680,3683,3686,3689,3691,3694,3696,3699,3702,3704],{"class":37,"line":360},[35,3659,490],{"class":394},[35,3661,493],{"class":276},[35,3663,3664],{"class":45},"\"matches pandas:\"",[35,3666,308],{"class":276},[35,3668,3669],{"class":394},"all",[35,3671,493],{"class":276},[35,3673,3674],{"class":394},"abs",[35,3676,3677],{"class":276},"(a ",[35,3679,382],{"class":272},[35,3681,3682],{"class":276}," b) ",[35,3684,3685],{"class":272},"\u003C",[35,3687,3688],{"class":394}," 0.005",[35,3690,2318],{"class":272},[35,3692,3693],{"class":276}," a, b ",[35,3695,1352],{"class":272},[35,3697,3698],{"class":394}," zip",[35,3700,3701],{"class":276},"(sheet_values, statement.loc[",[35,3703,1640],{"class":45},[35,3705,3706],{"class":276},"])))\n",[20,3708,3710],{"id":3709},"common-pitfalls","Common pitfalls",[3712,3713,3714,3722,3728,3734,3740,3750],"ul",{},[3715,3716,3717,3721],"li",{},[3718,3719,3720],"strong",{},"Skipping the balance check."," An export cut off mid-journal still produces a plausible-looking P&L.",[3715,3723,3724,3727],{},[3718,3725,3726],{},"Mixing signs."," Decide the convention once — revenue and costs both positive — and apply it by line, not by account.",[3715,3729,3730,3733],{},[3718,3731,3732],{},"Contra accounts."," Discounts, returns and allowances reduce revenue; keep them on the revenue side with a negative sign rather than as a cost.",[3715,3735,3736,3739],{},[3718,3737,3738],{},"Mapping in code."," Account groupings change; keep them in a table finance can edit, and fail on unmapped accounts.",[3715,3741,3742,3745,3746,3749],{},[3718,3743,3744],{},"Months with no activity."," Pivot with ",[32,3747,3748],{},"fill_value=0"," so a quiet month shows 0 rather than disappearing.",[3715,3751,3752,3755],{},[3718,3753,3754],{},"Rounding."," Round only for display; subtotals of rounded lines drift from the ledger total.",[20,3757,3759],{"id":3758},"conclusion","Conclusion",[10,3761,3762,3763,3765],{},"Check the ledger balances and every account is known, map account ranges to statement lines with a table and ",[32,3764,599],{},", sign amounts so revenue and costs are both positive, and compute net revenue, gross, operating and net profit with margins. Reconcile net profit to the raw ledger independently. Write summary lines with collapsible account detail and subtotal formulas, then recalculate once to confirm the workbook and pandas agree.",[20,3767,3769],{"id":3768},"frequently-asked-questions","Frequently asked questions",[10,3771,3772,3775],{},[3718,3773,3774],{},"How do I turn debits and credits into P&L amounts?","\nFor revenue and other income use credit minus debit; for costs use debit minus credit. Both then read as positive numbers, and profit is revenue minus costs.",[10,3777,3778,3781],{},[3718,3779,3780],{},"Where should the account-to-line mapping live?","\nIn a table — a sheet in the workbook or a small CSV — with account ranges or individual accounts and the line they belong to. The script reads it and fails if any P&L account is not mapped.",[10,3783,3784,3787],{},[3718,3785,3786],{},"How do I show account detail under each line in Excel?","\nWrite the account rows beneath each line with an outline level of 1 and hidden set, and put summary rows above the detail. Readers expand a line with the plus button in the margin.",[10,3789,3790,3793],{},[3718,3791,3792],{},"Why does my P&L not match the accounting system?","\nCheck the export's date range and whether it includes closing or adjusting journals, look for unmapped accounts, and reconcile net profit to the sum of credit minus debit across all P&L accounts.",[20,3795,3797],{"id":3796},"related","Related",[3712,3799,3800,3806,3813,3820,3827],{},[3715,3801,3802,3803,3805],{},"Up one level: ",[14,3804,17],{"href":16}," — the rest of the finance reports.",[3715,3807,3808,3812],{},[14,3809,3811],{"href":3810},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report\u002F","Build a Budget vs Actual Variance Report"," — comparing the statement with the plan.",[3715,3814,3815,3819],{},[14,3816,3818],{"href":3817},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments\u002F","Allocate Shared Costs Across Departments"," — P&Ls by department.",[3715,3821,3822,3826],{},[14,3823,3825],{"href":3824},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report\u002F","Build a Bank Reconciliation Report"," — another ledger check.",[3715,3828,3829,3831],{},[14,3830,3431],{"href":3430}," — outlines and collapsible detail.",[3833,3834,3835],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":30,"searchDepth":286,"depth":286,"links":3837},[3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848],{"id":22,"depth":286,"text":23},{"id":256,"depth":286,"text":257},{"id":592,"depth":286,"text":593},{"id":949,"depth":286,"text":950},{"id":1301,"depth":286,"text":1302},{"id":1987,"depth":286,"text":1988},{"id":3435,"depth":286,"text":3436},{"id":3709,"depth":286,"text":3710},{"id":3758,"depth":286,"text":3759},{"id":3768,"depth":286,"text":3769},{"id":3796,"depth":286,"text":3797},"2026-09-29","Turn a general ledger export into a monthly profit and loss statement in Excel: check the ledger balances, map account numbers to P&L lines with a chart of accounts, flip signs so revenue and costs read naturally, add gross, operating and net profit with margins, and write a workbook with collapsible account detail and live subtotal formulas.","md",[3853,3855,3857,3859],{"q":3774,"a":3854},"For revenue and other income use credit minus debit; for costs use debit minus credit. Both then read as positive numbers, and profit is revenue minus costs.",{"q":3780,"a":3856},"In a table — a sheet in the workbook or a small CSV — with account ranges or individual accounts and the line they belong to. The script reads it and fails if any P&L account is not mapped.",{"q":3786,"a":3858},"Write the account rows beneath each line with an outline level of 1 and hidden set, and put summary rows above the detail. Readers expand a line with the plus button in the margin.",{"q":3792,"a":3860},"Check the export's date range and whether it includes closing or adjusting journals, look for unmapped accounts, and reconcile net profit to the sum of credit minus debit across all P&L accounts.","build a profit and loss statement from a general ledger",null,{"breadcrumb":3864},[3865,3867,3870],{"name":3866,"item":1723},"Home",{"name":3868,"item":3869},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":3871,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger",{"title":3874,"description":3875},"Profit and Loss Statement from a General Ledger with Python","Build a monthly P&L from GL journal lines with pandas: validate debits equal credits, map account ranges to lines with pd.cut, sign conventions, gross\u002Foperating\u002Fnet profit and margins, grouped account rows and SUM formulas in Excel.","build-a-profit-and-loss-statement-from-a-general-ledger","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-profit-and-loss-statement-from-a-general-ledger\u002Findex",[3879,3880],"report-layouts","pivots-and-summaries","how-to","yTW8IvftYFAoaYEOf_626pr4JfOKDfxmI5-o1UIHh_s",[3884,3888],{"title":3885,"path":3886,"stem":3887,"children":-1},"Build a Production Yield and Scrap Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-production-yield-and-scrap-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-production-yield-and-scrap-report\u002Findex",{"title":3889,"path":3890,"stem":3891,"children":-1},"Build a Project Budget Burn Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-project-budget-burn-report\u002Findex",[3893,3895],{"path":3894,"title":3871},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":3896,"title":3868},"\u002Fautomating-reporting-workflows",[],[3899,3903,3906,3909,3912,3915],{"path":3900,"title":3901,"kind":3902},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":3904,"title":3905,"kind":3881},"\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":3907,"title":3908,"kind":3881},"\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":3910,"title":3911,"kind":3881},"\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":3913,"title":3914,"kind":3881},"\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":3916,"title":3917,"kind":3881},"\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",1790972126503]