diff --git a/public/FundSight_Budget_Planner.xlsx b/public/FundSight_Budget_Planner.xlsx new file mode 100644 index 0000000..ce2640a Binary files /dev/null and b/public/FundSight_Budget_Planner.xlsx differ diff --git a/scripts/generate_budget_spreadsheet.py b/scripts/generate_budget_spreadsheet.py new file mode 100644 index 0000000..3542091 --- /dev/null +++ b/scripts/generate_budget_spreadsheet.py @@ -0,0 +1,2056 @@ +#!/usr/bin/env python3 +""" +FundSight Budget & Financial Planning Spreadsheet Generator +Creates a comprehensive Excel workbook with full formatting, dropdowns, +conditional formatting, and formulas — mirroring FundSight app features. +""" + +from openpyxl import Workbook +from openpyxl.styles import ( + PatternFill, Font, Alignment, Border, Side, GradientFill +) +from openpyxl.styles.numbers import FORMAT_CURRENCY_USD_SIMPLE +from openpyxl.utils import get_column_letter, column_index_from_string +from openpyxl.formatting.rule import ( + ColorScaleRule, DataBarRule, IconSetRule, FormulaRule, CellIsRule +) +from openpyxl.worksheet.datavalidation import DataValidation +from openpyxl.worksheet.table import Table, TableStyleInfo +from openpyxl.drawing.image import Image +from openpyxl.chart import BarChart, LineChart, PieChart, Reference +from openpyxl.chart.series import SeriesLabel +import datetime +import os + +# ───────────────────────────────────────────── +# COLOUR PALETTE (mirrors FundSight brand) +# ───────────────────────────────────────────── +C = { + # Brand + "emerald": "10B981", + "emerald_dark": "059669", + "emerald_light":"D1FAE5", + "gold": "D4AF37", + "gold_light": "FEF9C3", + "gold_dark": "B7950B", + # Status + "red": "EF4444", + "red_light": "FEE2E2", + "amber": "F59E0B", + "amber_light": "FEF3C7", + "cyan": "06B6D4", + "cyan_light": "CFFAFE", + "purple": "8B5CF6", + "purple_light": "EDE9FE", + # Neutrals + "dark": "1E293B", + "dark2": "334155", + "mid": "64748B", + "light_border": "CBD5E1", + "row_alt": "F8FAFC", + "white": "FFFFFF", + "off_white": "F1F5F9", + # Budget type + "needs": "10B981", + "wants": "F59E0B", + "savings": "06B6D4", +} + +def fill(hex_color): + return PatternFill("solid", fgColor=hex_color) + +def font(bold=False, size=11, color="1E293B", italic=False, name="Calibri"): + return Font(bold=bold, size=size, color=color, italic=italic, name=name) + +def center(): + return Alignment(horizontal="center", vertical="center", wrap_text=True) + +def left(): + return Alignment(horizontal="left", vertical="center", wrap_text=False) + +def right_align(): + return Alignment(horizontal="right", vertical="center") + +def thin_border(sides="all"): + s = Side(style="thin", color=C["light_border"]) + n = Side(style=None) + if sides == "all": + return Border(left=s, right=s, top=s, bottom=s) + if sides == "bottom": + return Border(bottom=s) + if sides == "top_bottom": + return Border(top=s, bottom=s) + if sides == "outer": + return Border(left=s, right=s, top=s, bottom=s) + return Border(bottom=s) + +def medium_border(): + s = Side(style="medium", color=C["dark"]) + return Border(left=s, right=s, top=s, bottom=s) + +# Currency format (GYD) +FMT_GYD = '#,##0.00' +FMT_GYD2 = '"$"#,##0.00' +FMT_PCT = '0.0%' +FMT_DATE = 'DD-MMM-YYYY' +FMT_INT = '#,##0' + +def style_header_cell(cell, text, bg=C["dark"], fg=C["white"], + bold=True, size=11, align="center"): + cell.value = text + cell.fill = fill(bg) + cell.font = font(bold=bold, size=size, color=fg) + cell.alignment = center() if align == "center" else left() + cell.border = thin_border() + +def style_section_title(ws, row, col, text, span=1, + bg=C["emerald"], fg=C["white"]): + cell = ws.cell(row=row, column=col, value=text) + cell.fill = fill(bg) + cell.font = font(bold=True, size=12, color=fg) + cell.alignment = center() + cell.border = thin_border() + if span > 1: + ws.merge_cells( + start_row=row, start_column=col, + end_row=row, end_column=col + span - 1 + ) + +def set_col_width(ws, col_letter, width): + ws.column_dimensions[col_letter].width = width + +def set_row_height(ws, row, height): + ws.row_dimensions[row].height = height + +def zebra_row(ws, row, ncols, start_col=1): + """Apply alternating row fill.""" + for c in range(start_col, start_col + ncols): + cell = ws.cell(row=row, column=c) + if cell.fill.fgColor.rgb in ("00000000", "FFFFFFFF", ""): + cell.fill = fill(C["row_alt"] if row % 2 == 0 else C["white"]) + +# ───────────────────────────────────────────────────────────────────── +# REFERENCE DATA +# ───────────────────────────────────────────────────────────────────── +EXPENSE_CATS = [ + "Groceries & Food", "Utilities", "Transport & Fuel", "Healthcare", + "Entertainment", "Personal Care", "Education", "Rent / Housing", + "Clothing", "Dining Out", "Church / Donations", "Subscriptions", + "Loan Payments", "Savings & Investments", "Childcare", "Pets", + "Gifts & Events", "Miscellaneous", +] +INCOME_CATS = [ + "Salary / Wages", "Freelance / Contract", "Business Income", + "Rental Income", "Investment Returns", "Pension / NIS", + "Government Benefit", "Gift / Support", "Side Hustle", "Other Income", +] +ALL_CATS = EXPENSE_CATS + INCOME_CATS + +ACCOUNT_TYPES = ["Checking", "Savings", "Credit Card", "Investment", "Cash"] +ACCOUNTS = [ + "GBTI Checking", "GBTI Savings", "Republic Bank Checking", + "Republic Bank Savings", "Demerara Bank", "Citizens Bank", + "GPSCCU Savings", "Cash / Wallet", "Investment Account", +] +TX_TYPES = ["Expense", "Income", "Transfer"] +FREQ_TYPES = ["Monthly", "Weekly", "Bi-Weekly", "Quarterly", "Yearly"] +BILL_STATUS = ["Unpaid", "Paid", "Overdue", "Auto-Pay"] +GOAL_CATS = ["Emergency Fund", "Vacation", "House / Property", + "Education", "Retirement", "Vehicle", "Business", "Custom"] +LOAN_TYPES = ["Personal Loan", "Mortgage", "Car Loan", + "Student Loan", "Credit Card Debt", "Business Loan"] +RATE_TYPES = ["Reducing Balance", "Flat Rate"] +NET_WORTH_REASONS = [ + "Regular Update", "New Asset", "New Debt", "Debt Paid Off", + "Asset Sold", "Salary Change", "Investment Gain", "Investment Loss", +] +MONTHS = ["Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"] +BUDGET_TYPES = ["Needs (50%)", "Wants (30%)", "Savings (20%)"] +PRIORITY = ["High", "Medium", "Low"] + +NEEDS_CATS = ["Rent / Housing", "Utilities", "Groceries & Food", + "Transport & Fuel", "Healthcare", "Loan Payments", "Childcare"] +WANTS_CATS = ["Entertainment", "Dining Out", "Clothing", "Subscriptions", + "Gifts & Events", "Personal Care"] +SAVINGS_CATS = ["Savings & Investments", "Education", "Miscellaneous"] + +# Sample budget allocations (GYD amounts) +BUDGET_DEFAULTS = { + "Rent / Housing": 150000, + "Utilities": 35000, + "Groceries & Food": 80000, + "Transport & Fuel": 45000, + "Healthcare": 20000, + "Loan Payments": 60000, + "Childcare": 25000, + "Entertainment": 30000, + "Dining Out": 25000, + "Clothing": 20000, + "Subscriptions": 15000, + "Gifts & Events": 10000, + "Personal Care": 12000, + "Savings & Investments": 60000, + "Education": 20000, + "Miscellaneous": 15000, +} + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 1 — SETTINGS / REFERENCE (hidden drop-down source data) +# ───────────────────────────────────────────────────────────────────── +def build_settings(wb): + ws = wb.create_sheet("⚙ Settings") + ws.sheet_state = "visible" # keep visible so users can customize + + title_row_bg = C["dark"] + + def write_list(start_col, header, items, header_bg=C["emerald"]): + hcell = ws.cell(row=1, column=start_col, value=header) + hcell.fill = fill(header_bg) + hcell.font = font(bold=True, color=C["white"], size=10) + hcell.alignment = center() + hcell.border = thin_border() + ws.column_dimensions[get_column_letter(start_col)].width = 26 + for i, item in enumerate(items, start=2): + c = ws.cell(row=i, column=start_col, value=item) + c.alignment = left() + c.border = thin_border() + c.fill = fill(C["row_alt"] if i % 2 == 0 else C["white"]) + c.font = font(size=10) + + write_list(1, "Expense Categories", EXPENSE_CATS, C["emerald"]) + write_list(2, "Income Categories", INCOME_CATS, C["cyan"]) + write_list(3, "All Categories", ALL_CATS, C["dark"]) + write_list(4, "Accounts", ACCOUNTS, C["gold_dark"]) + write_list(5, "Account Types", ACCOUNT_TYPES, C["dark2"]) + write_list(6, "Transaction Types", TX_TYPES, C["purple"]) + write_list(7, "Frequencies", FREQ_TYPES, C["emerald_dark"]) + write_list(8, "Bill Status", BILL_STATUS, C["red"]) + write_list(9, "Goal Categories", GOAL_CATS, C["cyan"]) + write_list(10, "Loan Types", LOAN_TYPES, C["amber"]) + write_list(11, "Rate Types", RATE_TYPES, C["dark2"]) + write_list(12, "Budget Types", BUDGET_TYPES, C["emerald"]) + write_list(13, "Priority", PRIORITY, C["gold"]) + write_list(14, "NW Reasons", NET_WORTH_REASONS, C["dark"]) + + # Named ranges (used by data-validation in other sheets) + # We embed these as references in each DV call directly (simpler for openpyxl) + + set_row_height(ws, 1, 30) + ws.freeze_panes = "A2" + + # Instructions banner + ws["P1"] = "ℹ️ SETTINGS — Edit lists here to update ALL dropdowns across the workbook" + ws["P1"].fill = fill(C["gold_light"]) + ws["P1"].font = font(bold=True, size=11, color=C["dark"]) + ws["P1"].alignment = left() + ws.column_dimensions["P"].width = 65 + + return ws + +# ───────────────────────────────────────────────────────────────────── +# SHEET 2 — DASHBOARD +# ───────────────────────────────────────────────────────────────────── +def build_dashboard(wb): + ws = wb.create_sheet("📊 Dashboard", 0) + ws.sheet_view.showGridLines = False + + # Row heights + for r in range(1, 80): + set_row_height(ws, r, 22) + set_row_height(ws, 1, 8) # top padding + set_row_height(ws, 2, 50) # title row + + # Column widths + col_widths = {"A":2,"B":18,"C":14,"D":14,"E":14,"F":2,"G":18, + "H":14,"I":14,"J":14,"K":2,"L":22,"M":14,"N":14,"O":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + # ── Title Banner ────────────────────────────────────────────────── + ws.merge_cells("B2:O2") + tc = ws["B2"] + tc.value = "💰 FundSight — Personal Finance & Budget Planner" + tc.fill = fill(C["dark"]) + tc.font = font(bold=True, size=20, color=C["gold"]) + tc.alignment = center() + + # Sub-title / date + ws.merge_cells("B3:O3") + sc = ws["B3"] + sc.value = f"Dashboard Overview | Updated: {datetime.date.today().strftime('%d %B %Y')} | Currency: GYD $" + sc.fill = fill(C["dark2"]) + sc.font = font(size=10, color=C["off_white"], italic=True) + sc.alignment = center() + set_row_height(ws, 3, 20) + + # ── Section: Monthly Snapshot KPI Cards (row 5-9) ───────────────── + set_row_height(ws, 4, 12) + ws.merge_cells("B5:B5") # just label row + kpi_row = 5 + set_row_height(ws, kpi_row, 18) + + def kpi_card(col_letter, label, formula, bg, label_fg="FFFFFF"): + col = column_index_from_string(col_letter) + # label + lc = ws.cell(row=kpi_row, column=col, value=label) + lc.fill = fill(bg) + lc.font = font(bold=True, size=9, color=label_fg) + lc.alignment = center() + ws.merge_cells(start_row=kpi_row, start_column=col, + end_row=kpi_row, end_column=col+1) + # value + set_row_height(ws, kpi_row+1, 32) + vc = ws.cell(row=kpi_row+1, column=col, value=formula) + vc.fill = fill(bg) + vc.font = font(bold=True, size=18, color="FFFFFF") + vc.alignment = center() + vc.number_format = FMT_GYD2 + ws.merge_cells(start_row=kpi_row+1, start_column=col, + end_row=kpi_row+1, end_column=col+1) + # bottom label strip + set_row_height(ws, kpi_row+2, 16) + bc = ws.cell(row=kpi_row+2, column=col, value="This Month") + bc.fill = fill(bg) + bc.font = font(size=8, color=label_fg, italic=True) + bc.alignment = center() + ws.merge_cells(start_row=kpi_row+2, start_column=col, + end_row=kpi_row+2, end_column=col+1) + + kpi_card("B", "📥 TOTAL INCOME", "=SUMIF(Transactions!F:F,\"Income\",Transactions!E:E)", C["emerald"]) + kpi_card("D", "📤 TOTAL EXPENSES", "=SUMIF(Transactions!F:F,\"Expense\",Transactions!E:E)", C["red"]) + kpi_card("G", "💳 TOTAL BILLS DUE", "=SUMIF('Bills & Subs'!G:G,\"Unpaid\",'Bills & Subs'!C:C)",C["amber"]) + kpi_card("I", "🎯 SAVINGS PROGRESS","=SUM('Savings Goals'!C:C)", C["cyan"]) + kpi_card("L", "🏦 TOTAL DEBT", "=SUM('Debt Tracker'!E:E)", C["purple"]) + kpi_card("N", "📊 NET WORTH", "='Net Worth'!H2", C["dark"]) + + # ── Net Balance highlight (row 9) ──────────────────────────────── + set_row_height(ws, 9, 10) + set_row_height(ws, 10, 36) + ws.merge_cells("B10:O10") + nb = ws["B10"] + nb.value = '=\'📊 Dashboard\'!B6 - \'📊 Dashboard\'!D6' + nb.fill = fill(C["emerald_dark"]) + nb.font = font(bold=True, size=22, color="FFFFFF") + nb.alignment = center() + nb.number_format = '"NET BALANCE: $"#,##0.00' + + # ── Section: Budget vs Actual Table (row 12+) ───────────────────── + set_row_height(ws, 11, 10) + ws.merge_cells("B12:J12") + style_section_title(ws, 12, 2, "📋 BUDGET vs ACTUAL — This Month", span=9, bg=C["dark"]) + set_row_height(ws, 12, 24) + + hdrs = ["Category", "Budget Type", "Budgeted ($)", "Actual ($)", + "Remaining ($)", "% Used", "Status"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=13, column=ci) + style_header_cell(c, h, bg=C["dark2"]) + set_row_height(ws, 13, 20) + + bva_data = [ + ("Rent / Housing", "Needs", 150000), + ("Utilities", "Needs", 35000), + ("Groceries & Food", "Needs", 80000), + ("Transport & Fuel", "Needs", 45000), + ("Healthcare", "Needs", 20000), + ("Loan Payments", "Needs", 60000), + ("Entertainment", "Wants", 30000), + ("Dining Out", "Wants", 25000), + ("Clothing", "Wants", 20000), + ("Subscriptions", "Wants", 15000), + ("Personal Care", "Wants", 12000), + ("Savings & Investments","Savings", 60000), + ("Education", "Savings", 20000), + ("Miscellaneous", "Savings", 15000), + ] + + type_colors = {"Needs": C["emerald_light"], "Wants": C["amber_light"], + "Savings": C["cyan_light"]} + + for ri, (cat, btype, budg) in enumerate(bva_data, start=14): + r = ri + set_row_height(ws, r, 20) + bg = type_colors.get(btype, C["white"]) + actual_formula = f'=SUMIF(Transactions!D:D,B{r},Transactions!E:E)' + remain_formula = f'=D{r}-E{r}' + pct_formula = f'=IF(D{r}=0,0,E{r}/D{r})' + status_formula = (f'=IF(F{r}<=0,"🔴 Over Budget",' + f'IF(G{r}>=0.9,"🟡 Near Limit","🟢 On Track"))') + + vals = [cat, btype, budg, actual_formula, + remain_formula, pct_formula, status_formula] + fmts = [None, None, FMT_GYD2, FMT_GYD2, FMT_GYD2, FMT_PCT, None] + + for ci, (v, fmt) in enumerate(zip(vals, fmts), start=2): + cell = ws.cell(row=r, column=ci, value=v) + cell.fill = fill(bg) + cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() if ci >= 4 else left() + if fmt: + cell.number_format = fmt + + # Conditional formatting on % Used column (col H = 8) + pct_range = f"H14:H{13+len(bva_data)}" + ws.conditional_formatting.add(pct_range, CellIsRule( + operator="greaterThanOrEqual", formula=["0.9"], + fill=fill(C["red_light"]), font=font(color=C["red"], bold=True) + )) + ws.conditional_formatting.add(pct_range, CellIsRule( + operator="between", formula=["0.7", "0.9"], + fill=fill(C["amber_light"]), font=font(color="B45309", bold=False) + )) + ws.conditional_formatting.add(pct_range, CellIsRule( + operator="lessThan", formula=["0.7"], + fill=fill(C["emerald_light"]), font=font(color=C["emerald_dark"]) + )) + + bva_end = 13 + len(bva_data) + + # ── Section: 50/30/20 Summary (right side row 12+) ─────────────── + ws.merge_cells("L12:O12") + style_section_title(ws, 12, 12, "🎯 50 / 30 / 20 RULE", span=4, bg=C["dark"]) + + budget_summary = [ + ("NEEDS (50%)", C["emerald"], f'=SUMIF(C14:C{bva_end},"Needs",D14:D{bva_end})'), + ("WANTS (30%)", C["amber"], f'=SUMIF(C14:C{bva_end},"Wants",D14:D{bva_end})'), + ("SAVINGS(20%)", C["cyan"], f'=SUMIF(C14:C{bva_end},"Savings",D14:D{bva_end})'), + ] + for bi, (label, bg, formula) in enumerate(budget_summary, start=13): + set_row_height(ws, bi, 28) + lc = ws.cell(row=bi, column=12, value=label) + lc.fill = fill(bg); lc.font = font(bold=True, size=10, color="FFFFFF") + lc.alignment = center(); lc.border = thin_border() + ws.merge_cells(start_row=bi, start_column=12, + end_row=bi, end_column=13) + vc = ws.cell(row=bi, column=14, value=formula) + vc.fill = fill(bg); vc.font = font(bold=True, size=13, color="FFFFFF") + vc.number_format = FMT_GYD2 + vc.alignment = center(); vc.border = thin_border() + ws.merge_cells(start_row=bi, start_column=14, + end_row=bi, end_column=15) + + # ── Quick Tips (bottom) ─────────────────────────────────────────── + tip_row = bva_end + 2 + ws.merge_cells(f"B{tip_row}:O{tip_row}") + tc2 = ws.cell(row=tip_row, column=2, + value="💡 TIPS: Enter your monthly income in the Income sheet • Log transactions daily in the Transactions sheet • Update goals in Savings Goals • Review debt in Debt Tracker") + tc2.fill = fill(C["gold_light"]) + tc2.font = font(size=9, color=C["dark"], italic=True) + tc2.alignment = left() + set_row_height(ws, tip_row, 24) + + ws.freeze_panes = "B4" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 3 — MONTHLY BUDGET PLANNER +# ───────────────────────────────────────────────────────────────────── +def build_monthly_budget(wb): + ws = wb.create_sheet("💰 Monthly Budget") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":26,"C":14,"D":14,"E":14,"F":14,"G":12,"H":20,"I":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + # Title + ws.merge_cells("B1:H1") + tc = ws["B1"] + tc.value = "💰 MONTHLY BUDGET PLANNER" + tc.fill = fill(C["emerald"]) + tc.font = font(bold=True, size=16, color="FFFFFF") + tc.alignment = center() + set_row_height(ws, 1, 40) + + # Month selector row + ws.merge_cells("B2:C2") + ws["B2"] = "📅 Budget Month:" + ws["B2"].font = font(bold=True, size=11) + ws["B2"].alignment = right_align() + ws["D2"] = datetime.date.today().strftime("%B %Y") + ws["D2"].font = font(bold=True, size=12, color=C["emerald"]) + ws["D2"].fill = fill(C["emerald_light"]) + ws["D2"].alignment = center() + ws["D2"].border = thin_border() + + ws["F2"] = "Monthly Income:" + ws["F2"].font = font(bold=True, size=11) + ws["F2"].alignment = right_align() + ws["G2"] = 500000 # placeholder + ws["G2"].font = font(bold=True, size=12, color=C["emerald_dark"]) + ws["G2"].number_format = FMT_GYD2 + ws["G2"].fill = fill(C["emerald_light"]) + ws["G2"].border = thin_border() + ws["G2"].alignment = center() + set_row_height(ws, 2, 26) + set_row_height(ws, 3, 10) + + # Column headers + hdrs = ["Category", "Budget Type", "Budgeted ($)", + "Actual Spent ($)", "Remaining ($)", "% Used", "Status"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=4, column=ci) + style_header_cell(c, h, bg=C["dark"]) + set_row_height(ws, 4, 22) + + # Data rows grouped by type + groups = [ + ("NEEDS — Essential Expenses (Target: 50% of income)", C["emerald"], + [(cat, "Needs", BUDGET_DEFAULTS.get(cat, 0)) for cat in NEEDS_CATS]), + ("WANTS — Lifestyle Expenses (Target: 30% of income)", C["amber"], + [(cat, "Wants", BUDGET_DEFAULTS.get(cat, 0)) for cat in WANTS_CATS]), + ("SAVINGS & INVESTMENTS (Target: 20% of income)", C["cyan"], + [(cat, "Savings", BUDGET_DEFAULTS.get(cat, 0)) for cat in SAVINGS_CATS]), + ] + + type_fill = {"Needs": C["emerald_light"], "Wants": C["amber_light"], + "Savings": C["cyan_light"]} + + current_row = 5 + data_rows = [] + + for group_label, group_bg, items in groups: + # Group header + ws.merge_cells( + start_row=current_row, start_column=2, + end_row=current_row, end_column=8 + ) + gh = ws.cell(row=current_row, column=2, value=group_label) + gh.fill = fill(group_bg) + gh.font = font(bold=True, size=11, color="FFFFFF") + gh.alignment = left() + gh.border = thin_border() + set_row_height(ws, current_row, 22) + current_row += 1 + + for cat, btype, budg in items: + r = current_row + set_row_height(ws, r, 20) + bg = type_fill[btype] + + actual_f = f'=SUMIF(Transactions!D:D,B{r},Transactions!E:E)' + remain_f = f'=D{r}-E{r}' + pct_f = f'=IF(D{r}=0,0,E{r}/D{r})' + status_f = (f'=IF(G{r}<=0,"🔴 Over Budget",' + f'IF(G{r}>=0.9,"🟡 Near Limit","🟢 On Track"))') + + row_data = [cat, btype, budg, actual_f, remain_f, pct_f, status_f] + fmts = [None, None, FMT_GYD2, FMT_GYD2, FMT_GYD2, FMT_PCT, None] + + for ci, (v, fmt) in enumerate(zip(row_data, fmts), start=2): + cell = ws.cell(row=r, column=ci, value=v) + cell.fill = fill(bg) + cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() if ci >= 4 else left() + if fmt: + cell.number_format = fmt + + # Dropdown for budget type col C + dv_type = DataValidation( + type="list", + formula1='"Needs,Wants,Savings"', + allow_blank=False, + showDropDown=False + ) + dv_type.sqref = f"C{r}" + ws.add_data_validation(dv_type) + + data_rows.append(r) + current_row += 1 + + # Group subtotal + st_row = current_row + set_row_height(ws, st_row, 22) + ws.merge_cells(start_row=st_row, start_column=2, + end_row=st_row, end_column=3) + stc = ws.cell(row=st_row, column=2, value=f" Subtotal") + stc.fill = fill(group_bg); stc.font = font(bold=True, size=10, color="FFFFFF") + stc.border = thin_border(); stc.alignment = left() + + first_r = current_row - len(items) + for ci, fmt in [(4, FMT_GYD2), (5, FMT_GYD2), (6, FMT_GYD2)]: + sc = ws.cell(row=st_row, column=ci, + value=f"=SUM({get_column_letter(ci)}{first_r}:{get_column_letter(ci)}{current_row-1})") + sc.fill = fill(group_bg); sc.font = font(bold=True, size=10, color="FFFFFF") + sc.number_format = fmt; sc.border = thin_border(); sc.alignment = center() + + current_row += 2 # spacing + + # ── Grand Total row ─────────────────────────────────────────────── + tot_row = current_row + set_row_height(ws, tot_row, 26) + ws.merge_cells(start_row=tot_row, start_column=2, + end_row=tot_row, end_column=3) + gtc = ws.cell(row=tot_row, column=2, value="💰 TOTAL BUDGET") + gtc.fill = fill(C["dark"]); gtc.font = font(bold=True, size=12, color=C["gold"]) + gtc.border = thin_border(); gtc.alignment = center() + + if data_rows: + for ci, fmt in [(4, FMT_GYD2),(5, FMT_GYD2),(6, FMT_GYD2)]: + rows_str = "+".join([f"{get_column_letter(ci)}{r}" for r in data_rows]) + tc2 = ws.cell(row=tot_row, column=ci, value=f"={rows_str}") + tc2.fill = fill(C["dark"]); tc2.font = font(bold=True, size=12, color=C["gold"]) + tc2.number_format = fmt; tc2.border = thin_border(); tc2.alignment = center() + + # Unallocated row + unalloc_row = tot_row + 1 + set_row_height(ws, unalloc_row, 22) + ws.merge_cells(start_row=unalloc_row, start_column=2, + end_row=unalloc_row, end_column=3) + uc = ws.cell(row=unalloc_row, column=2, value="💵 Unallocated Income") + uc.fill = fill(C["emerald_light"]); uc.font = font(bold=True, size=10, color=C["emerald_dark"]) + uc.border = thin_border(); uc.alignment = left() + uv = ws.cell(row=unalloc_row, column=4, + value=f"=$G$2-{get_column_letter(4)}{tot_row}") + uv.fill = fill(C["emerald_light"]); uv.font = font(bold=True, size=10, color=C["emerald_dark"]) + uv.number_format = FMT_GYD2; uv.border = thin_border(); uv.alignment = center() + + # Conditional formatting pct column + if data_rows: + pct_col = get_column_letter(7) + pct_rng = f"{pct_col}5:{pct_col}{current_row}" + ws.conditional_formatting.add(pct_rng, CellIsRule( + operator="greaterThanOrEqual", formula=["1"], + fill=fill(C["red_light"]), font=font(color=C["red"], bold=True) + )) + ws.conditional_formatting.add(pct_rng, CellIsRule( + operator="between", formula=["0.9", "1"], + fill=fill(C["amber_light"]), font=font(color="B45309") + )) + ws.conditional_formatting.add(pct_rng, CellIsRule( + operator="lessThan", formula=["0.9"], + fill=fill(C["emerald_light"]), font=font(color=C["emerald_dark"]) + )) + + ws.freeze_panes = "B5" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 4 — INCOME TRACKER +# ───────────────────────────────────────────────────────────────────── +def build_income(wb): + ws = wb.create_sheet("💵 Income") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":12,"C":28,"D":22,"E":16,"F":14,"G":16,"H":14,"I":22,"J":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + ws.merge_cells("B1:I1") + tc = ws["B1"] + tc.value = "💵 INCOME TRACKER" + tc.fill = fill(C["emerald"]) + tc.font = font(bold=True, size=16, color="FFFFFF") + tc.alignment = center() + set_row_height(ws, 1, 40) + + # Summary KPIs + set_row_height(ws, 2, 10) + kpi_labels = ["Total Income This Month","Avg Monthly Income","YTD Income","# Income Sources"] + for ki, kl in enumerate(kpi_labels): + col = 2 + ki * 2 + ws.merge_cells(start_row=3, start_column=col, end_row=3, end_column=col+1) + kc = ws.cell(row=3, column=col, value=kl) + kc.fill = fill(C["dark2"]); kc.font = font(bold=True, size=9, color="FFFFFF") + kc.alignment = center(); kc.border = thin_border() + set_row_height(ws, 3, 18) + set_row_height(ws, 4, 30) + bg = [C["emerald"], C["cyan"], C["gold_dark"], C["dark"]][ki] + ws.merge_cells(start_row=4, start_column=col, end_row=4, end_column=col+1) + vc = ws.cell(row=4, column=col) + if ki == 0: + vc.value = '=SUMIF(F:F,"Income",E:E)' + elif ki == 1: + vc.value = '=IFERROR(SUMIF(F:F,"Income",E:E)/COUNTA(B7:B1000),0)' + elif ki == 2: + vc.value = '=SUMIF(F:F,"Income",E:E)' + else: + vc.value = '=COUNTA(D7:D1000)' + vc.fill = fill(bg); vc.font = font(bold=True, size=16, color="FFFFFF") + vc.number_format = FMT_GYD2 if ki < 3 else FMT_INT + vc.alignment = center(); vc.border = thin_border() + + set_row_height(ws, 5, 10) + + # Column headers + hdrs = ["Date", "Source / Payer", "Category", "Amount ($)", + "Type", "Account", "Recurring?", "Notes"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=6, column=ci) + style_header_cell(c, h, bg=C["emerald"]) + set_row_height(ws, 6, 22) + + # Data validation for income category + dv_cat = DataValidation(type="list", + formula1=f'"{"","".join(INCOME_CATS)}"', + allow_blank=True, showDropDown=False) + dv_cat.sqref = "D7:D1000" + ws.add_data_validation(dv_cat) + + dv_acc = DataValidation(type="list", + formula1=f'"{"","".join(ACCOUNTS)}"', + allow_blank=True, showDropDown=False) + dv_acc.sqref = "G7:G1000" + ws.add_data_validation(dv_acc) + + dv_rec = DataValidation(type="list", + formula1='"Yes,No"', allow_blank=True, showDropDown=False) + dv_rec.sqref = "H7:H1000" + ws.add_data_validation(dv_rec) + + # Sample rows + sample_income = [ + ("2026-04-01", "Employer Ltd", "Salary / Wages", 450000, "Income", "GBTI Checking", "Yes", "April salary"), + ("2026-04-05", "Client A", "Freelance / Contract", 35000, "Income", "Cash / Wallet", "No", "Web project"), + ("2026-04-10", "Rental", "Rental Income", 25000, "Income", "Republic Bank Checking","Yes","Apartment rent"), + ] + for ri, row_data in enumerate(sample_income, start=7): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci, val in enumerate(row_data, start=2): + cell = ws.cell(row=ri, column=ci, value=val) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() if ci >= 5 else left() + if ci == 5: + cell.number_format = FMT_GYD2 + if ci == 2: + cell.number_format = FMT_DATE + + # Empty rows for data entry + for ri in range(10, 52): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci in range(2, 10): + cell = ws.cell(row=ri, column=ci) + cell.fill = fill(bg); cell.border = thin_border() + if ci == 5: + cell.number_format = FMT_GYD2 + + ws.freeze_panes = "B7" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 5 — TRANSACTIONS LOG +# ───────────────────────────────────────────────────────────────────── +def build_transactions(wb): + ws = wb.create_sheet("📝 Transactions") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":12,"C":28,"D":22,"E":14,"F":12, + "G":20,"H":14,"I":14,"J":14,"K":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + ws.merge_cells("B1:J1") + tc = ws["B1"] + tc.value = "📝 TRANSACTION LOG" + tc.fill = fill(C["dark"]) + tc.font = font(bold=True, size=16, color=C["gold"]) + tc.alignment = center() + set_row_height(ws, 1, 40) + + # KPI row + set_row_height(ws, 2, 10) + kpis = [ + ("Total Expenses", '=SUMIF(F:F,"Expense",E:E)', C["red"]), + ("Total Income", '=SUMIF(F:F,"Income",E:E)', C["emerald"]), + ("Net Cash Flow", '=SUMIF(F:F,"Income",E:E)-SUMIF(F:F,"Expense",E:E)', C["cyan"]), + ("# Transactions", '=COUNTA(B6:B10000)', C["dark2"]), + ] + for ki, (label, formula, bg) in enumerate(kpis): + col = 2 + ki * 2 + set_row_height(ws, 3, 18); set_row_height(ws, 4, 30) + ws.merge_cells(start_row=3, start_column=col, end_row=3, end_column=col+1) + lc = ws.cell(row=3, column=col, value=label) + lc.fill = fill(bg); lc.font = font(bold=True, size=9, color="FFFFFF") + lc.alignment = center(); lc.border = thin_border() + ws.merge_cells(start_row=4, start_column=col, end_row=4, end_column=col+1) + vc = ws.cell(row=4, column=col, value=formula) + vc.fill = fill(bg); vc.font = font(bold=True, size=16, color="FFFFFF") + vc.number_format = FMT_GYD2 if ki < 3 else FMT_INT + vc.alignment = center(); vc.border = thin_border() + + set_row_height(ws, 5, 10) + + # Column headers + hdrs = ["Date","Description / Merchant","Category", + "Amount ($)","Type","Account","Notes","Recurring?","Reconciled?"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=6, column=ci) + style_header_cell(c, h, bg=C["dark"]) + set_row_height(ws, 6, 22) + + # Data validations + for dv_range, formula in [ + ("D7:D10000", '"' + '","'.join(ALL_CATS) + '"'), + ("F7:F10000", '"Expense,Income,Transfer"'), + ("G7:G10000", '"' + '","'.join(ACCOUNTS) + '"'), + ("I7:I10000", '"Yes,No"'), + ("J7:J10000", '"Yes,No"'), + ]: + dv = DataValidation(type="list", formula1=formula, + allow_blank=True, showDropDown=False) + dv.sqref = dv_range + ws.add_data_validation(dv) + + # Amount must be positive + dv_num = DataValidation(type="decimal", operator="greaterThan", + formula1="0", allow_blank=True) + dv_num.error = "Amount must be a positive number" + dv_num.errorTitle = "Invalid Amount" + dv_num.sqref = "E7:E10000" + ws.add_data_validation(dv_num) + + # Sample data + today = datetime.date.today() + samples = [ + (today.strftime("%Y-%m-%d"), "Bounty Supermarket", "Groceries & Food", 12500, "Expense", "GBTI Checking", "", "No", "No"), + (today.strftime("%Y-%m-%d"), "GPL Electricity", "Utilities", 18000, "Expense", "GBTI Checking", "", "Yes","No"), + (today.strftime("%Y-%m-%d"), "Employer Ltd", "Salary / Wages", 450000, "Income", "GBTI Checking", "", "Yes","Yes"), + (today.strftime("%Y-%m-%d"), "Chefette Restaurant", "Dining Out", 3500, "Expense", "Cash / Wallet", "", "No", "No"), + (today.strftime("%Y-%m-%d"), "Netflix", "Subscriptions", 4500, "Expense", "Republic Bank Checking", "", "Yes","No"), + (today.strftime("%Y-%m-%d"), "Shell Gasoline", "Transport & Fuel", 9000, "Expense", "GBTI Checking", "", "No", "No"), + (today.strftime("%Y-%m-%d"), "GBTI Savings Transfer","Savings & Investments", 50000, "Transfer","GBTI Savings", "", "Yes","No"), + ] + for ri, row_data in enumerate(samples, start=7): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci, val in enumerate(row_data, start=2): + cell = ws.cell(row=ri, column=ci, value=val) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() if ci >= 5 else left() + if ci == 5: + cell.number_format = FMT_GYD2 + if ci == 2: + cell.number_format = FMT_DATE + + # Empty rows + for ri in range(14, 107): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci in range(2, 11): + cell = ws.cell(row=ri, column=ci) + cell.fill = fill(bg); cell.border = thin_border() + if ci == 5: + cell.number_format = FMT_GYD2 + + # Conditional formatting — highlight income rows green, expense rows red + ws.conditional_formatting.add("B7:J1000", FormulaRule( + formula=["$F7=\"Expense\""], fill=fill("FFF0F0") + )) + ws.conditional_formatting.add("B7:J1000", FormulaRule( + formula=["$F7=\"Income\""], fill=fill("F0FFF4") + )) + + ws.freeze_panes = "B7" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 6 — BILLS & SUBSCRIPTIONS +# ───────────────────────────────────────────────────────────────────── +def build_bills(wb): + ws = wb.create_sheet("📅 Bills & Subs") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":28,"C":14,"D":10,"E":14,"F":16,"G":14,"H":14,"I":16,"J":24,"K":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + ws.merge_cells("B1:J1") + tc = ws["B1"] + tc.value = "📅 BILLS & SUBSCRIPTIONS TRACKER" + tc.fill = fill(C["amber"]) + tc.font = font(bold=True, size=16, color=C["dark"]) + tc.alignment = center() + set_row_height(ws, 1, 40) + + # Summary + set_row_height(ws, 2, 10) + kpis = [ + ("Total Monthly Bills", '=SUMIF(F:F,"Monthly",C:C)', C["dark"]), + ("Unpaid This Month", '=SUMIF(G:G,"Unpaid",C:C)', C["red"]), + ("Auto-Pay Total", '=SUMIF(H:H,"Yes",C:C)', C["emerald"]), + ("Overdue Bills", '=COUNTIF(G:G,"Overdue")', C["red"]), + ] + for ki, (label, formula, bg) in enumerate(kpis): + col = 2 + ki * 2 + ws.merge_cells(start_row=3, start_column=col, end_row=3, end_column=col+1) + lc = ws.cell(row=3, column=col, value=label) + lc.fill = fill(bg); lc.font = font(bold=True, size=9, color="FFFFFF") + lc.alignment = center(); lc.border = thin_border() + set_row_height(ws, 3, 18); set_row_height(ws, 4, 30) + ws.merge_cells(start_row=4, start_column=col, end_row=4, end_column=col+1) + vc = ws.cell(row=4, column=col, value=formula) + vc.fill = fill(bg); vc.font = font(bold=True, size=16, color="FFFFFF") + vc.number_format = FMT_GYD2 if ki < 3 else FMT_INT + vc.alignment = center(); vc.border = thin_border() + + set_row_height(ws, 5, 10) + + hdrs = ["Bill / Subscription Name", "Amount ($)", "Due Day", + "Frequency", "Category", "Status", "Auto-Pay?", "Account", "Notes"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=6, column=ci) + style_header_cell(c, h, bg=C["amber"]) + set_row_height(ws, 6, 22) + + # Validations + for rng, vals in [ + ("F7:F200", '"Monthly,Weekly,Bi-Weekly,Quarterly,Yearly"'), + ("G7:G200", '"Unpaid,Paid,Overdue,Auto-Pay"'), + ("H7:H200", '"Yes,No"'), + ("I7:I200", '"' + '","'.join(ACCOUNTS) + '"'), + ("E7:E200", '"' + '","'.join(EXPENSE_CATS) + '"'), + ]: + dv = DataValidation(type="list", formula1=vals, allow_blank=True, showDropDown=False) + dv.sqref = rng + ws.add_data_validation(dv) + + bills_data = [ + ("GPL Electricity", 18000, 1, "Monthly", "Utilities", "Unpaid", "No", "GBTI Checking", ""), + ("GWI Water", 5500, 5, "Monthly", "Utilities", "Unpaid", "No", "GBTI Checking", ""), + ("GTT Fibre Internet", 9000, 10, "Monthly", "Utilities", "Paid", "Yes", "GBTI Checking", ""), + ("Digicel Mobile", 6000, 15, "Monthly", "Utilities", "Unpaid", "Yes", "GBTI Checking", ""), + ("Netflix", 4500, 8, "Monthly", "Subscriptions", "Paid", "Yes", "Republic Bank Checking", ""), + ("Spotify", 2000, 12, "Monthly", "Subscriptions", "Paid", "Yes", "Republic Bank Checking", ""), + ("Health Insurance", 12000, 1, "Monthly", "Healthcare", "Unpaid", "No", "GBTI Checking", ""), + ("Car Insurance", 8000, 1, "Monthly", "Transport & Fuel", "Unpaid", "No", "GBTI Checking", ""), + ("Gym Membership", 5000, 1, "Monthly", "Personal Care", "Unpaid", "Yes", "Cash / Wallet", ""), + ("Republic Bank Loan", 45000, 25, "Monthly", "Loan Payments", "Unpaid", "No", "Republic Bank Checking", ""), + ("YouTube Premium", 1500, 20, "Monthly", "Subscriptions", "Paid", "Yes", "Republic Bank Checking", ""), + ("Amazon Prime", 3500, 18, "Yearly", "Subscriptions", "Paid", "Yes", "Republic Bank Checking", ""), + ("Starlink Internet", 15000, 3, "Monthly", "Utilities", "Unpaid", "No", "GBTI Checking", ""), + ] + + status_colors = {"Paid": C["emerald_light"], "Unpaid": C["amber_light"], + "Overdue": C["red_light"], "Auto-Pay": C["cyan_light"]} + + for ri, row_data in enumerate(bills_data, start=7): + set_row_height(ws, ri, 20) + status = row_data[6] + bg = status_colors.get(status, C["white"]) + for ci, val in enumerate(row_data, start=2): + cell = ws.cell(row=ri, column=ci, value=val) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() if ci >= 3 else left() + if ci == 3: + cell.number_format = FMT_GYD2 + + for ri in range(7 + len(bills_data), 57): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci in range(2, 11): + cell = ws.cell(row=ri, column=ci) + cell.fill = fill(bg); cell.border = thin_border() + if ci == 3: + cell.number_format = FMT_GYD2 + + # Conditional formatting — overdue highlighted red + ws.conditional_formatting.add("B7:J200", FormulaRule( + formula=['$G7="Overdue"'], fill=fill(C["red_light"]), + font=font(color=C["red"], bold=True) + )) + + ws.freeze_panes = "B7" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 7 — SAVINGS GOALS +# ───────────────────────────────────────────────────────────────────── +def build_savings(wb): + ws = wb.create_sheet("🎯 Savings Goals") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":24,"C":14,"D":14,"E":16,"F":14,"G":12,"H":20,"I":18,"J":16,"K":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + ws.merge_cells("B1:J1") + tc = ws["B1"] + tc.value = "🎯 SAVINGS GOALS TRACKER" + tc.fill = fill(C["cyan"]) + tc.font = font(bold=True, size=16, color=C["dark"]) + tc.alignment = center() + set_row_height(ws, 1, 40) + + # Summary KPIs + set_row_height(ws, 2, 10) + kpis = [ + ("Total Saved", '=SUM(D7:D100)', C["cyan"]), + ("Total Target", '=SUM(C7:C100)', C["dark"]), + ("Overall Progress", '=IFERROR(SUM(D7:D100)/SUM(C7:C100),0)', C["emerald"]), + ("Goals On Track", '=COUNTIF(J7:J100,"🟢 On Track")', C["emerald"]), + ] + for ki, (label, formula, bg) in enumerate(kpis): + col = 2 + ki * 2 + ws.merge_cells(start_row=3, start_column=col, end_row=3, end_column=col+1) + lc = ws.cell(row=3, column=col, value=label) + lc.fill = fill(bg); lc.font = font(bold=True, size=9, color="FFFFFF") + lc.alignment = center(); lc.border = thin_border() + set_row_height(ws, 3, 18); set_row_height(ws, 4, 30) + ws.merge_cells(start_row=4, start_column=col, end_row=4, end_column=col+1) + vc = ws.cell(row=4, column=col, value=formula) + vc.fill = fill(bg); vc.font = font(bold=True, size=16, color="FFFFFF") + vc.number_format = FMT_GYD2 if ki < 2 else (FMT_PCT if ki == 2 else FMT_INT) + vc.alignment = center(); vc.border = thin_border() + + set_row_height(ws, 5, 10) + + hdrs = ["Goal Name","Target ($)","Saved ($)","Monthly Contribution ($)", + "Target Date","% Complete","Months Left","Status"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=6, column=ci) + style_header_cell(c, h, bg=C["cyan"]) + set_row_height(ws, 6, 22) + + # Category dropdown + dv_cat = DataValidation(type="list", + formula1='"Emergency Fund,Vacation,House / Property,Education,Retirement,Vehicle,Business,Custom"', + allow_blank=True, showDropDown=False) + dv_cat.sqref = "B7:B100" + ws.add_data_validation(dv_cat) + + goals_data = [ + ("Emergency Fund", 600000, 280000, 40000, "2026-12-31"), + ("Vacation — Barbados", 250000, 75000, 25000, "2026-08-01"), + ("House Deposit", 5000000,800000, 80000, "2030-01-01"), + ("New Car", 2000000,350000, 60000, "2028-06-01"), + ("Education Fund", 800000, 120000, 20000, "2027-09-01"), + ] + + goal_colors = [C["emerald_light"], C["amber_light"], C["cyan_light"], + C["purple_light"], C["gold_light"]] + + for ri, (name, target, saved, contrib, tdate) in enumerate(goals_data, start=7): + r = ri + set_row_height(ws, r, 22) + bg = goal_colors[(ri - 7) % len(goal_colors)] + + pct_f = f'=IFERROR(D{r}/C{r},0)' + months_f = f'=IFERROR(DATEDIF(TODAY(),G{r},"M"),0)' + status_f = (f'=IF(D{r}>=C{r},"✅ Complete",' + f'IF(IFERROR(D{r}/C{r},0)>=0.75,"🟢 On Track",' + f'IF(IFERROR(D{r}/C{r},0)>=0.4,"🟡 Progressing","🔴 Needs Attention")))') + + row_vals = [name, target, saved, contrib, tdate, pct_f, months_f, status_f] + fmts = [None, FMT_GYD2, FMT_GYD2, FMT_GYD2, FMT_DATE, FMT_PCT, FMT_INT, None] + + for ci, (v, fmt) in enumerate(zip(row_vals, fmts), start=2): + cell = ws.cell(row=r, column=ci, value=v) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() if ci >= 3 else left() + if fmt: + cell.number_format = fmt + + for ri in range(7 + len(goals_data), 37): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci in range(2, 11): + ws.cell(row=ri, column=ci).fill = fill(bg) + ws.cell(row=ri, column=ci).border = thin_border() + + # Data bar for % complete + ws.conditional_formatting.add("H7:H100", + DataBarRule(start_type="num", start_value=0, + end_type="num", end_value=1, + color=C["emerald"])) + + ws.freeze_panes = "B7" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 8 — DEBT TRACKER +# ───────────────────────────────────────────────────────────────────── +def build_debt(wb): + ws = wb.create_sheet("🏦 Debt Tracker") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":24,"C":18,"D":14,"E":14,"F":10,"G":12, + "H":14,"I":14,"J":12,"K":14,"L":20,"M":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + ws.merge_cells("B1:L1") + tc = ws["B1"] + tc.value = "🏦 DEBT TRACKER — Loan Portfolio" + tc.fill = fill(C["purple"]) + tc.font = font(bold=True, size=16, color="FFFFFF") + tc.alignment = center() + set_row_height(ws, 1, 40) + + # Summary KPIs + set_row_height(ws, 2, 10) + kpi_data = [ + ("Total Debt", '=SUM(E7:E100)', C["red"]), + ("Total Min. Payments", '=SUM(H7:H100)', C["amber"]), + ("Avg Interest Rate", '=IFERROR(AVERAGE(F7:F100),0)', C["purple"]), + ("# Active Loans", '=COUNTA(B7:B100)', C["dark2"]), + ] + for ki, (label, formula, bg) in enumerate(kpi_data): + col = 2 + ki * 3 + ws.merge_cells(start_row=3, start_column=col, end_row=3, end_column=col+2) + lc = ws.cell(row=3, column=col, value=label) + lc.fill = fill(bg); lc.font = font(bold=True, size=9, color="FFFFFF") + lc.alignment = center(); lc.border = thin_border() + set_row_height(ws, 3, 18); set_row_height(ws, 4, 32) + ws.merge_cells(start_row=4, start_column=col, end_row=4, end_column=col+2) + vc = ws.cell(row=4, column=col, value=formula) + vc.fill = fill(bg); vc.font = font(bold=True, size=18, color="FFFFFF") + vc.number_format = FMT_GYD2 if ki != 2 and ki != 3 else (FMT_PCT if ki == 2 else FMT_INT) + vc.alignment = center(); vc.border = thin_border() + + set_row_height(ws, 5, 10) + + hdrs = ["Loan / Debt Name","Lender","Original Amount ($)","Balance ($)", + "Interest Rate","Rate Type","Monthly Payment ($)","Total Paid ($)", + "% Paid Off","Due Date","Notes"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=6, column=ci) + style_header_cell(c, h, bg=C["purple"]) + set_row_height(ws, 6, 22) + + for rng, vals in [ + ("C7:C100", '"Personal Loan,Mortgage,Car Loan,Student Loan,Credit Card Debt,Business Loan"'), + ("G7:G100", '"Reducing Balance,Flat Rate"'), + ("K7:K100", '"High Priority,Medium Priority,Low Priority"'), + ]: + dv = DataValidation(type="list", formula1=vals, allow_blank=True, showDropDown=False) + dv.sqref = rng + ws.add_data_validation(dv) + + loans_data = [ + ("Republic Bank Personal Loan", "Republic Bank", 500000, 320000, 0.18, "Reducing Balance", 45000, 180000), + ("Car Loan — Toyota", "Demerara Bank", 1200000, 890000, 0.15, "Reducing Balance", 32000, 310000), + ("Credit Card Balance", "Citizens Bank", 150000, 85000, 0.28, "Flat Rate", 12000, 65000), + ("Student Loan", "GBTI", 300000, 120000, 0.08, "Reducing Balance", 8000, 180000), + ] + + for ri, (name, lender, orig, bal, rate, rtype, payment, paid) in enumerate(loans_data, start=7): + r = ri + set_row_height(ws, r, 22) + bg = C["purple_light"] if ri % 2 == 0 else "F8F7FF" + pct_f = f'=IFERROR(I{r}/D{r},0)' + due_date = 25 + + row_vals = [name, lender, orig, bal, rate, rtype, payment, paid, pct_f, due_date, ""] + fmts = [None, None, FMT_GYD2, FMT_GYD2, FMT_PCT, None, FMT_GYD2, FMT_GYD2, FMT_PCT, None, None] + + for ci, (v, fmt) in enumerate(zip(row_vals, fmts), start=2): + cell = ws.cell(row=r, column=ci, value=v) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() if ci >= 4 else left() + if fmt: + cell.number_format = fmt + + # Empty rows + for ri in range(7 + len(loans_data), 32): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci in range(2, 13): + ws.cell(row=ri, column=ci).fill = fill(bg) + ws.cell(row=ri, column=ci).border = thin_border() + + # ── Debt Strategy section ────────────────────────────────────────── + strat_row = 33 + set_row_height(ws, strat_row, 10) + ws.merge_cells(f"B{strat_row+1}:L{strat_row+1}") + style_section_title(ws, strat_row+1, 2, + "📊 DEBT PAYOFF STRATEGIES — Avalanche vs Snowball", span=11, bg=C["dark"]) + set_row_height(ws, strat_row+1, 24) + + strat_hdrs = ["Strategy","Principle","Best For","Psychological Benefit","Interest Saved"] + for ci, h in enumerate(strat_hdrs, start=2): + ws.cell(row=strat_row+2, column=ci).value = h + ws.cell(row=strat_row+2, column=ci).fill = fill(C["dark2"]) + ws.cell(row=strat_row+2, column=ci).font = font(bold=True, color="FFFFFF", size=10) + ws.cell(row=strat_row+2, column=ci).border = thin_border() + ws.cell(row=strat_row+2, column=ci).alignment = center() + set_row_height(ws, strat_row+2, 20) + + strategies = [ + ("❄️ Avalanche","Pay HIGHEST interest rate first", + "Saving money on interest","Low — math-based","Maximum"), + ("⛄ Snowball","Pay SMALLEST balance first", + "Motivation & quick wins","High — quick victories","Moderate"), + ] + strat_fills = [C["cyan_light"], C["gold_light"]] + for si, (strat, principle, best, psych, saved) in enumerate(strategies): + r = strat_row + 3 + si + set_row_height(ws, r, 22) + for ci, v in enumerate([strat, principle, best, psych, saved], start=2): + cell = ws.cell(row=r, column=ci, value=v) + cell.fill = fill(strat_fills[si]) + cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = left() if ci <= 4 else center() + + # Progress bars for % paid off + ws.conditional_formatting.add("K7:K100", + DataBarRule(start_type="num", start_value=0, + end_type="num", end_value=1, color=C["purple"])) + + ws.freeze_panes = "B7" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 9 — NET WORTH TRACKER +# ───────────────────────────────────────────────────────────────────── +def build_net_worth(wb): + ws = wb.create_sheet("📈 Net Worth") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":14,"C":18,"D":18,"E":18,"F":14,"G":14,"H":18,"I":20,"J":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + ws.merge_cells("B1:I1") + tc = ws["B1"] + tc.value = "📈 NET WORTH TRACKER" + tc.fill = fill(C["dark"]) + tc.font = font(bold=True, size=16, color=C["gold"]) + tc.alignment = center() + set_row_height(ws, 1, 40) + + set_row_height(ws, 2, 10) + kpis = [ + ("Latest Net Worth", "=IFERROR(H2,0)", C["emerald"]), + ("Total Assets", "=IFERROR(C2,0)", C["cyan"]), + ("Total Debt", "=IFERROR(D2,0)", C["red"]), + ("DTI Ratio", "=IFERROR(F2,0)", C["amber"]), + ] + for ki, (label, formula, bg) in enumerate(kpis): + col = 2 + ki * 2 + ws.merge_cells(start_row=3, start_column=col, end_row=3, end_column=col+1) + lc = ws.cell(row=3, column=col, value=label) + lc.fill = fill(bg); lc.font = font(bold=True, size=9, color="FFFFFF") + lc.alignment = center(); lc.border = thin_border() + set_row_height(ws, 3, 18); set_row_height(ws, 4, 32) + ws.merge_cells(start_row=4, start_column=col, end_row=4, end_column=col+1) + vc = ws.cell(row=4, column=col, value=formula) + vc.fill = fill(bg); vc.font = font(bold=True, size=18, color="FFFFFF") + vc.number_format = FMT_GYD2 if ki < 3 else FMT_PCT + vc.alignment = center(); vc.border = thin_border() + + set_row_height(ws, 5, 10) + + hdrs = ["Date","Total Assets ($)","Total Debt ($)","Net Worth ($)", + "DTI Ratio","MoM Change ($)","Savings Rate","Notes / Reason"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=6, column=ci) + style_header_cell(c, h, bg=C["dark"]) + set_row_height(ws, 6, 22) + + # Reason dropdown + dv_reason = DataValidation(type="list", + formula1='"Regular Update,New Asset,New Debt,Debt Paid Off,Asset Sold,Salary Change,Investment Gain,Investment Loss"', + allow_blank=True, showDropDown=False) + dv_reason.sqref = "J7:J500" + ws.add_data_validation(dv_reason) + + # Sample monthly data (last 12 months) + base_nw = 1200000 + base_assets = 3500000 + base_debt = 2300000 + snapshots = [] + for i in range(12, 0, -1): + month_date = datetime.date.today().replace(day=1) + import calendar + month_back = (month_date.month - i) % 12 or 12 + year_back = month_date.year - ((i - month_date.month + 12) // 12) + d = datetime.date(year_back, month_back, 1) + assets = base_assets + i * 45000 + debt = base_debt - i * 18000 + nw = assets - debt + dti = debt / (450000 * 12) + savings_rate = 0.18 + (12 - i) * 0.003 + snapshots.append((d.strftime("%Y-%m-%d"), assets, debt, nw, dti, savings_rate)) + + for ri, (date, assets, debt, nw, dti, sr) in enumerate(snapshots, start=7): + r = ri + set_row_height(ws, r, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + mom_f = f'=E{r}-E{r-1}' if ri > 7 else "" + row_vals = [date, assets, debt, + f'=C{r}-D{r}', dti, mom_f if ri > 7 else 0, sr, "Regular Update"] + fmts = [FMT_DATE, FMT_GYD2, FMT_GYD2, FMT_GYD2, FMT_PCT, FMT_GYD2, FMT_PCT, None] + for ci, (v, fmt) in enumerate(zip(row_vals, fmts), start=2): + cell = ws.cell(row=r, column=ci, value=v) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border() + cell.alignment = center() + if fmt: + cell.number_format = fmt + + # Empty entry rows + for ri in range(7 + len(snapshots), 57): + set_row_height(ws, ri, 20) + bg = C["row_alt"] if ri % 2 == 0 else C["white"] + for ci in range(2, 10): + cell = ws.cell(row=ri, column=ci) + cell.fill = fill(bg); cell.border = thin_border() + + # Conditional formatting — positive net worth change = green + ws.conditional_formatting.add("H7:H500", CellIsRule( + operator="greaterThan", formula=["0"], + fill=fill(C["emerald_light"]), font=font(color=C["emerald_dark"], bold=True) + )) + ws.conditional_formatting.add("H7:H500", CellIsRule( + operator="lessThan", formula=["0"], + fill=fill(C["red_light"]), font=font(color=C["red"], bold=True) + )) + + ws.freeze_panes = "B7" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 10 — ANNUAL SUMMARY +# ───────────────────────────────────────────────────────────────────── +def build_annual(wb): + ws = wb.create_sheet("📊 Annual Summary") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":22} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + for i, m in enumerate(MONTHS): + col = get_column_letter(3 + i) + ws.column_dimensions[col].width = 13 + ws.column_dimensions[get_column_letter(15)].width = 14 # Total + ws.column_dimensions[get_column_letter(16)].width = 2 # padding + + ws.merge_cells(f"B1:{get_column_letter(15)}1") + tc = ws["B1"] + tc.value = "📊 ANNUAL INCOME & EXPENSE SUMMARY" + tc.fill = fill(C["dark"]) + tc.font = font(bold=True, size=16, color=C["gold"]) + tc.alignment = center() + set_row_height(ws, 1, 40) + + # Year selector + ws["B2"] = "Year:" + ws["B2"].font = font(bold=True, size=11) + ws["C2"] = datetime.date.today().year + ws["C2"].font = font(bold=True, size=12, color=C["gold"]) + ws["C2"].fill = fill(C["gold_light"]) + ws["C2"].alignment = center() + ws["C2"].border = thin_border() + set_row_height(ws, 2, 24) + set_row_height(ws, 3, 10) + + # Header row — months + ws.cell(row=4, column=2).value = "Category" + ws.cell(row=4, column=2).fill = fill(C["dark"]) + ws.cell(row=4, column=2).font = font(bold=True, color="FFFFFF", size=10) + ws.cell(row=4, column=2).border = thin_border() + ws.cell(row=4, column=2).alignment = left() + set_row_height(ws, 4, 22) + + for mi, m in enumerate(MONTHS): + c = ws.cell(row=4, column=3+mi, value=m) + c.fill = fill(C["dark2"]); c.font = font(bold=True, color="FFFFFF", size=10) + c.border = thin_border(); c.alignment = center() + + tot_hdr = ws.cell(row=4, column=15, value="TOTAL") + tot_hdr.fill = fill(C["dark"]); tot_hdr.font = font(bold=True, color=C["gold"], size=10) + tot_hdr.border = thin_border(); tot_hdr.alignment = center() + + # INCOME section + income_label_row = 5 + ws.merge_cells(f"B{income_label_row}:{get_column_letter(15)}{income_label_row}") + income_hdr = ws.cell(row=income_label_row, column=2, value="INCOME") + income_hdr.fill = fill(C["emerald"]); income_hdr.font = font(bold=True, color="FFFFFF", size=11) + income_hdr.border = thin_border(); income_hdr.alignment = center() + set_row_height(ws, income_label_row, 20) + + income_rows_start = income_label_row + 1 + income_row_items = INCOME_CATS[:6] # top 6 income categories + for ri, cat in enumerate(income_row_items): + r = income_rows_start + ri + set_row_height(ws, r, 20) + bg = C["emerald_light"] if ri % 2 == 0 else C["white"] + ws.cell(row=r, column=2, value=cat).fill = fill(bg) + ws.cell(row=r, column=2).font = font(size=10) + ws.cell(row=r, column=2).border = thin_border() + for mi in range(12): + cell = ws.cell(row=r, column=3+mi, value=0) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border(); cell.alignment = center() + cell.number_format = FMT_GYD2 + # Row total + row_total = ws.cell(row=r, column=15, + value=f'=SUM(C{r}:N{r})') + row_total.fill = fill(C["emerald_light"]) + row_total.font = font(bold=True, size=10, color=C["emerald_dark"]) + row_total.number_format = FMT_GYD2 + row_total.border = thin_border(); row_total.alignment = center() + + income_total_row = income_rows_start + len(income_row_items) + set_row_height(ws, income_total_row, 22) + ws.cell(row=income_total_row, column=2, value=" Total Income").fill = fill(C["emerald"]) + ws.cell(row=income_total_row, column=2).font = font(bold=True, color="FFFFFF", size=10) + ws.cell(row=income_total_row, column=2).border = thin_border() + for mi in range(12): + col = 3 + mi + fc = ws.cell(row=income_total_row, column=col, + value=f'=SUM({get_column_letter(col)}{income_rows_start}:{get_column_letter(col)}{income_total_row-1})') + fc.fill = fill(C["emerald"]); fc.font = font(bold=True, color="FFFFFF", size=10) + fc.number_format = FMT_GYD2; fc.border = thin_border(); fc.alignment = center() + grand_inc = ws.cell(row=income_total_row, column=15, + value=f'=SUM(C{income_total_row}:N{income_total_row})') + grand_inc.fill = fill(C["emerald_dark"]); grand_inc.font = font(bold=True, color="FFFFFF", size=11) + grand_inc.number_format = FMT_GYD2; grand_inc.border = thin_border(); grand_inc.alignment = center() + + # EXPENSE section + expense_label_row = income_total_row + 2 + ws.merge_cells(f"B{expense_label_row}:{get_column_letter(15)}{expense_label_row}") + exp_hdr = ws.cell(row=expense_label_row, column=2, value="EXPENSES") + exp_hdr.fill = fill(C["red"]); exp_hdr.font = font(bold=True, color="FFFFFF", size=11) + exp_hdr.border = thin_border(); exp_hdr.alignment = center() + set_row_height(ws, expense_label_row, 20) + + expense_rows_start = expense_label_row + 1 + exp_row_items = EXPENSE_CATS[:12] + for ri, cat in enumerate(exp_row_items): + r = expense_rows_start + ri + set_row_height(ws, r, 20) + bg = C["red_light"] if ri % 2 == 0 else C["white"] + ws.cell(row=r, column=2, value=cat).fill = fill(bg) + ws.cell(row=r, column=2).font = font(size=10) + ws.cell(row=r, column=2).border = thin_border() + for mi in range(12): + cell = ws.cell(row=r, column=3+mi, value=0) + cell.fill = fill(bg); cell.font = font(size=10) + cell.border = thin_border(); cell.alignment = center() + cell.number_format = FMT_GYD2 + row_total = ws.cell(row=r, column=15, value=f'=SUM(C{r}:N{r})') + row_total.fill = fill(C["red_light"]) + row_total.font = font(bold=True, size=10, color=C["red"]) + row_total.number_format = FMT_GYD2 + row_total.border = thin_border(); row_total.alignment = center() + + expense_total_row = expense_rows_start + len(exp_row_items) + set_row_height(ws, expense_total_row, 22) + ws.cell(row=expense_total_row, column=2, value=" Total Expenses").fill = fill(C["red"]) + ws.cell(row=expense_total_row, column=2).font = font(bold=True, color="FFFFFF", size=10) + ws.cell(row=expense_total_row, column=2).border = thin_border() + for mi in range(12): + col = 3 + mi + fc = ws.cell(row=expense_total_row, column=col, + value=f'=SUM({get_column_letter(col)}{expense_rows_start}:{get_column_letter(col)}{expense_total_row-1})') + fc.fill = fill(C["red"]); fc.font = font(bold=True, color="FFFFFF", size=10) + fc.number_format = FMT_GYD2; fc.border = thin_border(); fc.alignment = center() + grand_exp = ws.cell(row=expense_total_row, column=15, + value=f'=SUM(C{expense_total_row}:N{expense_total_row})') + grand_exp.fill = fill("CC2222"); grand_exp.font = font(bold=True, color="FFFFFF", size=11) + grand_exp.number_format = FMT_GYD2; grand_exp.border = thin_border(); grand_exp.alignment = center() + + # Net Savings row + net_row = expense_total_row + 2 + set_row_height(ws, net_row, 28) + ws.cell(row=net_row, column=2, value="💰 NET SAVINGS / (DEFICIT)").fill = fill(C["dark"]) + ws.cell(row=net_row, column=2).font = font(bold=True, color=C["gold"], size=11) + ws.cell(row=net_row, column=2).border = thin_border() + for mi in range(12): + col = 3 + mi + fc = ws.cell(row=net_row, column=col, + value=f'={get_column_letter(col)}{income_total_row}-{get_column_letter(col)}{expense_total_row}') + fc.fill = fill(C["dark"]); fc.font = font(bold=True, color=C["gold"], size=10) + fc.number_format = FMT_GYD2; fc.border = thin_border(); fc.alignment = center() + grand_net = ws.cell(row=net_row, column=15, + value=f'=O{income_total_row}-O{expense_total_row}') + grand_net.fill = fill(C["dark"]); grand_net.font = font(bold=True, color=C["gold"], size=12) + grand_net.number_format = FMT_GYD2; grand_net.border = thin_border(); grand_net.alignment = center() + + # Conditional on net row: positive=green, negative=red + net_rng = f"C{net_row}:O{net_row}" + ws.conditional_formatting.add(net_rng, CellIsRule( + operator="greaterThan", formula=["0"], + fill=fill(C["emerald_light"]), font=font(color=C["emerald_dark"], bold=True) + )) + ws.conditional_formatting.add(net_rng, CellIsRule( + operator="lessThan", formula=["0"], + fill=fill(C["red_light"]), font=font(color=C["red"], bold=True) + )) + + ws.freeze_panes = "C5" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 11 — PROJECT COST ESTIMATOR (Bill of Quantities) +# ───────────────────────────────────────────────────────────────────── +def build_project_costs(wb): + ws = wb.create_sheet("🏗 Project Costs") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":6,"C":36,"D":12,"E":14,"F":14,"G":14,"H":14,"I":20,"J":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + # Title + ws.merge_cells("B1:I1") + tc = ws["B1"] + tc.value = "🏗 PROJECT COST ESTIMATOR — Bill of Quantities" + tc.fill = fill(C["dark"]) + tc.font = font(bold=True, size=16, color=C["gold"]) + tc.alignment = center() + set_row_height(ws, 1, 40) + + # Project info block + set_row_height(ws, 2, 10) + info_fields = [ + ("B3","Project Name:", "D3", "New House Construction / Road Works"), + ("B4","Client / Owner:", "D4", ""), + ("B5","Prepared By:", "D5", ""), + ("B6","Date:", "D6", datetime.date.today().strftime("%d %B %Y")), + ("F3","Project Ref #:", "H3", ""), + ("F4","Location:", "H4", ""), + ("F5","Rev. No:", "H5", "Rev. 1"), + ("F6","Currency:", "H6", "GYD $"), + ] + for label_cell, label, val_cell, val in info_fields: + r, c_str = int(label_cell[1:]), label_cell[0] + lc = ws[label_cell] + lc.value = label + lc.font = font(bold=True, size=10) + lc.alignment = right_align() + ws.merge_cells(f"{val_cell}:{chr(ord(val_cell[0])+1)}{val_cell[1:]}") + vc = ws[val_cell] + vc.value = val + vc.font = font(size=10) + vc.fill = fill(C["off_white"]) + vc.border = thin_border() + vc.alignment = left() + set_row_height(ws, r, 22) + + # Section headers row + set_row_height(ws, 7, 10) + hdrs = ["#", "Description of Work", "Unit", "Qty", "Unit Rate ($)", "Amount ($)", "VAT (14%)", "Total ($)", "Notes"] + for ci, h in enumerate(hdrs, start=2): + c = ws.cell(row=8, column=ci) + style_header_cell(c, h, bg=C["dark"]) + set_row_height(ws, 8, 22) + + # Unit dropdown + dv_unit = DataValidation(type="list", + formula1='"LS,m,m²,m³,kg,ton,ft,ft²,ft³,each,set,hr,day,wk"', + allow_blank=True, showDropDown=False) + dv_unit.sqref = "D9:D200" + ws.add_data_validation(dv_unit) + + # Sample BOQ sections with items + boq_sections = [ + ("A — SITE PREPARATION & EARTHWORKS", C["amber"], [ + ("Clear & grub site", "m²", 250, 800), + ("Excavation — topsoil removal (150mm)", "m³", 45, 3500), + ("Excavation — foundations (1.2m deep)", "m³", 80, 5500), + ("Fill & compact with select material", "m³", 60, 4200), + ("Disposal of surplus excavated material", "m³", 40, 2000), + ]), + ("B — CONCRETE WORKS", C["emerald"], [ + ("Blinding concrete (1:3:6 mix) 75mm thick", "m²", 85, 2200), + ("Reinforced concrete foundations (1:2:4)", "m³", 22, 45000), + ("Reinforced concrete columns (1:2:4)", "m³", 8, 52000), + ("Reinforced concrete ground beam", "m³", 12, 48000), + ("Reinforced concrete ground slab 150mm", "m²", 120, 12000), + ("BRC Mesh A193 reinforcement", "m²", 250, 4500), + ]), + ("C — MASONRY & BLOCKWORK", C["cyan"], [ + ("6\" concrete hollow blocks (wall)", "m²", 180, 8500), + ("4\" concrete hollow blocks (partition)", "m²", 60, 7200), + ("Mortar (1:4 cement:sand mix)", "m³", 4, 35000), + ("Surface bond coat both sides", "m²", 480, 1800), + ]), + ("D — ROOFING", C["purple"], [ + ("Timber roof trusses (span 6m)", "each",18, 22000), + ("2×4 purlins @ 600mm c/c", "m", 120, 2800), + ("0.5mm corrugated galv. roofing sheets", "m²", 200, 4500), + ("Ridge capping — galvanised", "m", 18, 3200), + ("Fascia board 1×6 treated timber", "m", 48, 2600), + ("Soffits — fibre cement 6mm", "m²", 55, 5500), + ]), + ("E — DOORS, WINDOWS & FINISHES", C["gold_dark"], [ + ("Timber panel doors (900×2100)", "each", 6, 45000), + ("Aluminium sliding windows (1500×1200)", "each", 10, 38000), + ("Internal plastering — 12mm skim coat", "m²", 480, 3500), + ("Floor tiles (600×600 ceramic)", "m²", 130, 8500), + ("Painting — 2 coats interior", "m²", 620, 1500), + ("Painting — 2 coats exterior", "m²", 300, 2200), + ]), + ("F — ELECTRICAL & PLUMBING", C["cyan"], [ + ("Electrical wiring installation (LS)", "LS", 1,350000), + ("Consumer unit & breakers 12-way", "each", 1, 85000), + ("Plumbing — water supply pipework (LS)", "LS", 1,280000), + ("Plumbing — sanitary fittings (LS)", "LS", 1,320000), + ("Septic tank (2000 litre precast)", "each", 1,180000), + ]), + ("G — LABOUR", C["dark2"], [ + ("Skilled mason — foreman", "day", 90, 12000), + ("Skilled mason", "day", 180, 8500), + ("Unskilled labourer", "day", 200, 5000), + ("Carpenter", "day", 60, 9000), + ("Electrician", "day", 20, 15000), + ("Plumber", "day", 15, 15000), + ]), + ("H — PRELIMINARIES & CONTINGENCIES", C["dark"], [ + ("Site establishment & mobilisation", "LS", 1,150000), + ("Scaffolding & access (hire)", "wk", 16, 25000), + ("Project management & supervision", "LS", 1,200000), + ("Contingencies (5%)", "LS", 1, 0), + ]), + ] + + current_row = 9 + all_total_cells = [] + + for section_label, section_bg, items in boq_sections: + # Section header + ws.merge_cells(f"B{current_row}:I{current_row}") + sh = ws.cell(row=current_row, column=2, value=section_label) + sh.fill = fill(section_bg); sh.font = font(bold=True, size=11, color="FFFFFF") + sh.border = thin_border(); sh.alignment = left() + set_row_height(ws, current_row, 22) + current_row += 1 + + section_start = current_row + for item_idx, (desc, unit, qty, rate) in enumerate(items, start=1): + r = current_row + set_row_height(ws, r, 20) + bg = C["row_alt"] if item_idx % 2 == 0 else C["white"] + + # Col B: item number + nc = ws.cell(row=r, column=2, value=f"{section_label[0]}.{item_idx}") + nc.fill = fill(bg); nc.font = font(size=9, color=C["mid"]) + nc.border = thin_border(); nc.alignment = center() + + # Col C: description + dc = ws.cell(row=r, column=3, value=desc) + dc.fill = fill(bg); dc.font = font(size=10) + dc.border = thin_border(); dc.alignment = left() + + # Col D: unit + uc = ws.cell(row=r, column=4, value=unit) + uc.fill = fill(bg); uc.font = font(size=10) + uc.border = thin_border(); uc.alignment = center() + + # Col E: qty + qc = ws.cell(row=r, column=5, value=qty) + qc.fill = fill(bg); qc.font = font(size=10) + qc.border = thin_border(); qc.alignment = center() + qc.number_format = '#,##0.00' + + # Col F: unit rate + rc = ws.cell(row=r, column=6, value=rate) + rc.fill = fill(bg); rc.font = font(size=10) + rc.border = thin_border(); rc.alignment = right_align() + rc.number_format = FMT_GYD2 + + # Col G: amount = qty * rate + amount_formula = f'=E{r}*F{r}' + if desc.startswith("Contingencies"): + # contingency = 5% of sum so far + section_sum = f'=SUM(G{section_start}:G{r-1})' + amount_formula = f'={section_sum}*0.05' + + ac = ws.cell(row=r, column=7, value=amount_formula) + ac.fill = fill(bg); ac.font = font(size=10) + ac.border = thin_border(); ac.alignment = right_align() + ac.number_format = FMT_GYD2 + + # Col H: VAT + vc2 = ws.cell(row=r, column=8, value=f'=G{r}*0.14') + vc2.fill = fill(bg); vc2.font = font(size=10) + vc2.border = thin_border(); vc2.alignment = right_align() + vc2.number_format = FMT_GYD2 + + # Col I: total incl VAT + tc2 = ws.cell(row=r, column=9, value=f'=G{r}+H{r}') + tc2.fill = fill(bg); tc2.font = font(size=10) + tc2.border = thin_border(); tc2.alignment = right_align() + tc2.number_format = FMT_GYD2 + + current_row += 1 + + # Section subtotal + st_r = current_row + set_row_height(ws, st_r, 22) + ws.merge_cells(f"B{st_r}:F{st_r}") + stc2 = ws.cell(row=st_r, column=2, value=f" Sub-Total {section_label}") + stc2.fill = fill(section_bg); stc2.font = font(bold=True, color="FFFFFF", size=10) + stc2.border = thin_border(); stc2.alignment = left() + + for ci, fmt in [(7, FMT_GYD2), (8, FMT_GYD2), (9, FMT_GYD2)]: + sub = ws.cell(row=st_r, column=ci, + value=f'=SUM({get_column_letter(ci)}{section_start}:{get_column_letter(ci)}{st_r-1})') + sub.fill = fill(section_bg); sub.font = font(bold=True, color="FFFFFF", size=11) + sub.number_format = fmt; sub.border = thin_border(); sub.alignment = right_align() + + all_total_cells.append(f"I{st_r}") + current_row += 2 # gap + + # Grand total + grand_row = current_row + set_row_height(ws, grand_row, 32) + ws.merge_cells(f"B{grand_row}:F{grand_row}") + gtc = ws.cell(row=grand_row, column=2, value=" GRAND TOTAL (incl. VAT)") + gtc.fill = fill(C["dark"]); gtc.font = font(bold=True, color=C["gold"], size=13) + gtc.border = medium_border(); gtc.alignment = center() + + total_formula = "=" + "+".join(all_total_cells) + for ci, fmt in [(7, FMT_GYD2), (8, FMT_GYD2), (9, FMT_GYD2)]: + sub_total = "=" + "+".join([tc.replace("I", get_column_letter(ci)) for tc in all_total_cells]) + gc = ws.cell(row=grand_row, column=ci, value=sub_total) + gc.fill = fill(C["gold_dark"]); gc.font = font(bold=True, color="FFFFFF", size=13) + gc.number_format = fmt; gc.border = medium_border(); gc.alignment = right_align() + + # Note + note_row = grand_row + 2 + ws.merge_cells(f"B{note_row}:I{note_row}") + nc = ws.cell(row=note_row, column=2, + value="NOTE: All rates are in GYD $. VAT applied at 14%. Quantities to be verified on site before work commences. This estimate is valid for 30 days from date of issue.") + nc.fill = fill(C["gold_light"]); nc.font = font(size=9, italic=True, color=C["dark"]) + nc.alignment = left() + set_row_height(ws, note_row, 24) + + ws.freeze_panes = "C9" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 12 — TAX ESTIMATOR +# ───────────────────────────────────────────────────────────────────── +def build_tax(wb): + ws = wb.create_sheet("🧮 Tax Estimator") + ws.sheet_view.showGridLines = False + + col_widths = {"A":2,"B":32,"C":20,"D":20,"E":20,"F":2} + for col, w in col_widths.items(): + set_col_width(ws, col, w) + + ws.merge_cells("B1:E1") + tc = ws["B1"] + tc.value = "🧮 TAX ESTIMATOR — Guyana (GRA) 2025/2026" + tc.fill = fill(C["dark"]) + tc.font = font(bold=True, size=16, color=C["gold"]) + tc.alignment = center() + set_row_height(ws, 1, 40) + + ws.merge_cells("B2:E2") + sc = ws["B2"] + sc.value = "Based on GRA tax rules: 25% flat rate, 1/3 personal allowance, 5.6% NIS employee" + sc.fill = fill(C["dark2"]); sc.font = font(size=9, italic=True, color=C["off_white"]) + sc.alignment = center() + set_row_height(ws, 2, 20) + set_row_height(ws, 3, 10) + + def section_hdr(row, text, bg): + ws.merge_cells(f"B{row}:E{row}") + c = ws.cell(row=row, column=2, value=text) + c.fill = fill(bg); c.font = font(bold=True, size=11, color="FFFFFF") + c.border = thin_border(); c.alignment = left() + set_row_height(ws, row, 22) + + def input_row(row, label, default_val, fmt=FMT_GYD2, note=""): + set_row_height(ws, row, 22) + lc = ws.cell(row=row, column=2, value=label) + lc.font = font(size=10); lc.border = thin_border(); lc.alignment = left() + lc.fill = fill(C["off_white"]) + vc = ws.cell(row=row, column=3, value=default_val) + vc.font = font(bold=True, size=11, color=C["emerald_dark"]) + vc.fill = fill(C["emerald_light"]); vc.border = thin_border() + vc.number_format = fmt; vc.alignment = center() + # highlight as input + vc.fill = PatternFill("solid", fgColor="DCFCE7") + if note: + nc = ws.cell(row=row, column=4, value=note) + nc.font = font(size=9, italic=True, color=C["mid"]); nc.alignment = left() + nc.fill = fill(C["white"]); nc.border = thin_border() + + def calc_row(row, label, formula, fmt=FMT_GYD2, highlight=False): + set_row_height(ws, row, 22) + lc = ws.cell(row=row, column=2, value=label) + lc.font = font(size=10, bold=highlight); lc.border = thin_border() + lc.fill = fill(C["row_alt"]); lc.alignment = left() + vc = ws.cell(row=row, column=3, value=formula) + vc.number_format = fmt; vc.border = thin_border(); vc.alignment = center() + if highlight: + vc.fill = fill(C["gold_light"]) + vc.font = font(bold=True, size=12, color=C["gold_dark"]) + else: + vc.fill = fill(C["row_alt"]); vc.font = font(size=10) + + # INCOME section + section_hdr(4, "📥 INCOME", C["emerald"]) + input_row(5, "Monthly Gross Salary", 450000, FMT_GYD2, "Enter your monthly gross") + input_row(6, "Other Monthly Income", 25000, FMT_GYD2, "Freelance, rental, etc.") + calc_row(7, "Annual Gross Income", "=($C$5+$C$6)*12") + set_row_height(ws, 8, 10) + + # ALLOWANCES section + section_hdr(9, "🔖 ALLOWANCES & DEDUCTIONS", C["cyan"]) + calc_row(10, "Personal Allowance (1/3 of gross or $100K — higher)", + "=MAX(C7/3, 1200000)", FMT_GYD2) + input_row(11, "NIS Employee Contribution (5.6%)", "=$C$7*0.056", FMT_GYD2, "Auto-calculated") + ws.cell(row=11, column=3).value = "=C7*0.056" + ws.cell(row=11, column=3).fill = fill(C["row_alt"]) + ws.cell(row=11, column=3).font = font(size=10) + input_row(12, "Additional Deductions", 0, FMT_GYD2, "Pension scheme, etc.") + calc_row(13, "Total Deductions", "=C10+C11+C12") + set_row_height(ws, 14, 10) + + # TAX CALCULATION + section_hdr(15, "🧮 TAX CALCULATION", C["amber"]) + calc_row(16, "Chargeable Income", "=MAX(0, C7-C13)") + calc_row(17, "Income Tax (25% flat rate)", "=C16*0.25", FMT_GYD2, highlight=True) + calc_row(18, "Effective Tax Rate", "=IFERROR(C17/C7,0)", FMT_PCT) + set_row_height(ws, 19, 10) + + # TAKE HOME + section_hdr(20, "💰 TAKE HOME PAY", C["emerald_dark"]) + calc_row(21, "Annual Net Take-Home", "=C7-C17-C11") + calc_row(22, "Monthly Net Take-Home", "=(C7-C17-C11)/12", FMT_GYD2, highlight=True) + calc_row(23, "Monthly NIS Contribution", "=C11/12", FMT_GYD2) + calc_row(24, "Monthly Tax Withheld (PAYE)", "=C17/12", FMT_GYD2) + set_row_height(ws, 25, 10) + + # EMPLOYER COSTS + section_hdr(26, "🏢 EMPLOYER COSTS (for business owners)", C["dark2"]) + calc_row(27, "NIS Employer Contribution (8.4%)", "=C7*0.084", FMT_GYD2) + calc_row(28, "Total Annual Employer Cost", "=C7+C27", FMT_GYD2, highlight=True) + calc_row(29, "Total Monthly Employer Cost", "=C28/12", FMT_GYD2) + + # Input cells highlighted note + note_row = 31 + ws.merge_cells(f"B{note_row}:E{note_row}") + nc = ws.cell(row=note_row, column=2, + value="💡 GREEN cells are INPUT fields — update your salary and income. All other cells calculate automatically.") + nc.fill = fill(C["emerald_light"]); nc.font = font(size=9, color=C["emerald_dark"]) + nc.alignment = left() + set_row_height(ws, note_row, 22) + + ws.freeze_panes = "B4" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# SHEET 13 — HELP & GUIDE +# ───────────────────────────────────────────────────────────────────── +def build_help(wb): + ws = wb.create_sheet("📖 Help & Guide") + ws.sheet_view.showGridLines = False + + set_col_width(ws, "A", 3) + set_col_width(ws, "B", 30) + set_col_width(ws, "C", 65) + set_col_width(ws, "D", 3) + + ws.merge_cells("B1:C1") + tc = ws["B1"] + tc.value = "📖 FUNDSIGHT BUDGET SPREADSHEET — Help & User Guide" + tc.fill = fill(C["dark"]) + tc.font = font(bold=True, size=16, color=C["gold"]) + tc.alignment = center() + set_row_height(ws, 1, 44) + + ws.merge_cells("B2:C2") + sc = ws["B2"] + sc.value = "Welcome! This spreadsheet mirrors your FundSight app. Use this alongside the app for full financial management." + sc.fill = fill(C["emerald"]); sc.font = font(size=10, italic=True, color="FFFFFF") + sc.alignment = center() + set_row_height(ws, 2, 22) + + sections = [ + ("📊 Dashboard", C["dark"], [ + ("What it shows:", "Live overview of your income, expenses, budget vs actual, and financial KPIs for the current month."), + ("How to use:", "The dashboard auto-calculates from your Transactions and Bills sheets. No manual entry needed here."), + ("Tip:", "Check the 'Net Balance' row to see if you're in the green for the month."), + ]), + ("💰 Monthly Budget", C["emerald"], [ + ("What it does:", "Plan your monthly budget using the 50/30/20 rule — 50% Needs, 30% Wants, 20% Savings."), + ("How to use:", "Update the 'Budgeted ($)' column each month. Actual Spent pulls from Transactions automatically."), + ("Tip:", "Use the Status column to spot overspending early. 🟡 Near Limit = action needed."), + ]), + ("📝 Transactions", C["dark2"], [ + ("What it does:", "Log every income and expense transaction with category, account, and amount."), + ("How to use:", "Enter date, description, select category from dropdown, enter amount, select Income/Expense/Transfer."), + ("Row colours:", "Green rows = Income | Red-tinted rows = Expense | White rows = Transfer"), + ("Tip:", "Use the Notes column for merchant names, invoice numbers, or reminders."), + ]), + ("💵 Income", C["emerald_dark"], [ + ("What it does:", "Dedicated tracker for all income sources — salary, freelance, rental, investments, etc."), + ("How to use:", "Log every income source. The dashboard KPIs pull from this sheet."), + ("Tip:", "Mark recurring income (Yes/No) to track which income is guaranteed vs variable."), + ]), + ("📅 Bills & Subscriptions", C["amber"], [ + ("What it does:", "Track all recurring bills, utilities, and subscriptions with due dates and status."), + ("How to use:", "Update the Status column to Paid as you pay each bill. Mark Auto-Pay = Yes for direct debits."), + ("Tip:", "Overdue bills are highlighted red automatically. Review weekly."), + ]), + ("🎯 Savings Goals", C["cyan"], [ + ("What it does:", "Set and track savings goals with target amounts, dates, and monthly contributions."), + ("How to use:", "Enter goal name, target amount, how much you have saved so far, and your target date."), + ("Progress bars:", "The % Complete column shows a data bar progress indicator."), + ("Tip:", "Update 'Saved ($)' monthly as you contribute to each goal."), + ]), + ("🏦 Debt Tracker", C["purple"], [ + ("What it does:", "Track all loans and debts with balances, interest rates, and payoff progress."), + ("How to use:", "Enter each loan's original amount, current balance, interest rate, and monthly payment."), + ("Strategies:", "Avalanche = pay highest interest first (saves more money). Snowball = pay smallest balance first (more motivation)."), + ("Tip:", "Update the balance monthly as you make payments."), + ]), + ("📈 Net Worth", C["dark"], [ + ("What it does:", "Monthly snapshots of your total assets, debts, and net worth over time."), + ("How to use:", "Add a new row each month with your total assets (bank balances + property + investments) and total debt."), + ("Tip:", "Watch the MoM Change column — consistent positive numbers mean you're building wealth."), + ]), + ("📊 Annual Summary", C["dark2"], [ + ("What it does:", "12-month view of income and expenses by category — see the full year at a glance."), + ("How to use:", "Enter monthly totals for each category manually, or link from your Transactions sheet."), + ("Tip:", "Net Savings row at the bottom goes green for positive months, red for deficit months."), + ]), + ("🏗 Project Costs", C["gold_dark"], [ + ("What it does:", "Full Bill of Quantities (BOQ) for construction, renovation, or any project costing."), + ("How to use:", "Fill in Description, Unit, Quantity, and Unit Rate. Amount, VAT (14%), and Total auto-calculate."), + ("Sections:", "8 pre-built sections: Site Prep, Concrete, Masonry, Roofing, Finishes, MEP, Labour, Preliminaries."), + ("Tip:", "Update project name, client, and date in the header. Print or PDF to share with contractors."), + ]), + ("🧮 Tax Estimator", C["emerald"], [ + ("What it does:", "Estimates your Guyana income tax (PAYE), NIS contributions, and take-home pay."), + ("How to use:", "Enter your monthly gross salary in the green input cell. All other fields auto-calculate."), + ("Tip:", "Shows both employee and employer costs — useful if you run your own business."), + ]), + ("⚙ Settings", C["dark"], [ + ("What it does:", "Master reference lists that populate ALL dropdowns across every sheet."), + ("How to use:", "Add, edit, or remove items from any list here. Changes apply automatically to all dropdowns."), + ("Tip:", "Don't delete column headers. Keep lists in column A of each category."), + ]), + ] + + current_row = 4 + for sheet_name, color, items in sections: + set_row_height(ws, current_row, 10) + current_row += 1 + + ws.merge_cells(f"B{current_row}:C{current_row}") + sh = ws.cell(row=current_row, column=2, value=f" {sheet_name}") + sh.fill = fill(color); sh.font = font(bold=True, size=12, color="FFFFFF") + sh.border = thin_border(); sh.alignment = left() + set_row_height(ws, current_row, 24) + current_row += 1 + + for label, content in items: + set_row_height(ws, current_row, 20) + lc = ws.cell(row=current_row, column=2, value=label) + lc.fill = fill(C["off_white"]); lc.font = font(bold=True, size=10, color=C["dark2"]) + lc.border = thin_border(); lc.alignment = left() + cc = ws.cell(row=current_row, column=3, value=content) + cc.fill = fill(C["white"]); cc.font = font(size=10) + cc.border = thin_border(); cc.alignment = left() + cc.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True) + current_row += 1 + + # Footer + set_row_height(ws, current_row + 1, 32) + ws.merge_cells(f"B{current_row+1}:C{current_row+1}") + fc = ws.cell(row=current_row+1, column=2, + value="🌟 Built to mirror FundSight — Your Financial Command Centre | fundsight.app | Currency: GYD $") + fc.fill = fill(C["dark"]); fc.font = font(bold=True, size=11, color=C["gold"]) + fc.alignment = center() + + ws.freeze_panes = "B3" + return ws + + +# ───────────────────────────────────────────────────────────────────── +# MAIN — assemble and save workbook +# ───────────────────────────────────────────────────────────────────── +def main(): + wb = Workbook() + # Remove default sheet + default_ws = wb.active + wb.remove(default_ws) + + print("Building sheets...") + build_dashboard(wb) + print(" ✓ Dashboard") + build_monthly_budget(wb) + print(" ✓ Monthly Budget") + build_income(wb) + print(" ✓ Income Tracker") + build_transactions(wb) + print(" ✓ Transactions") + build_bills(wb) + print(" ✓ Bills & Subscriptions") + build_savings(wb) + print(" ✓ Savings Goals") + build_debt(wb) + print(" ✓ Debt Tracker") + build_net_worth(wb) + print(" ✓ Net Worth") + build_annual(wb) + print(" ✓ Annual Summary") + build_project_costs(wb) + print(" ✓ Project Cost Estimator") + build_tax(wb) + print(" ✓ Tax Estimator") + build_help(wb) + print(" ✓ Help & Guide") + build_settings(wb) + print(" ✓ Settings") + + # Tab colours + tab_colors = { + "📊 Dashboard": C["dark"], + "💰 Monthly Budget": C["emerald"], + "💵 Income": C["emerald_dark"], + "📝 Transactions": C["dark2"], + "📅 Bills & Subs": C["amber"], + "🎯 Savings Goals": C["cyan"], + "🏦 Debt Tracker": C["purple"], + "📈 Net Worth": C["dark"], + "📊 Annual Summary": C["dark2"], + "🏗 Project Costs": C["gold_dark"], + "🧮 Tax Estimator": C["emerald"], + "📖 Help & Guide": C["gold"], + "⚙ Settings": C["mid"], + } + for ws in wb.worksheets: + name = ws.title + for key, color in tab_colors.items(): + if key in name or name in key: + ws.sheet_properties.tabColor = color + break + + out_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "public", + "FundSight_Budget_Planner.xlsx" + ) + os.makedirs(os.path.dirname(out_path), exist_ok=True) + wb.save(out_path) + print(f"\n✅ Saved: {out_path}") + size_kb = os.path.getsize(out_path) // 1024 + print(f" File size: {size_kb} KB") + print(f" Sheets: {len(wb.worksheets)}") + for i, ws in enumerate(wb.worksheets, 1): + print(f" {i:2d}. {ws.title}") + +if __name__ == "__main__": + main()