"""Read saved Excel values and colours into the management snapshots. Run with the bundled Python runtime (openpyxl required). Never edits the workbook. Usage: python import-management-workbook.py "/path/to/Turnover sheet.xlsx" """ import hashlib import json import re import sys from datetime import datetime from pathlib import Path import openpyxl def number(value): if value is None: return None if isinstance(value, bool) or not isinstance(value, (int, float)): raise ValueError(f"Expected a number, got {value!r}") return value def colour(cell): value = cell.font.color if value is None or (value.type == "theme" and value.theme == 1): return "black" if value.type == "rgb": rgb = value.rgb[-6:].upper() return {"000000": "black", "7030A0": "purple", "FF0000": "red"}.get(rgb, rgb) raise ValueError(f"Unsupported font colour at {cell.coordinate}: {value}") def schedule(sheet): months = {} date = None for row in range(1, sheet.max_row + 1): heading = sheet.cell(row, 2).value if isinstance(heading, datetime): date = heading.strftime("%Y-%m-01") months.setdefault(date, []) name, amount = sheet.cell(row, 3).value, sheet.cell(row, 5).value if date and isinstance(name, str) and isinstance(amount, (int, float)): months[date].append({"id": f"retention-{row}", "project": name.strip(), "debtor": (sheet.cell(row, 4).value or "").strip() or None, "amount": amount}) # Preserve empty months, including gaps in the source's date headings. first, last = min(months), max(months) year, month = map(int, first[:7].split("-")) while f"{year:04d}-{month:02d}-01" <= last: months.setdefault(f"{year:04d}-{month:02d}-01", []) year, month = (year + 1, 1) if month == 12 else (year, month + 1) return [{"date": date, "entries": entries} for date, entries in sorted(months.items())] def main(): source = Path(sys.argv[1]) workbook = openpyxl.load_workbook(source, data_only=True) formulas = openpyxl.load_workbook(source, data_only=False) root = Path(__file__).resolve().parents[1] / "src/lib/management" historical = workbook[" 2024 - 2025"] comparisons = [] for year, rows, result_row in [(2024, [23, 25, 28, 30], 32), (2025, [60, 62, 65, 67], 69)]: comparisons.append({"year": year, **{key: [number(historical.cell(row, col).value) for col in range(3, 15)] for key, row in zip(["income", "costs", "retentionAdded", "retentionPayouts"], rows)}, "reportedCashResult": number(historical.cell(result_row, 15).value), "reportedResultWithRetention": number(historical.cell(result_row + 1, 15).value), "retentionClosing": number(historical.cell(rows[2] + 1, 15).value)}) # December is the annual payout subtotal, not another monthly payout. comparisons[-1]["retentionPayouts"][-1] = None balances = [] for date_row, first_row, retention_row, wip_row, net_row in [(72, 74, 83, 84, 86), (88, 89, 98, 99, 101)]: balances.append({"label": historical.cell(date_row, 5).value.strftime("%B %Y"), "liabilities": [{"name": historical.cell(r, 5).value, "amount": number(historical.cell(r, 6).value)} for r in range(first_row, first_row + 7)], "retention": number(historical.cell(retention_row, 6).value), "wip": number(historical.cell(wip_row, 6).value), "reportedNet": number(historical.cell(net_row, 6).value)}) comparison_sheet = workbook["Income 2026"] income_comparisons = [] for year, date_row, income_row in [(2025, 114, 116), (2026, 119, 121)]: for month, col in enumerate(range(5, 17), 1): date = comparison_sheet.cell(date_row, col).value if not isinstance(date, datetime) or (date.year, date.month) != (year, month): raise ValueError(f"Invalid comparison period at {comparison_sheet.cell(date_row, col).coordinate}") income_comparisons.append({"year": year, "income": [number(comparison_sheet.cell(income_row, col).value) for col in range(5, 17)]}) for year in [2025, 2026, 2027]: name = "Income per month 2025" if year == 2025 else f"Income {year}" sheet, formula = workbook[name], formulas[name] start = 8 if year == 2025 else 7 def labelled_row(label): matches = [r for r in range(1, 50) if str(sheet.cell(r, 3).value or "").strip() == label] if not matches: raise ValueError(f"Missing {label} in {sheet.title}") return matches[0] retention_row = labelled_row("RETENTION") payout_row = labelled_row("RETENTION PAYOUTS") income_row = labelled_row("TOTAL INCOME INCLUDING VAT") cost_row = labelled_row("TOTAL COST INCLUDING VAT") columns = range(start, start + 12) cash_factors, forecast_cost_rates = [], [] for col in columns: income_expression = str(formula.cell(income_row, col).value) factor = re.fullmatch(r"=SUM\([^)]*\)\*(0\.\d+)", income_expression) cash_factors.append(float(factor[1]) if factor else 1) cost_expression = str(formula.cell(cost_row, col).value) rate = re.fullmatch(rf"=[A-Z]+{income_row}\*(0\.\d+)", cost_expression) forecast_cost_rates.append(float(rate[1]) if rate else None) rates = [] for col in columns: expression = formula.cell(retention_row, col).value match = re.fullmatch(r"=SUM\([A-Z]+(\d+):[A-Z]+(\d+)\)\*(0\.\d+)", expression) if not match: raise ValueError(f"Unrecognised retention formula: {name}!{formula.cell(retention_row, col).coordinate} {expression}") rates.append((int(match[1]), int(match[2]), float(match[3]))) projects = [] for row in range(6, retention_row - 1): name_value = sheet.cell(row, 3).value if not isinstance(name_value, str) or not isinstance(sheet.cell(row, 2).value, (int, float)): continue future = colour(sheet.cell(row, 3)) == "red" income = [number(sheet.cell(row, col).value) for col in columns] actual, forecast, future_values = [], [], [] for col, value in zip(columns, income): kind = colour(sheet.cell(row, col)) if value is not None else None if kind not in (None, "black", "purple", "red"): raise ValueError(f"Unknown income colour: {name}!{sheet.cell(row, col).coordinate} {kind}") future_values.append(value if kind == "red" or future else None) actual.append(value if kind == "black" and not future else None) forecast.append(value if kind == "purple" and not future else None) project_rates = [rate if lo <= row <= hi and not future else 0 for lo, hi, rate in rates] costs = None if year == 2025: cost_sheet = workbook["Cost per month"] matches = [r for r in range(7, 22) if cost_sheet.cell(r, 3).value == name_value] if len(matches) > 1: raise ValueError(f"Duplicate cost row for {name_value}") if matches: costs = [number(cost_sheet.cell(matches[0], c).value) for c in range(8, 20)] projects.append({"id": re.sub(r"[^a-z0-9]+", "-", name_value.lower()).strip("-"), "name": name_value, "manager": sheet.cell(row, 4).value if year == 2025 else None, "client": sheet.cell(row, 5 if year == 2025 else 4).value, "engineer": sheet.cell(row, 6 if year == 2025 else 5).value, "statedValue": number(sheet.cell(row, start - 1).value), "remainingValue": number(sheet.cell(row, 19).value) if year == 2026 else None, "income": income, "forecastIncome": forecast, "actualIncome": actual, "futureIncome": future_values, "excludedFromCashflow": future, "costs": costs, "retentionEligible": any(project_rates), "retentionRates": project_rates}) snapshot = {"sourceFile": source.name, "sourceSheet": sheet.title, "planYear": year, "sourceSha256": hashlib.sha256(source.read_bytes()).hexdigest(), "periodLabel": f"March {year}–February {year + 1}", "months": [sheet.cell(3, c).value.strip() for c in columns], "projects": projects, "reportedIncome": [number(sheet.cell(income_row, c).value) for c in columns], "cashflowIncomeFactors": cash_factors, "forecastCostRates": forecast_cost_rates, "reportedCosts": [number(sheet.cell(cost_row, c).value) for c in columns], "reportedCostAnnual": number(workbook["Cost per month"]["G27"].value) if year == 2025 else number(sheet.cell(cost_row, start - 1).value), "reportedRetention": [number(sheet.cell(retention_row, c).value) for c in columns], "reportedRetentionClosing": [number(sheet.cell(retention_row + 1, c).value) for c in columns], "reportedCumulativeBalance": [number(sheet.cell(37, c).value) for c in columns] if year == 2025 else [None] * 12, "openingRetention": number(sheet.cell(retention_row + 1, start - 1).value), "retentionPayouts": [number(sheet.cell(payout_row, c).value) for c in columns], "headOfficeCosts": [number(workbook["Cost per month"].cell(22, c).value) for c in range(8, 20)] if year == 2025 else [0] * 12, "costDetailAvailable": year == 2025, "retentionSchedule": schedule(workbook[f"Retention {year}"]) if year != 2025 else [], "comparisons": comparisons, "incomeComparisons": income_comparisons, "balanceSnapshots": balances} target = root / ("workbook-snapshot.json" if year == 2026 else f"workbook-snapshot-{year}.json") target.write_text(json.dumps(snapshot, indent=2) + "\n") included = [p for p in projects if not p["excludedFromCashflow"]] print(year, len(projects), "jobs", "included income", sum(sum(v or 0 for v in p["income"]) for p in included), "excluded", [p["name"] for p in projects if p["excludedFromCashflow"]]) if __name__ == "__main__": main()