Skip to content

Commit ac14fd5

Browse files
jared-duvalclaude
andcommitted
Fix salary column detection for 3-year and 10-year templates
Budget Details salary columns are not fixed — they shift by template: 3-year: COST-yr. 1 at col 17, total at col 20 (17+3) 5-year: COST-yr. 1 at col 20, total at col 25 (20+5) 10-year: COST-yr. 1 at col 30, total at col 40 (30+10) The Summary sheet total column follows the same pattern: 4 + years. Detect both dynamically from the Budget Details row 10 header instead of hardcoding the 5-year values everywhere. Fixes personnel, fringe, and ODC totals for all non-5-year templates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d7b6dd8 commit ac14fd5

1 file changed

Lines changed: 30 additions & 9 deletions

File tree

generate_budget_justification.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)