@@ -213,6 +213,27 @@ def __init__(self, filepath):
213213 if summary_name is None :
214214 raise KeyError ("Could not find 'Summary_of_Personnel Costs' sheet in workbook" )
215215 self .summary_sheet = self .wb [summary_name ]
216+ self ._detect_budget_detail_cols ()
217+
218+ def _detect_budget_detail_cols (self ):
219+ """Detect salary year-1 column from Budget Details row 10 header.
220+
221+ Templates differ:
222+ 3-year: COST-yr. 1 at col 17, total at col 20 (17+3)
223+ 5-year: COST-yr. 1 at col 20, total at col 25 (20+5)
224+ 10-year: COST-yr. 1 at col 30, total at col 40 (30+10)
225+
226+ Summary sheet total column follows 4 + years (col 7 / 9 / 14).
227+ """
228+ sheet = self .wb ['Budget Details' ]
229+ self .sal_yr1_col = 20 # fallback (5-year default)
230+ for col in range (14 , 55 ):
231+ val = sheet .cell (10 , col ).value
232+ if val is not None and str (val ).strip () == 'COST-yr. 1' :
233+ self .sal_yr1_col = col
234+ break
235+ self .sal_total_col = self .sal_yr1_col + self .years
236+ self .summary_total_col = 4 + self .years # col 7 for 3yr, 9 for 5yr, 14 for 10yr
216237
217238 def detect_years (self ):
218239 """Auto-detect 3, 5, or 10 year template by scanning year columns"""
@@ -258,18 +279,18 @@ def extract_senior_personnel(self):
258279 pm_value = clean_numeric (sheet .cell (row , 7 + i ).value )
259280 person [f'pm_y{ i + 1 } ' ] = pm_value
260281
261- # Extract salary for each year (columns 20, 21, 22, 23, 24 for 5-year)
282+ # Extract salary for each year
262283 for i in range (self .years ):
263- salary_value = clean_numeric (sheet .cell (row , 20 + i ).value )
284+ salary_value = clean_numeric (sheet .cell (row , self . sal_yr1_col + i ).value )
264285 person [f'salary_y{ i + 1 } ' ] = salary_value
265286
266- # Extract total salary (column 25)
267- person ['total_salary' ] = clean_numeric (sheet .cell (row , 25 ).value )
287+ # Extract total salary
288+ person ['total_salary' ] = clean_numeric (sheet .cell (row , self . sal_total_col ).value )
268289
269290 # Extract total compensation from Summary sheet to calculate fringe
270291 # Summary sheet starts at row 6 for first senior person (row 11 in Budget Details)
271292 summary_row = row - 11 + 6
272- total_compensation = clean_numeric (self .summary_sheet .cell (summary_row , 9 ).value ) # Column I = total
293+ total_compensation = clean_numeric (self .summary_sheet .cell (summary_row , self . summary_total_col ).value )
273294
274295 # Calculate fringe as total_compensation - total_salary
275296 if total_compensation and person ['total_salary' ]:
@@ -352,11 +373,11 @@ def extract_other_personnel(self):
352373
353374 # Extract salary for each year and total
354375 for i in range (self .years ):
355- salary_value = clean_numeric (sheet .cell (row , 20 + i ).value )
376+ salary_value = clean_numeric (sheet .cell (row , self . sal_yr1_col + i ).value )
356377 position [f'salary_y{ i + 1 } ' ] = salary_value
357378
358- # Extract total salary (column 25)
359- position ['total_salary' ] = clean_numeric (sheet .cell (row , 25 ).value )
379+ # Extract total salary
380+ position ['total_salary' ] = clean_numeric (sheet .cell (row , self . sal_total_col ).value )
360381
361382 # Find matching row in Summary sheet to get fringe
362383 # Summary sheet has Other Personnel starting around row 31
@@ -576,7 +597,7 @@ def extract_other_direct_costs_items(self):
576597
577598 # Get yearly values
578599 for i in range (self .years ):
579- year_val = clean_numeric (sheet .cell (row , 20 + i ).value )
600+ year_val = clean_numeric (sheet .cell (row , self . sal_yr1_col + i ).value )
580601 if year_val > 0 :
581602 item ['yearly' ][f'year{ i + 1 } ' ] = year_val
582603
0 commit comments