[{"data":1,"prerenderedAt":2537},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks":2502,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks":2511,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks":2516,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks":2517},{"id":4,"title":5,"body":6,"dateModified":2467,"datePublished":2467,"description":2468,"extension":2469,"faq":2470,"keyword":2479,"kind":2480,"meta":2481,"navigation":434,"path":2490,"seo":2491,"slug":2494,"stem":2495,"topics":2496,"type":2500,"__hash__":2501},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks\u002Findex.md","Consolidate Timesheets from Many Workbooks with Python",{"type":7,"value":8,"toc":2454},"minimark",[9,19,27,328,333,361,372,376,379,852,861,865,868,1111,1114,1118,1121,1678,1681,1811,1943,1947,1950,2170,2177,2181,2184,2321,2324,2328,2372,2376,2384,2388,2391,2395,2401,2407,2413,2419,2423,2450],[10,11,12,13,18],"p",{},"A timesheet template works well for the people filling it in: their name and the week at the top, projects down the side, days across, hours in the grid. It works badly for whoever has to add up thirty of them on Monday morning. The consolidation is mechanical — open each file, pick out the name, week and grid, stack the results — but the real work is in what goes wrong: someone has not submitted, someone sent a corrected version, a project code is mistyped, a day has fourteen hours. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",", builds a consolidation that handles all of those and produces hours by project and person.",[10,20,21,22,26],{},"For combining files that are already simple tables, ",[14,23,25],{"href":24},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcombine-multiple-excel-files-into-one-python\u002F","Combine Multiple Excel Files into One with Python"," is the simpler starting point; this page is about template-shaped files with header fields and a grid.",[28,29,37,38,37,42,37,46,37,54,37,63,37,67,37,74,37,78,37,82,37,86,37,90,37,94,37,98,37,102,37,107,37,109,37,113,37,115,37,118,37,120,37,123,37,125,37,128,37,131,37,135,37,137,37,141,37,143,37,146,37,148,37,150,37,152,37,155,37,158,37,162,37,164,37,167,37,169,37,171,37,173,37,175,37,178,37,182,37,185,37,188,37,191,37,194,37,197,37,201,37,204,37,206,37,209,37,211,37,214,37,216,37,218,37,220,37,223,37,225,37,228,37,230,37,232,37,234,37,237,37,239,37,241,37,243,37,245,37,247,37,249,37,251,37,253,37,255,37,257,37,259,37,261,37,263,37,265,37,267,37,269,37,272,37,275,37,277,37,279,37,281,37,283,37,285,37,287,37,289,37,291,37,297,37,301,37,309,37,314,37,322],"svg",{"viewBox":30,"role":31,"ariaLabelledBy":32,"xmlns":35,"style":36},"46 0 668 192","img",[33,34],"ts-melt-t","ts-melt-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:668px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[39,40,41],"title",{"id":33},"One timesheet grid becomes rows of person, project, day and hours",[43,44,45],"desc",{"id":34},"Left, Ana's timesheet grid with projects P-100 and P-200 down the side and days Monday to Friday across. Right, the same data unpivoted: one row per person, project and day with the hours, such as Ana, P-100, Mon, 8 and Ana, P-200, Wed, 4.",[47,48],"rect",{"x":49,"y":50,"width":51,"height":52,"fill":53},"46","0","668","192","#ffffff",[47,55],{"x":56,"y":57,"width":58,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"62.4","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[47,64],{"x":65,"y":57,"width":66,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"94.4","68.0",[68,69,73],"text",{"x":70,"y":71,"style":72},"128.4","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[47,75],{"x":76,"y":57,"width":77,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"162.4","51.3",[68,79,81],{"x":80,"y":71,"style":72},"188.0","B",[47,83],{"x":84,"y":57,"width":85,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"213.7","47.8",[68,87,89],{"x":88,"y":71,"style":72},"237.6","C",[47,91],{"x":92,"y":57,"width":93,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"261.5","52.5",[68,95,97],{"x":96,"y":71,"style":72},"287.7","D",[47,99],{"x":56,"y":100,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"64.0","28.0",[68,103,106],{"x":104,"y":105,"style":72},"78.4","83.0","1",[47,108],{"x":65,"y":100,"width":66,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,110,112],{"x":70,"y":105,"style":111},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Project",[47,114],{"x":76,"y":100,"width":77,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,116,117],{"x":80,"y":105,"style":111},"Mon",[47,119],{"x":84,"y":100,"width":85,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,121,122],{"x":88,"y":105,"style":111},"Tue",[47,124],{"x":92,"y":100,"width":93,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,126,127],{"x":96,"y":105,"style":111},"Wed",[47,129],{"x":56,"y":130,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"92.0",[68,132,134],{"x":104,"y":133,"style":72},"111.0","2",[47,136],{"x":65,"y":130,"width":66,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,138,140],{"x":70,"y":133,"style":139},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","P-100",[47,142],{"x":76,"y":130,"width":77,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,144,145],{"x":80,"y":133,"style":139},"8",[47,147],{"x":84,"y":130,"width":85,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,149,145],{"x":88,"y":133,"style":139},[47,151],{"x":92,"y":130,"width":93,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,153,154],{"x":96,"y":133,"style":139},"4",[47,156],{"x":56,"y":157,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"120.0",[68,159,161],{"x":104,"y":160,"style":72},"139.0","3",[47,163],{"x":65,"y":157,"width":66,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,165,166],{"x":70,"y":160,"style":139},"P-200",[47,168],{"x":76,"y":157,"width":77,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[47,170],{"x":84,"y":157,"width":85,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[47,172],{"x":92,"y":157,"width":93,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,174,154],{"x":96,"y":160,"style":139},[47,176],{"x":177,"y":57,"width":58,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"424.0",[47,179],{"x":180,"y":57,"width":181,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"456.0","59.9",[68,183,73],{"x":184,"y":71,"style":72},"486.0",[47,186],{"x":187,"y":57,"width":66,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"515.9",[68,189,81],{"x":190,"y":71,"style":72},"549.9",[47,192],{"x":193,"y":57,"width":93,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"583.9",[68,195,89],{"x":196,"y":71,"style":72},"610.1",[47,198],{"x":199,"y":57,"width":200,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"636.4","61.2",[68,202,97],{"x":203,"y":71,"style":72},"667.0",[47,205],{"x":177,"y":100,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},[68,207,106],{"x":208,"y":105,"style":72},"440.0",[47,210],{"x":180,"y":100,"width":181,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,212,213],{"x":184,"y":105,"style":111},"Name",[47,215],{"x":187,"y":100,"width":66,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,217,112],{"x":190,"y":105,"style":111},[47,219],{"x":193,"y":100,"width":93,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,221,222],{"x":196,"y":105,"style":111},"Day",[47,224],{"x":199,"y":100,"width":200,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,226,227],{"x":203,"y":105,"style":111},"Hours",[47,229],{"x":177,"y":130,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},[68,231,134],{"x":208,"y":133,"style":72},[47,233],{"x":180,"y":130,"width":181,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,235,236],{"x":184,"y":133,"style":139},"Ana",[47,238],{"x":187,"y":130,"width":66,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,240,140],{"x":190,"y":133,"style":139},[47,242],{"x":193,"y":130,"width":93,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,244,117],{"x":196,"y":133,"style":139},[47,246],{"x":199,"y":130,"width":200,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,248,145],{"x":203,"y":133,"style":139},[47,250],{"x":177,"y":157,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},[68,252,161],{"x":208,"y":160,"style":72},[47,254],{"x":180,"y":157,"width":181,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,256,236],{"x":184,"y":160,"style":139},[47,258],{"x":187,"y":157,"width":66,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,260,140],{"x":190,"y":160,"style":139},[47,262],{"x":193,"y":157,"width":93,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,264,122],{"x":196,"y":160,"style":139},[47,266],{"x":199,"y":157,"width":200,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,268,145],{"x":203,"y":160,"style":139},[47,270],{"x":177,"y":271,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"148.0",[68,273,154],{"x":208,"y":274,"style":72},"167.0",[47,276],{"x":180,"y":271,"width":181,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,278,236],{"x":184,"y":274,"style":139},[47,280],{"x":187,"y":271,"width":66,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,282,166],{"x":190,"y":274,"style":139},[47,284],{"x":193,"y":271,"width":93,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,286,127],{"x":196,"y":274,"style":139},[47,288],{"x":199,"y":271,"width":200,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,290,154],{"x":203,"y":274,"style":139},[68,292,296],{"x":293,"y":294,"style":295},"188.2","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[68,298,300],{"x":299,"y":294,"style":295},"560.8","after",[302,303],"line",{"x1":304,"y1":305,"x2":306,"y2":305,"stroke":307,"style":308},"324.0","94.0","410.0","var(--brand,#5b5cf0)","stroke-width:2px",[310,311],"polygon",{"points":312,"fill":313},"414.0,94.0 405.0,99.0 405.0,89.0","#5b5cf0",[47,315],{"x":316,"y":317,"width":318,"height":319,"rx":320,"fill":53,"stroke":61,"style":321},"344.4","61.0","49.1","22.0","11","stroke-width:1.2px",[68,323,327],{"x":324,"y":325,"style":326},"369.0","76.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","melt",[329,330,332],"h2",{"id":331},"prerequisites","Prerequisites",[334,335,340],"pre",{"className":336,"code":337,"language":338,"meta":339,"style":339},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[341,342,343],"code",{"__ignoreMap":339},[344,345,347,351,355,358],"span",{"class":302,"line":346},1,[344,348,350],{"class":349},"sMTad","pip",[344,352,354],{"class":353},"srMev"," install",[344,356,357],{"class":353}," pandas",[344,359,360],{"class":353}," openpyxl\n",[10,362,363,364,367,368,371],{},"The ",[341,365,366],{},"timesheets"," folder holds five files for the week ending 27 September 2026: Ana, Ben and Cai submitted once; Dee submitted twice, the second time correcting a 14-hour Tuesday. ",[341,369,370],{},"staff_and_projects.xlsx"," lists five staff with their contracted hours — Eli has not submitted — and the valid project codes with their client and whether they are billable.",[329,373,375],{"id":374},"step-1-read-one-timesheet","Step 1 — Read one timesheet",[10,377,378],{},"Each file has the name in B2, the week ending in B3 and the grid starting at row 5 with a header of Project and the seven days, ending at a Total row. Read the header cells with openpyxl and the grid with pandas, stopping at the Total row:",[334,380,384],{"className":381,"code":382,"language":383,"meta":339,"style":339},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\nimport pandas as pd\nfrom openpyxl import load_workbook\n\nDAYS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n\ndef read_timesheet(path):\n    ws = load_workbook(path, read_only=True, data_only=True)[\"Timesheet\"]\n    name, week = ws[\"B2\"].value, ws[\"B3\"].value\n    rows = list(ws.iter_rows(min_row=5, values_only=True))\n    header, body = rows[0], []\n    for row in rows[1:]:\n        if row[0] in (None, \"Total\"):\n            break\n        body.append(row)\n    grid = pd.DataFrame(body, columns=header)\n    long = grid.melt(id_vars=\"Project\", value_vars=DAYS, var_name=\"Day\", value_name=\"Hours\").dropna(subset=[\"Hours\"])\n    long[\"Hours\"] = long[\"Hours\"].astype(float)\n    return long.assign(Name=name, Week=pd.Timestamp(week), File=Path(path).name)\n\nprint(read_timesheet(\"timesheets\u002Fana_week39.xlsx\").to_string(index=False))\n","python",[341,385,386,402,416,429,436,486,491,504,542,565,599,615,634,664,670,676,695,759,788,822,827],{"__ignoreMap":339},[344,387,388,392,396,399],{"class":302,"line":346},[344,389,391],{"class":390},"s-kum","from",[344,393,395],{"class":394},"skGVy"," pathlib ",[344,397,398],{"class":390},"import",[344,400,401],{"class":394}," Path\n",[344,403,405,407,410,413],{"class":302,"line":404},2,[344,406,398],{"class":390},[344,408,409],{"class":394}," pandas ",[344,411,412],{"class":390},"as",[344,414,415],{"class":394}," pd\n",[344,417,419,421,424,426],{"class":302,"line":418},3,[344,420,391],{"class":390},[344,422,423],{"class":394}," openpyxl ",[344,425,398],{"class":390},[344,427,428],{"class":394}," load_workbook\n",[344,430,432],{"class":302,"line":431},4,[344,433,435],{"emptyLinePlaceholder":434},true,"\n",[344,437,439,443,446,449,452,455,458,460,463,465,468,470,473,475,478,480,483],{"class":302,"line":438},5,[344,440,442],{"class":441},"sP0c6","DAYS",[344,444,445],{"class":390}," =",[344,447,448],{"class":394}," [",[344,450,451],{"class":353},"\"Mon\"",[344,453,454],{"class":394},", ",[344,456,457],{"class":353},"\"Tue\"",[344,459,454],{"class":394},[344,461,462],{"class":353},"\"Wed\"",[344,464,454],{"class":394},[344,466,467],{"class":353},"\"Thu\"",[344,469,454],{"class":394},[344,471,472],{"class":353},"\"Fri\"",[344,474,454],{"class":394},[344,476,477],{"class":353},"\"Sat\"",[344,479,454],{"class":394},[344,481,482],{"class":353},"\"Sun\"",[344,484,485],{"class":394},"]\n",[344,487,489],{"class":302,"line":488},6,[344,490,435],{"emptyLinePlaceholder":434},[344,492,494,497,501],{"class":302,"line":493},7,[344,495,496],{"class":390},"def",[344,498,500],{"class":499},"s_Opv"," read_timesheet",[344,502,503],{"class":394},"(path):\n",[344,505,507,510,513,516,520,522,525,527,530,532,534,537,540],{"class":302,"line":506},8,[344,508,509],{"class":394},"    ws ",[344,511,512],{"class":390},"=",[344,514,515],{"class":394}," load_workbook(path, ",[344,517,519],{"class":518},"sa561","read_only",[344,521,512],{"class":390},[344,523,524],{"class":441},"True",[344,526,454],{"class":394},[344,528,529],{"class":518},"data_only",[344,531,512],{"class":390},[344,533,524],{"class":441},[344,535,536],{"class":394},")[",[344,538,539],{"class":353},"\"Timesheet\"",[344,541,485],{"class":394},[344,543,545,548,550,553,556,559,562],{"class":302,"line":544},9,[344,546,547],{"class":394},"    name, week ",[344,549,512],{"class":390},[344,551,552],{"class":394}," ws[",[344,554,555],{"class":353},"\"B2\"",[344,557,558],{"class":394},"].value, ws[",[344,560,561],{"class":353},"\"B3\"",[344,563,564],{"class":394},"].value\n",[344,566,568,571,573,576,579,582,584,587,589,592,594,596],{"class":302,"line":567},10,[344,569,570],{"class":394},"    rows ",[344,572,512],{"class":390},[344,574,575],{"class":441}," list",[344,577,578],{"class":394},"(ws.iter_rows(",[344,580,581],{"class":518},"min_row",[344,583,512],{"class":390},[344,585,586],{"class":441},"5",[344,588,454],{"class":394},[344,590,591],{"class":518},"values_only",[344,593,512],{"class":390},[344,595,524],{"class":441},[344,597,598],{"class":394},"))\n",[344,600,602,605,607,610,612],{"class":302,"line":601},11,[344,603,604],{"class":394},"    header, body ",[344,606,512],{"class":390},[344,608,609],{"class":394}," rows[",[344,611,50],{"class":441},[344,613,614],{"class":394},"], []\n",[344,616,618,621,624,627,629,631],{"class":302,"line":617},12,[344,619,620],{"class":390},"    for",[344,622,623],{"class":394}," row ",[344,625,626],{"class":390},"in",[344,628,609],{"class":394},[344,630,106],{"class":441},[344,632,633],{"class":394},":]:\n",[344,635,637,640,643,645,648,650,653,656,658,661],{"class":302,"line":636},13,[344,638,639],{"class":390},"        if",[344,641,642],{"class":394}," row[",[344,644,50],{"class":441},[344,646,647],{"class":394},"] ",[344,649,626],{"class":390},[344,651,652],{"class":394}," (",[344,654,655],{"class":441},"None",[344,657,454],{"class":394},[344,659,660],{"class":353},"\"Total\"",[344,662,663],{"class":394},"):\n",[344,665,667],{"class":302,"line":666},14,[344,668,669],{"class":390},"            break\n",[344,671,673],{"class":302,"line":672},15,[344,674,675],{"class":394},"        body.append(row)\n",[344,677,679,682,684,687,690,692],{"class":302,"line":678},16,[344,680,681],{"class":394},"    grid ",[344,683,512],{"class":390},[344,685,686],{"class":394}," pd.DataFrame(body, ",[344,688,689],{"class":518},"columns",[344,691,512],{"class":390},[344,693,694],{"class":394},"header)\n",[344,696,698,701,703,706,709,711,714,716,719,721,723,725,728,730,733,735,738,740,743,746,749,751,754,756],{"class":302,"line":697},17,[344,699,700],{"class":518},"    long",[344,702,445],{"class":390},[344,704,705],{"class":394}," grid.melt(",[344,707,708],{"class":518},"id_vars",[344,710,512],{"class":390},[344,712,713],{"class":353},"\"Project\"",[344,715,454],{"class":394},[344,717,718],{"class":518},"value_vars",[344,720,512],{"class":390},[344,722,442],{"class":441},[344,724,454],{"class":394},[344,726,727],{"class":518},"var_name",[344,729,512],{"class":390},[344,731,732],{"class":353},"\"Day\"",[344,734,454],{"class":394},[344,736,737],{"class":518},"value_name",[344,739,512],{"class":390},[344,741,742],{"class":353},"\"Hours\"",[344,744,745],{"class":394},").dropna(",[344,747,748],{"class":518},"subset",[344,750,512],{"class":390},[344,752,753],{"class":394},"[",[344,755,742],{"class":353},[344,757,758],{"class":394},"])\n",[344,760,762,764,766,768,770,772,775,777,779,782,785],{"class":302,"line":761},18,[344,763,700],{"class":518},[344,765,753],{"class":394},[344,767,742],{"class":353},[344,769,647],{"class":394},[344,771,512],{"class":390},[344,773,774],{"class":518}," long",[344,776,753],{"class":394},[344,778,742],{"class":353},[344,780,781],{"class":394},"].astype(",[344,783,784],{"class":441},"float",[344,786,787],{"class":394},")\n",[344,789,791,794,796,799,801,803,806,809,811,814,817,819],{"class":302,"line":790},19,[344,792,793],{"class":390},"    return",[344,795,774],{"class":518},[344,797,798],{"class":394},".assign(",[344,800,213],{"class":518},[344,802,512],{"class":390},[344,804,805],{"class":394},"name, ",[344,807,808],{"class":518},"Week",[344,810,512],{"class":390},[344,812,813],{"class":394},"pd.Timestamp(week), ",[344,815,816],{"class":518},"File",[344,818,512],{"class":390},[344,820,821],{"class":394},"Path(path).name)\n",[344,823,825],{"class":302,"line":824},20,[344,826,435],{"emptyLinePlaceholder":434},[344,828,830,833,836,839,842,845,847,850],{"class":302,"line":829},21,[344,831,832],{"class":441},"print",[344,834,835],{"class":394},"(read_timesheet(",[344,837,838],{"class":353},"\"timesheets\u002Fana_week39.xlsx\"",[344,840,841],{"class":394},").to_string(",[344,843,844],{"class":518},"index",[344,846,512],{"class":390},[344,848,849],{"class":441},"False",[344,851,598],{"class":394},[10,853,854,856,857,860],{},[341,855,327],{}," turns the seven day columns into a Day column and an Hours column; dropping empty cells leaves only the hours actually entered, and converting to float gives one numeric type whether people typed 8 or 7.5. ",[341,858,859],{},"data_only=True"," matters if the template has formulas, such as a daily total: it reads their cached values rather than the formula text. Stopping at the Total row, instead of reading a fixed number of rows, copes with people adding a project row.",[329,862,864],{"id":863},"step-2-read-every-file-keeping-only-the-latest-version-per-person","Step 2 — Read every file, keeping only the latest version per person",[10,866,867],{},"Read the whole folder. When someone has sent more than one file for the same week, keep the most recently modified and log the one it replaced:",[334,869,871],{"className":381,"code":870,"language":383,"meta":339,"style":339},"files = sorted(Path(\"timesheets\").glob(\"*.xlsx\"))\nframes = []\nfor f in files:\n    ts = read_timesheet(f)\n    frames.append(ts.assign(Modified=pd.Timestamp(f.stat().st_mtime, unit=\"s\")))\nhours = pd.concat(frames, ignore_index=True)\n\nlatest = hours.groupby([\"Name\", \"Week\"])[\"Modified\"].transform(\"max\")\nreplaced = hours.loc[hours[\"Modified\"] \u003C latest, [\"Name\", \"File\"]].drop_duplicates()\nhours = hours[hours[\"Modified\"] == latest].drop(columns=\"Modified\")\nprint(len(files), \"files;\", \"replaced:\", replaced[\"File\"].tolist())\nprint(hours.groupby(\"Name\")[\"Hours\"].sum().to_dict())\n",[341,872,873,897,907,920,930,954,973,977,1009,1039,1066,1095],{"__ignoreMap":339},[344,874,875,878,880,883,886,889,892,895],{"class":302,"line":346},[344,876,877],{"class":394},"files ",[344,879,512],{"class":390},[344,881,882],{"class":441}," sorted",[344,884,885],{"class":394},"(Path(",[344,887,888],{"class":353},"\"timesheets\"",[344,890,891],{"class":394},").glob(",[344,893,894],{"class":353},"\"*.xlsx\"",[344,896,598],{"class":394},[344,898,899,902,904],{"class":302,"line":404},[344,900,901],{"class":394},"frames ",[344,903,512],{"class":390},[344,905,906],{"class":394}," []\n",[344,908,909,912,915,917],{"class":302,"line":418},[344,910,911],{"class":390},"for",[344,913,914],{"class":394}," f ",[344,916,626],{"class":390},[344,918,919],{"class":394}," files:\n",[344,921,922,925,927],{"class":302,"line":431},[344,923,924],{"class":394},"    ts ",[344,926,512],{"class":390},[344,928,929],{"class":394}," read_timesheet(f)\n",[344,931,932,935,938,940,943,946,948,951],{"class":302,"line":438},[344,933,934],{"class":394},"    frames.append(ts.assign(",[344,936,937],{"class":518},"Modified",[344,939,512],{"class":390},[344,941,942],{"class":394},"pd.Timestamp(f.stat().st_mtime, ",[344,944,945],{"class":518},"unit",[344,947,512],{"class":390},[344,949,950],{"class":353},"\"s\"",[344,952,953],{"class":394},")))\n",[344,955,956,959,961,964,967,969,971],{"class":302,"line":488},[344,957,958],{"class":394},"hours ",[344,960,512],{"class":390},[344,962,963],{"class":394}," pd.concat(frames, ",[344,965,966],{"class":518},"ignore_index",[344,968,512],{"class":390},[344,970,524],{"class":441},[344,972,787],{"class":394},[344,974,975],{"class":302,"line":493},[344,976,435],{"emptyLinePlaceholder":434},[344,978,979,982,984,987,990,992,995,998,1001,1004,1007],{"class":302,"line":506},[344,980,981],{"class":394},"latest ",[344,983,512],{"class":390},[344,985,986],{"class":394}," hours.groupby([",[344,988,989],{"class":353},"\"Name\"",[344,991,454],{"class":394},[344,993,994],{"class":353},"\"Week\"",[344,996,997],{"class":394},"])[",[344,999,1000],{"class":353},"\"Modified\"",[344,1002,1003],{"class":394},"].transform(",[344,1005,1006],{"class":353},"\"max\"",[344,1008,787],{"class":394},[344,1010,1011,1014,1016,1019,1021,1023,1026,1029,1031,1033,1036],{"class":302,"line":544},[344,1012,1013],{"class":394},"replaced ",[344,1015,512],{"class":390},[344,1017,1018],{"class":394}," hours.loc[hours[",[344,1020,1000],{"class":353},[344,1022,647],{"class":394},[344,1024,1025],{"class":390},"\u003C",[344,1027,1028],{"class":394}," latest, [",[344,1030,989],{"class":353},[344,1032,454],{"class":394},[344,1034,1035],{"class":353},"\"File\"",[344,1037,1038],{"class":394},"]].drop_duplicates()\n",[344,1040,1041,1043,1045,1048,1050,1052,1055,1058,1060,1062,1064],{"class":302,"line":567},[344,1042,958],{"class":394},[344,1044,512],{"class":390},[344,1046,1047],{"class":394}," hours[hours[",[344,1049,1000],{"class":353},[344,1051,647],{"class":394},[344,1053,1054],{"class":390},"==",[344,1056,1057],{"class":394}," latest].drop(",[344,1059,689],{"class":518},[344,1061,512],{"class":390},[344,1063,1000],{"class":353},[344,1065,787],{"class":394},[344,1067,1068,1070,1073,1076,1079,1082,1084,1087,1090,1092],{"class":302,"line":601},[344,1069,832],{"class":441},[344,1071,1072],{"class":394},"(",[344,1074,1075],{"class":441},"len",[344,1077,1078],{"class":394},"(files), ",[344,1080,1081],{"class":353},"\"files;\"",[344,1083,454],{"class":394},[344,1085,1086],{"class":353},"\"replaced:\"",[344,1088,1089],{"class":394},", replaced[",[344,1091,1035],{"class":353},[344,1093,1094],{"class":394},"].tolist())\n",[344,1096,1097,1099,1102,1104,1106,1108],{"class":302,"line":617},[344,1098,832],{"class":441},[344,1100,1101],{"class":394},"(hours.groupby(",[344,1103,989],{"class":353},[344,1105,536],{"class":394},[344,1107,742],{"class":353},[344,1109,1110],{"class":394},"].sum().to_dict())\n",[10,1112,1113],{},"Dee's first file, with the 14-hour Tuesday, is superseded by the corrected one. Using the file's modification time works for files saved into a folder; if files arrive by email, use the received time or a version number in the file name instead, because copying a file can reset its modification time. Keep the list of replaced files — it goes on the exceptions sheet.",[329,1115,1117],{"id":1116},"step-3-check-who-is-missing-and-what-does-not-add-up","Step 3 — Check who is missing and what does not add up",[10,1119,1120],{},"Now compare the submissions with the reference data: staff with no timesheet, project codes that do not exist, days over a plausible maximum, and weeks short of contracted hours:",[334,1122,1124],{"className":381,"code":1123,"language":383,"meta":339,"style":339},"staff = pd.read_excel(\"staff_and_projects.xlsx\", sheet_name=\"Staff\")\nprojects = pd.read_excel(\"staff_and_projects.xlsx\", sheet_name=\"Projects\")\n\nissues = []\nfor name in sorted(set(staff[\"Name\"]) - set(hours[\"Name\"])):\n    issues.append({\"Name\": name, \"Issue\": \"no timesheet submitted\"})\nfor name in sorted(set(hours[\"Name\"]) - set(staff[\"Name\"])):\n    issues.append({\"Name\": name, \"Issue\": \"name not on the staff list\"})\nfor r in hours[~hours[\"Project\"].isin(projects[\"Code\"])].itertuples():\n    issues.append({\"Name\": r.Name, \"Issue\": f\"unknown project {r.Project} ({r.Hours:g}h on {r.Day})\"})\ndaily = hours.groupby([\"Name\", \"Day\"])[\"Hours\"].sum()\nfor (name, day), h in daily[daily > 12].items():\n    issues.append({\"Name\": name, \"Issue\": f\"{h:g} hours on {day}\"})\nweekly = hours.groupby(\"Name\", as_index=False)[\"Hours\"].sum().merge(staff, on=\"Name\")\nfor r in weekly[weekly[\"Hours\"] \u003C weekly[\"Contracted hours\"]].itertuples(index=False, name=None):\n    issues.append({\"Name\": r[0], \"Issue\": f\"{r[1]:g} of {r[2]:g} contracted hours\"})\nfor name, f in replaced.itertuples(index=False, name=None):\n    issues.append({\"Name\": name, \"Issue\": f\"superseded file {f} ignored\"})\nexceptions = pd.DataFrame(issues)\nprint(exceptions.to_string(index=False))\n",[341,1125,1126,1151,1173,1177,1186,1224,1246,1276,1293,1322,1378,1400,1421,1461,1498,1541,1597,1625,1653,1663],{"__ignoreMap":339},[344,1127,1128,1131,1133,1136,1139,1141,1144,1146,1149],{"class":302,"line":346},[344,1129,1130],{"class":394},"staff ",[344,1132,512],{"class":390},[344,1134,1135],{"class":394}," pd.read_excel(",[344,1137,1138],{"class":353},"\"staff_and_projects.xlsx\"",[344,1140,454],{"class":394},[344,1142,1143],{"class":518},"sheet_name",[344,1145,512],{"class":390},[344,1147,1148],{"class":353},"\"Staff\"",[344,1150,787],{"class":394},[344,1152,1153,1156,1158,1160,1162,1164,1166,1168,1171],{"class":302,"line":404},[344,1154,1155],{"class":394},"projects ",[344,1157,512],{"class":390},[344,1159,1135],{"class":394},[344,1161,1138],{"class":353},[344,1163,454],{"class":394},[344,1165,1143],{"class":518},[344,1167,512],{"class":390},[344,1169,1170],{"class":353},"\"Projects\"",[344,1172,787],{"class":394},[344,1174,1175],{"class":302,"line":418},[344,1176,435],{"emptyLinePlaceholder":434},[344,1178,1179,1182,1184],{"class":302,"line":431},[344,1180,1181],{"class":394},"issues ",[344,1183,512],{"class":390},[344,1185,906],{"class":394},[344,1187,1188,1190,1193,1195,1197,1199,1202,1205,1207,1210,1213,1216,1219,1221],{"class":302,"line":438},[344,1189,911],{"class":390},[344,1191,1192],{"class":394}," name ",[344,1194,626],{"class":390},[344,1196,882],{"class":441},[344,1198,1072],{"class":394},[344,1200,1201],{"class":441},"set",[344,1203,1204],{"class":394},"(staff[",[344,1206,989],{"class":353},[344,1208,1209],{"class":394},"]) ",[344,1211,1212],{"class":390},"-",[344,1214,1215],{"class":441}," set",[344,1217,1218],{"class":394},"(hours[",[344,1220,989],{"class":353},[344,1222,1223],{"class":394},"])):\n",[344,1225,1226,1229,1231,1234,1237,1240,1243],{"class":302,"line":488},[344,1227,1228],{"class":394},"    issues.append({",[344,1230,989],{"class":353},[344,1232,1233],{"class":394},": name, ",[344,1235,1236],{"class":353},"\"Issue\"",[344,1238,1239],{"class":394},": ",[344,1241,1242],{"class":353},"\"no timesheet submitted\"",[344,1244,1245],{"class":394},"})\n",[344,1247,1248,1250,1252,1254,1256,1258,1260,1262,1264,1266,1268,1270,1272,1274],{"class":302,"line":493},[344,1249,911],{"class":390},[344,1251,1192],{"class":394},[344,1253,626],{"class":390},[344,1255,882],{"class":441},[344,1257,1072],{"class":394},[344,1259,1201],{"class":441},[344,1261,1218],{"class":394},[344,1263,989],{"class":353},[344,1265,1209],{"class":394},[344,1267,1212],{"class":390},[344,1269,1215],{"class":441},[344,1271,1204],{"class":394},[344,1273,989],{"class":353},[344,1275,1223],{"class":394},[344,1277,1278,1280,1282,1284,1286,1288,1291],{"class":302,"line":506},[344,1279,1228],{"class":394},[344,1281,989],{"class":353},[344,1283,1233],{"class":394},[344,1285,1236],{"class":353},[344,1287,1239],{"class":394},[344,1289,1290],{"class":353},"\"name not on the staff list\"",[344,1292,1245],{"class":394},[344,1294,1295,1297,1300,1302,1305,1308,1311,1313,1316,1319],{"class":302,"line":544},[344,1296,911],{"class":390},[344,1298,1299],{"class":394}," r ",[344,1301,626],{"class":390},[344,1303,1304],{"class":394}," hours[",[344,1306,1307],{"class":390},"~",[344,1309,1310],{"class":394},"hours[",[344,1312,713],{"class":353},[344,1314,1315],{"class":394},"].isin(projects[",[344,1317,1318],{"class":353},"\"Code\"",[344,1320,1321],{"class":394},"])].itertuples():\n",[344,1323,1324,1326,1328,1331,1333,1335,1338,1341,1345,1348,1351,1353,1355,1358,1361,1363,1366,1368,1371,1373,1376],{"class":302,"line":567},[344,1325,1228],{"class":394},[344,1327,989],{"class":353},[344,1329,1330],{"class":394},": r.Name, ",[344,1332,1236],{"class":353},[344,1334,1239],{"class":394},[344,1336,1337],{"class":390},"f",[344,1339,1340],{"class":353},"\"unknown project ",[344,1342,1344],{"class":1343},"sSjpA","{",[344,1346,1347],{"class":394},"r.Project",[344,1349,1350],{"class":1343},"}",[344,1352,652],{"class":353},[344,1354,1344],{"class":1343},[344,1356,1357],{"class":394},"r.Hours",[344,1359,1360],{"class":390},":g",[344,1362,1350],{"class":1343},[344,1364,1365],{"class":353},"h on ",[344,1367,1344],{"class":1343},[344,1369,1370],{"class":394},"r.Day",[344,1372,1350],{"class":1343},[344,1374,1375],{"class":353},")\"",[344,1377,1245],{"class":394},[344,1379,1380,1383,1385,1387,1389,1391,1393,1395,1397],{"class":302,"line":601},[344,1381,1382],{"class":394},"daily ",[344,1384,512],{"class":390},[344,1386,986],{"class":394},[344,1388,989],{"class":353},[344,1390,454],{"class":394},[344,1392,732],{"class":353},[344,1394,997],{"class":394},[344,1396,742],{"class":353},[344,1398,1399],{"class":394},"].sum()\n",[344,1401,1402,1404,1407,1409,1412,1415,1418],{"class":302,"line":617},[344,1403,911],{"class":390},[344,1405,1406],{"class":394}," (name, day), h ",[344,1408,626],{"class":390},[344,1410,1411],{"class":394}," daily[daily ",[344,1413,1414],{"class":390},">",[344,1416,1417],{"class":441}," 12",[344,1419,1420],{"class":394},"].items():\n",[344,1422,1423,1425,1427,1429,1431,1433,1435,1438,1440,1443,1445,1447,1450,1452,1455,1457,1459],{"class":302,"line":636},[344,1424,1228],{"class":394},[344,1426,989],{"class":353},[344,1428,1233],{"class":394},[344,1430,1236],{"class":353},[344,1432,1239],{"class":394},[344,1434,1337],{"class":390},[344,1436,1437],{"class":353},"\"",[344,1439,1344],{"class":1343},[344,1441,1442],{"class":394},"h",[344,1444,1360],{"class":390},[344,1446,1350],{"class":1343},[344,1448,1449],{"class":353}," hours on ",[344,1451,1344],{"class":1343},[344,1453,1454],{"class":394},"day",[344,1456,1350],{"class":1343},[344,1458,1437],{"class":353},[344,1460,1245],{"class":394},[344,1462,1463,1466,1468,1471,1473,1475,1478,1480,1482,1484,1486,1489,1492,1494,1496],{"class":302,"line":666},[344,1464,1465],{"class":394},"weekly ",[344,1467,512],{"class":390},[344,1469,1470],{"class":394}," hours.groupby(",[344,1472,989],{"class":353},[344,1474,454],{"class":394},[344,1476,1477],{"class":518},"as_index",[344,1479,512],{"class":390},[344,1481,849],{"class":441},[344,1483,536],{"class":394},[344,1485,742],{"class":353},[344,1487,1488],{"class":394},"].sum().merge(staff, ",[344,1490,1491],{"class":518},"on",[344,1493,512],{"class":390},[344,1495,989],{"class":353},[344,1497,787],{"class":394},[344,1499,1500,1502,1504,1506,1509,1511,1513,1515,1518,1521,1524,1526,1528,1530,1532,1535,1537,1539],{"class":302,"line":672},[344,1501,911],{"class":390},[344,1503,1299],{"class":394},[344,1505,626],{"class":390},[344,1507,1508],{"class":394}," weekly[weekly[",[344,1510,742],{"class":353},[344,1512,647],{"class":394},[344,1514,1025],{"class":390},[344,1516,1517],{"class":394}," weekly[",[344,1519,1520],{"class":353},"\"Contracted hours\"",[344,1522,1523],{"class":394},"]].itertuples(",[344,1525,844],{"class":518},[344,1527,512],{"class":390},[344,1529,849],{"class":441},[344,1531,454],{"class":394},[344,1533,1534],{"class":518},"name",[344,1536,512],{"class":390},[344,1538,655],{"class":441},[344,1540,663],{"class":394},[344,1542,1543,1545,1547,1550,1552,1555,1557,1559,1561,1563,1565,1568,1570,1573,1575,1577,1580,1582,1584,1586,1588,1590,1592,1595],{"class":302,"line":678},[344,1544,1228],{"class":394},[344,1546,989],{"class":353},[344,1548,1549],{"class":394},": r[",[344,1551,50],{"class":441},[344,1553,1554],{"class":394},"], ",[344,1556,1236],{"class":353},[344,1558,1239],{"class":394},[344,1560,1337],{"class":390},[344,1562,1437],{"class":353},[344,1564,1344],{"class":1343},[344,1566,1567],{"class":394},"r[",[344,1569,106],{"class":441},[344,1571,1572],{"class":394},"]",[344,1574,1360],{"class":390},[344,1576,1350],{"class":1343},[344,1578,1579],{"class":353}," of ",[344,1581,1344],{"class":1343},[344,1583,1567],{"class":394},[344,1585,134],{"class":441},[344,1587,1572],{"class":394},[344,1589,1360],{"class":390},[344,1591,1350],{"class":1343},[344,1593,1594],{"class":353}," contracted hours\"",[344,1596,1245],{"class":394},[344,1598,1599,1601,1604,1606,1609,1611,1613,1615,1617,1619,1621,1623],{"class":302,"line":697},[344,1600,911],{"class":390},[344,1602,1603],{"class":394}," name, f ",[344,1605,626],{"class":390},[344,1607,1608],{"class":394}," replaced.itertuples(",[344,1610,844],{"class":518},[344,1612,512],{"class":390},[344,1614,849],{"class":441},[344,1616,454],{"class":394},[344,1618,1534],{"class":518},[344,1620,512],{"class":390},[344,1622,655],{"class":441},[344,1624,663],{"class":394},[344,1626,1627,1629,1631,1633,1635,1637,1639,1642,1644,1646,1648,1651],{"class":302,"line":761},[344,1628,1228],{"class":394},[344,1630,989],{"class":353},[344,1632,1233],{"class":394},[344,1634,1236],{"class":353},[344,1636,1239],{"class":394},[344,1638,1337],{"class":390},[344,1640,1641],{"class":353},"\"superseded file ",[344,1643,1344],{"class":1343},[344,1645,1337],{"class":394},[344,1647,1350],{"class":1343},[344,1649,1650],{"class":353}," ignored\"",[344,1652,1245],{"class":394},[344,1654,1655,1658,1660],{"class":302,"line":790},[344,1656,1657],{"class":394},"exceptions ",[344,1659,512],{"class":390},[344,1661,1662],{"class":394}," pd.DataFrame(issues)\n",[344,1664,1665,1667,1670,1672,1674,1676],{"class":302,"line":824},[344,1666,832],{"class":441},[344,1668,1669],{"class":394},"(exceptions.to_string(",[344,1671,844],{"class":518},[344,1673,512],{"class":390},[344,1675,849],{"class":441},[344,1677,598],{"class":394},[10,1679,1680],{},"Eli has not submitted, Cai booked six hours to P-999, which is not a project, and Ben recorded 30 of 37.5 contracted hours — perhaps a day of leave that should be booked to a leave code. Dee's corrected Tuesday of 10 hours passes the 12-hour check; the original 14 would have failed it, and the superseded file is logged. Each issue names the person, so the list can be sent straight back to them.",[28,1682,37,1688,37,1691,37,1694,37,1699,37,1705,37,1709,37,1713,37,1717,37,1721,37,1724,37,1727,37,1731,37,1733,37,1735,37,1737,37,1740,37,1743,37,1746,37,1748,37,1751,37,1756,37,1760,37,1763,37,1766,37,1768,37,1771,37,1773,37,1776,37,1779,37,1782,37,1784,37,1787,37,1791,37,1795,37,1798,37,1801,37,1803,37,1806,37,1808],{"viewBox":1683,"role":31,"ariaLabelledBy":1684,"xmlns":35,"style":1687},"161 0 438 228",[1685,1686],"ts-exceptions-t","ts-exceptions-d","width:100%;max-width:438px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[39,1689,1690],{"id":1685},"The exceptions sheet for the week ending 27 September",[43,1692,1693],{"id":1686},"Four exceptions. Eli Brandt has not submitted a timesheet. Cai Wong booked 6 hours on Friday to unknown project P-999. Ben Okafor recorded 30 of 37.5 contracted hours. Dee Novak's superseded file dee_week39.xlsx was ignored.",[47,1695],{"x":1696,"y":50,"width":1697,"height":1698,"fill":53},"161","438","228",[68,1700,1704],{"x":1701,"y":1702,"style":1703},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Exceptions",[47,1706],{"x":1707,"y":1708,"width":58,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"177.1","48.0",[47,1710],{"x":1711,"y":1708,"width":1712,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"209.1","92.9",[68,1714,73],{"x":1715,"y":1716,"style":72},"255.6","65.0",[47,1718],{"x":1719,"y":1708,"width":1720,"height":59,"rx":50,"fill":60,"stroke":61,"style":62},"302.0","280.9",[68,1722,81],{"x":1723,"y":1716,"style":72},"442.4",[47,1725],{"x":1707,"y":1726,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"72.0",[68,1728,106],{"x":1729,"y":1730,"style":72},"193.1","91.0",[47,1732],{"x":1711,"y":1726,"width":1712,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,1734,213],{"x":1715,"y":1730,"style":111},[47,1736],{"x":1719,"y":1726,"width":1720,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,1738,1739],{"x":1723,"y":1730,"style":111},"Issue",[47,1741],{"x":1707,"y":1742,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"100.0",[68,1744,134],{"x":1729,"y":1745,"style":72},"119.0",[47,1747],{"x":1711,"y":1742,"width":1712,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,1749,1750],{"x":1715,"y":1745,"style":139},"Eli Brandt",[47,1752],{"x":1719,"y":1742,"width":1720,"height":101,"rx":50,"fill":1753,"stroke":1754,"style":1755},"#fee8f2","var(--accent,#f43f8f)","stroke-width:1.5px",[68,1757,1759],{"x":1723,"y":1745,"style":1758},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","no timesheet submitted",[47,1761],{"x":1707,"y":1762,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"128.0",[68,1764,161],{"x":1729,"y":1765,"style":72},"147.0",[47,1767],{"x":1711,"y":1762,"width":1712,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,1769,1770],{"x":1715,"y":1765,"style":139},"Cai Wong",[47,1772],{"x":1719,"y":1762,"width":1720,"height":101,"rx":50,"fill":1753,"stroke":1754,"style":1755},[68,1774,1775],{"x":1723,"y":1765,"style":1758},"unknown project P-999 (6h on Fri)",[47,1777],{"x":1707,"y":1778,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"156.0",[68,1780,154],{"x":1729,"y":1781,"style":72},"175.0",[47,1783],{"x":1711,"y":1778,"width":1712,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,1785,1786],{"x":1715,"y":1781,"style":139},"Ben Okafor",[47,1788],{"x":1719,"y":1778,"width":1720,"height":101,"rx":50,"fill":1789,"stroke":1790,"style":1755},"#fdefd8","var(--gold,#b4740a)",[68,1792,1794],{"x":1723,"y":1781,"style":1793},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","30 of 37.5 contracted hours",[47,1796],{"x":1707,"y":1797,"width":58,"height":101,"rx":50,"fill":60,"stroke":61,"style":62},"184.0",[68,1799,586],{"x":1729,"y":1800,"style":72},"203.0",[47,1802],{"x":1711,"y":1797,"width":1712,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,1804,1805],{"x":1715,"y":1800,"style":139},"Dee Novak",[47,1807],{"x":1719,"y":1797,"width":1720,"height":101,"rx":50,"fill":53,"stroke":61,"style":62},[68,1809,1810],{"x":1723,"y":1800,"style":139},"superseded file dee_week39.xlsx ignored",[28,1812,37,1818,37,1821,37,1824,37,1828,37,1833,37,1838,37,1844,37,1847,37,1851,37,1855,37,1860,37,1862,37,1867,37,1869,37,1872,37,1875,37,1879,37,1881,37,1884,37,1886,37,1889,37,1892,37,1896,37,1898,37,1902,37,1904,37,1907,37,1910,37,1914,37,1916,37,1920,37,1922,37,1925,37,1928,37,1932,37,1934,37,1938,37,1940],{"viewBox":1813,"role":31,"ariaLabelledBy":1814,"xmlns":35,"style":1817},"4 0 752 242",[1815,1816],"ts-checks-t","ts-checks-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[39,1819,1820],{"id":1815},"What the consolidation checks, and why",[43,1822,1823],{"id":1816},"A table of checks. Missing timesheet: someone on the staff list has no file; chase them. Unknown project: a code not in the project list, such as P-999; correct the code. Impossible day: more than 12 hours on one day; confirm or correct. Short week: fewer hours than contracted; confirm leave or missing time. Superseded file: an older version replaced by a newer one; logged for traceability.",[47,1825],{"x":154,"y":50,"width":1826,"height":1827,"fill":53},"752","242",[47,1829],{"x":1830,"y":1831,"width":1832,"height":57,"rx":50,"fill":53,"stroke":61,"style":62},"20.0","16.0","145.8",[47,1834],{"x":1835,"y":1831,"width":1836,"height":57,"rx":50,"fill":1837,"stroke":307,"style":321},"165.8","287.1","#f0f4ff",[68,1839,1843],{"x":1840,"y":1841,"style":1842},"309.3","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Detected by",[47,1845],{"x":1846,"y":1831,"width":1836,"height":57,"rx":50,"fill":1837,"stroke":307,"style":321},"452.9",[68,1848,1850],{"x":1849,"y":1841,"style":1842},"596.4","Action",[47,1852],{"x":1830,"y":1853,"width":1832,"height":1854,"rx":50,"fill":60,"stroke":61,"style":62},"56.0","34.0",[68,1856,1859],{"x":58,"y":1857,"style":1858},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Missing timesheet",[47,1861],{"x":1835,"y":1853,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1863,1866],{"x":1840,"y":1864,"style":1865},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","staff list minus submitters",[47,1868],{"x":1846,"y":1853,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1870,1871],{"x":1849,"y":1864,"style":1865},"chase the person",[47,1873],{"x":1830,"y":1874,"width":1832,"height":1854,"rx":50,"fill":60,"stroke":61,"style":62},"90.0",[68,1876,1878],{"x":58,"y":1877,"style":1858},"112.0","Unknown project",[47,1880],{"x":1835,"y":1874,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1882,1883],{"x":1840,"y":133,"style":1865},"code not in Projects",[47,1885],{"x":1846,"y":1874,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1887,1888],{"x":1849,"y":133,"style":1865},"correct the code",[47,1890],{"x":1830,"y":1891,"width":1832,"height":1854,"rx":50,"fill":60,"stroke":61,"style":62},"124.0",[68,1893,1895],{"x":58,"y":1894,"style":1858},"146.0","Impossible day",[47,1897],{"x":1835,"y":1891,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1899,1901],{"x":1840,"y":1900,"style":1865},"145.0","over 12 hours in a day",[47,1903],{"x":1846,"y":1891,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1905,1906],{"x":1849,"y":1900,"style":1865},"confirm or correct",[47,1908],{"x":1830,"y":1909,"width":1832,"height":1854,"rx":50,"fill":60,"stroke":61,"style":62},"158.0",[68,1911,1913],{"x":58,"y":1912,"style":1858},"180.0","Short week",[47,1915],{"x":1835,"y":1909,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1917,1919],{"x":1840,"y":1918,"style":1865},"179.0","below contracted hours",[47,1921],{"x":1846,"y":1909,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1923,1924],{"x":1849,"y":1918,"style":1865},"confirm leave",[47,1926],{"x":1830,"y":1927,"width":1832,"height":1854,"rx":50,"fill":60,"stroke":61,"style":62},"192.0",[68,1929,1931],{"x":58,"y":1930,"style":1858},"214.0","Superseded file",[47,1933],{"x":1835,"y":1927,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1935,1937],{"x":1840,"y":1936,"style":1865},"213.0","older version per person",[47,1939],{"x":1846,"y":1927,"width":1836,"height":1854,"rx":50,"fill":53,"stroke":61,"style":62},[68,1941,1942],{"x":1849,"y":1936,"style":1865},"logged only",[329,1944,1946],{"id":1945},"step-4-summarise-hours-by-project-and-person","Step 4 — Summarise hours by project and person",[10,1948,1949],{},"With the data in long format, every summary is a pivot. Join the project list to get the client and billable flag, then build the views managers ask for:",[334,1951,1953],{"className":381,"code":1952,"language":383,"meta":339,"style":339},"hours = hours.merge(projects.rename(columns={\"Code\": \"Project\"}), on=\"Project\", how=\"left\")\nby_project = hours.pivot_table(index=[\"Client\", \"Project\"], columns=\"Name\", values=\"Hours\", aggfunc=\"sum\",\n                               fill_value=0, margins=True, margins_name=\"Total\")\nprint(by_project.to_string())\n\nbillable = hours.groupby(\"Name\").apply(lambda g: g.loc[g[\"Billable\"] == True, \"Hours\"].sum() \u002F g[\"Hours\"].sum(),\n                                       include_groups=False)\nprint((billable * 100).round(1).to_dict())\n",[341,1954,1955,1997,2050,2079,2086,2090,2138,2149],{"__ignoreMap":339},[344,1956,1957,1959,1961,1964,1966,1968,1970,1972,1974,1976,1979,1981,1983,1985,1987,1990,1992,1995],{"class":302,"line":346},[344,1958,958],{"class":394},[344,1960,512],{"class":390},[344,1962,1963],{"class":394}," hours.merge(projects.rename(",[344,1965,689],{"class":518},[344,1967,512],{"class":390},[344,1969,1344],{"class":394},[344,1971,1318],{"class":353},[344,1973,1239],{"class":394},[344,1975,713],{"class":353},[344,1977,1978],{"class":394},"}), ",[344,1980,1491],{"class":518},[344,1982,512],{"class":390},[344,1984,713],{"class":353},[344,1986,454],{"class":394},[344,1988,1989],{"class":518},"how",[344,1991,512],{"class":390},[344,1993,1994],{"class":353},"\"left\"",[344,1996,787],{"class":394},[344,1998,1999,2002,2004,2007,2009,2011,2013,2016,2018,2020,2022,2024,2026,2028,2030,2033,2035,2037,2039,2042,2044,2047],{"class":302,"line":404},[344,2000,2001],{"class":394},"by_project ",[344,2003,512],{"class":390},[344,2005,2006],{"class":394}," hours.pivot_table(",[344,2008,844],{"class":518},[344,2010,512],{"class":390},[344,2012,753],{"class":394},[344,2014,2015],{"class":353},"\"Client\"",[344,2017,454],{"class":394},[344,2019,713],{"class":353},[344,2021,1554],{"class":394},[344,2023,689],{"class":518},[344,2025,512],{"class":390},[344,2027,989],{"class":353},[344,2029,454],{"class":394},[344,2031,2032],{"class":518},"values",[344,2034,512],{"class":390},[344,2036,742],{"class":353},[344,2038,454],{"class":394},[344,2040,2041],{"class":518},"aggfunc",[344,2043,512],{"class":390},[344,2045,2046],{"class":353},"\"sum\"",[344,2048,2049],{"class":394},",\n",[344,2051,2052,2055,2057,2059,2061,2064,2066,2068,2070,2073,2075,2077],{"class":302,"line":418},[344,2053,2054],{"class":518},"                               fill_value",[344,2056,512],{"class":390},[344,2058,50],{"class":441},[344,2060,454],{"class":394},[344,2062,2063],{"class":518},"margins",[344,2065,512],{"class":390},[344,2067,524],{"class":441},[344,2069,454],{"class":394},[344,2071,2072],{"class":518},"margins_name",[344,2074,512],{"class":390},[344,2076,660],{"class":353},[344,2078,787],{"class":394},[344,2080,2081,2083],{"class":302,"line":431},[344,2082,832],{"class":441},[344,2084,2085],{"class":394},"(by_project.to_string())\n",[344,2087,2088],{"class":302,"line":438},[344,2089,435],{"emptyLinePlaceholder":434},[344,2091,2092,2095,2097,2099,2101,2104,2107,2110,2113,2115,2117,2120,2122,2124,2127,2130,2133,2135],{"class":302,"line":488},[344,2093,2094],{"class":394},"billable ",[344,2096,512],{"class":390},[344,2098,1470],{"class":394},[344,2100,989],{"class":353},[344,2102,2103],{"class":394},").apply(",[344,2105,2106],{"class":390},"lambda",[344,2108,2109],{"class":394}," g: g.loc[g[",[344,2111,2112],{"class":353},"\"Billable\"",[344,2114,647],{"class":394},[344,2116,1054],{"class":390},[344,2118,2119],{"class":441}," True",[344,2121,454],{"class":394},[344,2123,742],{"class":353},[344,2125,2126],{"class":394},"].sum() ",[344,2128,2129],{"class":390},"\u002F",[344,2131,2132],{"class":394}," g[",[344,2134,742],{"class":353},[344,2136,2137],{"class":394},"].sum(),\n",[344,2139,2140,2143,2145,2147],{"class":302,"line":493},[344,2141,2142],{"class":518},"                                       include_groups",[344,2144,512],{"class":390},[344,2146,849],{"class":441},[344,2148,787],{"class":394},[344,2150,2151,2153,2156,2159,2162,2165,2167],{"class":302,"line":506},[344,2152,832],{"class":441},[344,2154,2155],{"class":394},"((billable ",[344,2157,2158],{"class":390},"*",[344,2160,2161],{"class":441}," 100",[344,2163,2164],{"class":394},").round(",[344,2166,106],{"class":441},[344,2168,2169],{"class":394},").to_dict())\n",[10,2171,2172,2173,2176],{},"The left merge keeps P-999's hours with an empty client rather than dropping them, and ",[341,2174,2175],{},"pivot_table"," then leaves them out of the project view because its client is missing — the exceptions sheet is where they appear until corrected. Billable utilisation — the share of each person's hours on billable projects — is the figure operations managers watch most; Ben's administrative day brings Ben's figure down to 75%. Cai's figure is lower than it will be once P-999 is corrected, because those six hours count towards Cai's total but not as billable.",[329,2178,2180],{"id":2179},"step-5-write-the-consolidated-workbook","Step 5 — Write the consolidated workbook",[10,2182,2183],{},"Write the project summary, the long-format detail for anyone who wants to pivot it differently, and the exceptions:",[334,2185,2187],{"className":381,"code":2186,"language":383,"meta":339,"style":339},"with pd.ExcelWriter(\"timesheets_week_2026-09-27.xlsx\", engine=\"openpyxl\") as writer:\n    by_project.to_excel(writer, sheet_name=\"By project\")\n    hours[[\"Week\", \"Name\", \"Client\", \"Project\", \"Day\", \"Hours\", \"Billable\", \"File\"]].to_excel(writer, sheet_name=\"Detail\", index=False)\n    exceptions.to_excel(writer, sheet_name=\"Exceptions\", index=False)\nprint(pd.ExcelFile(\"timesheets_week_2026-09-27.xlsx\").sheet_names)\n",[341,2188,2189,2218,2232,2287,2309],{"__ignoreMap":339},[344,2190,2191,2194,2197,2200,2202,2205,2207,2210,2213,2215],{"class":302,"line":346},[344,2192,2193],{"class":390},"with",[344,2195,2196],{"class":394}," pd.ExcelWriter(",[344,2198,2199],{"class":353},"\"timesheets_week_2026-09-27.xlsx\"",[344,2201,454],{"class":394},[344,2203,2204],{"class":518},"engine",[344,2206,512],{"class":390},[344,2208,2209],{"class":353},"\"openpyxl\"",[344,2211,2212],{"class":394},") ",[344,2214,412],{"class":390},[344,2216,2217],{"class":394}," writer:\n",[344,2219,2220,2223,2225,2227,2230],{"class":302,"line":404},[344,2221,2222],{"class":394},"    by_project.to_excel(writer, ",[344,2224,1143],{"class":518},[344,2226,512],{"class":390},[344,2228,2229],{"class":353},"\"By project\"",[344,2231,787],{"class":394},[344,2233,2234,2237,2239,2241,2243,2245,2247,2249,2251,2253,2255,2257,2259,2261,2263,2265,2267,2270,2272,2274,2277,2279,2281,2283,2285],{"class":302,"line":418},[344,2235,2236],{"class":394},"    hours[[",[344,2238,994],{"class":353},[344,2240,454],{"class":394},[344,2242,989],{"class":353},[344,2244,454],{"class":394},[344,2246,2015],{"class":353},[344,2248,454],{"class":394},[344,2250,713],{"class":353},[344,2252,454],{"class":394},[344,2254,732],{"class":353},[344,2256,454],{"class":394},[344,2258,742],{"class":353},[344,2260,454],{"class":394},[344,2262,2112],{"class":353},[344,2264,454],{"class":394},[344,2266,1035],{"class":353},[344,2268,2269],{"class":394},"]].to_excel(writer, ",[344,2271,1143],{"class":518},[344,2273,512],{"class":390},[344,2275,2276],{"class":353},"\"Detail\"",[344,2278,454],{"class":394},[344,2280,844],{"class":518},[344,2282,512],{"class":390},[344,2284,849],{"class":441},[344,2286,787],{"class":394},[344,2288,2289,2292,2294,2296,2299,2301,2303,2305,2307],{"class":302,"line":431},[344,2290,2291],{"class":394},"    exceptions.to_excel(writer, ",[344,2293,1143],{"class":518},[344,2295,512],{"class":390},[344,2297,2298],{"class":353},"\"Exceptions\"",[344,2300,454],{"class":394},[344,2302,844],{"class":518},[344,2304,512],{"class":390},[344,2306,849],{"class":441},[344,2308,787],{"class":394},[344,2310,2311,2313,2316,2318],{"class":302,"line":438},[344,2312,832],{"class":441},[344,2314,2315],{"class":394},"(pd.ExcelFile(",[344,2317,2199],{"class":353},[344,2319,2320],{"class":394},").sheet_names)\n",[10,2322,2323],{},"The Detail sheet records the source file of every row, so a figure in the summary can be traced to the timesheet it came from. Running this on a schedule the morning after the deadline, and emailing the exceptions to each person named, turns chasing timesheets into a reply to an automatic message.",[329,2325,2327],{"id":2326},"common-pitfalls","Common pitfalls",[2329,2330,2331,2339,2348,2354,2360,2366],"ul",{},[2332,2333,2334,2338],"li",{},[2335,2336,2337],"strong",{},"Fixed grid sizes."," Stop at the Total row rather than reading a set number of rows.",[2332,2340,2341,2344,2345,2347],{},[2335,2342,2343],{},"Formulas in the template."," Use ",[341,2346,859],{}," so totals are values, not formula text.",[2332,2349,2350,2353],{},[2335,2351,2352],{},"Duplicate submissions."," Keep the latest per person and week; log what was replaced.",[2332,2355,2356,2359],{},[2335,2357,2358],{},"Silent unknown codes."," Left-merge the project list and report unmatched codes.",[2332,2361,2362,2365],{},[2335,2363,2364],{},"Name mismatches."," \"Ben Okafor\" and \"B. Okafor\" do not match; take names from a dropdown in the template, or better, an employee ID.",[2332,2367,2368,2371],{},[2335,2369,2370],{},"Missing people."," Compare against the staff list; an absent file produces no error on its own.",[329,2373,2375],{"id":2374},"performance-notes","Performance notes",[10,2377,2378,2379,2383],{},"Opening workbooks dominates the run time — roughly tens of milliseconds each in read-only mode — so a few hundred timesheets take seconds. If thousands arrive, read them ",[14,2380,2382],{"href":2381},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fprocess-multiple-excel-files-in-parallel-with-python\u002F","in parallel",".",[329,2385,2387],{"id":2386},"conclusion","Conclusion",[10,2389,2390],{},"Read each timesheet's header cells and grid, stopping at the Total row, and melt the grid into one row per person, project and day. Keep only the latest file per person and week, compare submitters with the staff list, and check project codes, daily maximums and contracted hours. Join the project list, pivot hours by project and person, and write the summary, the traceable detail and the exceptions to one workbook.",[329,2392,2394],{"id":2393},"frequently-asked-questions","Frequently asked questions",[10,2396,2397,2400],{},[2335,2398,2399],{},"How do I combine many timesheet workbooks into one table?","\nLoop over the files, read the header cells (name, week) with openpyxl, read the day-by-project grid below them, and melt the grid so each row is one person, project and day. Concatenate those long tables and pivot as needed.",[10,2402,2403,2406],{},[2335,2404,2405],{},"Why unpivot the timesheet grid?","\nA grid with days as columns is easy to fill in but awkward to total. In long format — one row per person, project and day — hours by project, by person, by client or by day are all a single groupby or pivot_table.",[10,2408,2409,2412],{},[2335,2410,2411],{},"How do I find out who has not submitted a timesheet?","\nCompare the names read from the submitted files with the staff list. Anyone on the staff list with no file for the week is missing; anyone who submitted but is not on the list is a name mismatch to check.",[10,2414,2415,2418],{},[2335,2416,2417],{},"What if someone sends a corrected timesheet?","\nKeep only the latest file per person and week, using the file's modification time or a version in the file name, and log which file replaced which so the correction is traceable.",[329,2420,2422],{"id":2421},"related","Related",[2329,2424,2425,2431,2436,2443],{},[2332,2426,2427,2428,2430],{},"Up one level: ",[14,2429,17],{"href":16}," — the pipeline these reports share.",[2332,2432,2433,2435],{},[14,2434,25],{"href":24}," — the simpler case of plain tables.",[2332,2437,2438,2442],{},[14,2439,2441],{"href":2440},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet\u002F","Extract Report Header Fields from an Excel Sheet"," — reading fields above a table.",[2332,2444,2445,2449],{},[14,2446,2448],{"href":2447},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwatch-a-folder-and-process-new-excel-files\u002F","Watch a Folder and Process New Excel Files"," — consolidating as files arrive.",[2451,2452,2453],"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_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":339,"searchDepth":404,"depth":404,"links":2455},[2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466],{"id":331,"depth":404,"text":332},{"id":374,"depth":404,"text":375},{"id":863,"depth":404,"text":864},{"id":1116,"depth":404,"text":1117},{"id":1945,"depth":404,"text":1946},{"id":2179,"depth":404,"text":2180},{"id":2326,"depth":404,"text":2327},{"id":2374,"depth":404,"text":2375},{"id":2386,"depth":404,"text":2387},{"id":2393,"depth":404,"text":2394},{"id":2421,"depth":404,"text":2422},"2026-09-25","Everyone fills in the same timesheet template and sends it back; you need hours by project and person for the week. Read the name and week from each file's header cells, unpivot the day grid into rows, spot who has not submitted, catch unknown project codes, impossible days and short weeks, and write one consolidated workbook.","md",[2471,2473,2475,2477],{"q":2399,"a":2472},"Loop over the files, read the header cells (name, week) with openpyxl, read the day-by-project grid below them, and melt the grid so each row is one person, project and day. Concatenate those long tables and pivot as needed.",{"q":2405,"a":2474},"A grid with days as columns is easy to fill in but awkward to total. In long format — one row per person, project and day — hours by project, by person, by client or by day are all a single groupby or pivot_table.",{"q":2411,"a":2476},"Compare the names read from the submitted files with the staff list. Anyone on the staff list with no file for the week is missing; anyone who submitted but is not on the list is a name mismatch to check.",{"q":2417,"a":2478},"Keep only the latest file per person and week, using the file's modification time or a version in the file name, and log which file replaced which so the correction is traceable.","consolidate timesheets from many workbooks",null,{"breadcrumb":2482},[2483,2485,2488],{"name":2484,"item":2129},"Home",{"name":2486,"item":2487},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2489,"item":16},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks",{"title":2492,"description":2493},"Consolidate Excel Timesheets from Many Files with Python","Combine timesheet workbooks with openpyxl and pandas: read header cells and a day-by-project grid from a template, melt to long format, handle resubmissions, find missing timesheets, validate project codes and hours, and pivot hours by project and person.","consolidate-timesheets-from-many-workbooks","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-timesheets-from-many-workbooks\u002Findex",[2497,2498,2499],"sheets-and-files","report-layouts","pandas","how-to","WYSgriSoz6VgvULBpAEnwSrT5Or6R1R3oecZg90fxlc",[2503,2507],{"title":2504,"path":2505,"stem":2506,"children":-1},"Consolidate Employee Expense Claims into a Report","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconsolidate-employee-expense-claims-into-a-report\u002Findex",{"title":2508,"path":2509,"stem":2510,"children":-1},"Convert Multi-Currency Transactions with an FX Rate Table","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table\u002Findex",[2512,2514],{"path":2513,"title":2489},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2515,"title":2486},"\u002Fautomating-reporting-workflows",[],[2518,2522,2525,2528,2531,2534],{"path":2519,"title":2520,"kind":2521},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python","Working with Multiple Excel Sheets in Python","guide",{"path":2523,"title":2524,"kind":2500},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fapply-the-same-change-to-every-sheet-in-a-workbook","Apply the Same Change to Every Sheet in a Workbook",{"path":2526,"title":2527,"kind":2500},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcombine-multiple-excel-files-into-one-python","Combine Multiple Excel Files into One in Python",{"path":2529,"title":2530,"kind":2500},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcopy-a-sheet-between-excel-workbooks-with-python","Copy a Sheet Between Excel Workbooks with Python",{"path":2532,"title":2533,"kind":2500},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcreate-a-sheet-for-each-group-in-a-dataframe","Create a Sheet for Each Group in a DataFrame",{"path":2535,"title":2536,"kind":2500},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Flist-every-sheet-with-its-size-and-visibility","List Every Sheet with Its Size and Visibility",1790972127968]