Tax function in Google Sheets

I do my financial budgeting in Google sheets, but I currently just approximate the tax charge. I guess a simple way to do it more accurately is to copy the tables and do some kind of table lookup.

Since there are my Googlers and technical people on here, I wondered if anyone has a good way to:

  1. automatically extract the tax data from the PDFs; and/or
  2. using this data create a custom/named function in Google sheets that would take as inputs the taxable income and tax band/status and output the tax due?

Or have some suggestion as to how to do either one in an efficient way?

I have an excel spreadsheet with a simple formula that calculates the taxes (Bund, Kanton und Gemeinde) based on the tables from ESTV (copy/paste) and taxable income. Send me a PM and I’ll give you a copy, you’d just need to get the tables for your canton and commune. But you’d need to wait until next week as I’m on holidays :slight_smile:

3 Likes

My financial planning is also on Google Sheets. :slight_smile:

TL;DR: DM me for CSV tables

  • Bund
    • direkte Bundessteuern, Verheiratetentarif
    • AHV Nichterwerbstätige
  • Zurich
    • Staatssteuer, Verheiratetentarif
    • Vermögenssteuer, Verheiratetentarif
    • Kapitalbezugssteuer

Details:

I calculate/plan my income, wealth (tax sheltered and not) as well as tax charges with yearly planning out for a couple of decades.*
I even do charts based on when my partner and I fully retire, whether we collect a pension or plan for full or partial payout, etc. You can easily go overboard if you get hooked on the numbers … :smiley:

Specifically to your question, I plan taxes based on my Sheets tax tables and corresponding lookups plus some Sheets formulas.

I’ve got the electronic table for direkte Bundessteuern, Verheiratetentarif from a couple of years ago up to an income of 896k, if that’s useful to you. It looks like this:

Unless I completely misremember I actually downloaded this electronically from some Swiss government website. I feel I would otherwise distinctly remember the pain of typing up a 100+ rows with 3 columns … :wink: … maybe I copy&pasted from PDF and massaged the table in Sheets or so.

I’ve also got copies for (also Verheiratetentarif, Zurich) Vermögenssteuer and Staatssteuer (although these are fairly small tables, I bet you’ll spend more time downloading them from here and uploading them to Google Sheets than just typing them up. Other cantons might vary, of course.

I even have the table for AHV Nichterwerbstätige if you plan on fully retiring early and paying into AHV as long as you have to based on your Vermögen.

Oh, I almost forgot the table for Kapitalbezugssteuern (again for Zurich) if you’re getting close to being eligible and want to opt for Kapitalbezug instead of Rente.

DM me if you’d like copies - forum doesn’t seem to allow to attach CSVs (I hope DMs do. Or we’ll find a way).


* A word of warning: more detailed planning via Sheets lured me into going into lots of detail, e.g. taking into account tax deductions for children you support (for the range of expected years), tax breaks for paying into pillars 2 and 3a, expected returns for the different parts of your wealth (bonus points: splitting pillar 2 returns into obligatorisch and überobligatorisch buckets; ditto for 3a money in different accounts with different portfolios), picking the years when it’s best to do Kapitalbezug taking into account the windows for earliest and latest payout as well as pillars available for payout for you and (if applicable) your partner … you name it!

Edit: Accidentally posted while adding the TLDR.

2 Likes

For AHV Nichterwerbstätige I used the formula: =if((P4)>1740,(3.5828+rounddown((P4-1740)/50)*0.159),rounddown((P4))*0.00212-0.106)

Currently, I copy/paste PDFs and do a bit of manual jiggling in Sheets. Though I suspect the tax tables could be replaced by a more simple formula. Maybe I should look into that as an alternative.

I also added the tax breaks for kids (since you anyway have different tax rates for the kids/no kids periods). Pilar 2/3a I have ignored as I am looking purely at the post-retirement period.

1 Like

I feel you, my personal finance dashboard has grown way beyond a simple spreadsheet :rofl:

1 Like

I just downloaded the Federal PDF. You don’t remember copying it because you can nicely select the tax and income columns and paste them cleanly into Sheets (unlike Baselland’s multi-column monster).

Checking the tax, it seems that they are linear functions at different income bands so should be easy to approximate with a handful of formulas.

1 Like

I started going down the rabbit hole of calculating the formulas, but wondered if it even makes sense as the steps change quite regularly and you anyway need a sort of lookup to figure out which equation to use. Maybe it is better just to do it as the table look-up.

1 Like

Can’t really speak to your formula for AHV Nichterwerbstätige - though some of the magic numbers in your formula correspond to numbers in the spreadsheet that I am using - but this is one of my more complicated sections in my spreadsheet as it also hinges on “normal” income versus Renteneinkommen.
The detailed rules have been mentioned before on this forum: Beiträge der Nichterwerbstätigen an die AHV, die IV und die EO.

The corresponding section in my spreadsheet contains a bunch of rows required for calculating this

image

and I am sure I’ll be able to revisit and reconstruct it. My appetite for doing this again is … um, I’ll wait for until I actually have to do it. :wink:

Oh, I do table lookups (and additional formulas as needed).

I’ll admit, though, that I haven’t updated the tables since my initial populating the lookup tables ins 2020. :slight_smile:

Anyway, if (when?) I update the tables, it will just nicely update the entire spreadsheet, which seems like less of a burden to me.

Edit: typos.

Completely off topic:

Anecdote: initially, I typically sized (via copy&pase) my Google spreadsheets to contain in the order of 10k rows and probably about 100 columns or so. Just so I wouldn’t have to bother with “extend with 100 more rows/columns” every time my sheet grew a little bigger. Dozens of such sub-sheets in the most egregious Sheets of mine.

Because … you know, roughly following GMail’s initial motto: “never delete an email ever again (maybe archive if you’re old fashioned)”.*

Admittedly stupid assumption of mine, of course, but if you drink enough of the Kool-Aid (especially the free internal one available), you may believe the smooth talking.

Anyway …

Eventually I ran into Google’s limit (about a quarter ago or so) of exceeding a million cells per spreadsheet (even if most cells - like 90+% - were empty) I am since restricting my sheets to mostly to just what I need in terms of number of rows and columns.

I actually now kind of almost feel nostalgic about paying attention to resource limits. This was already a thing right around when I first came into contact with computing about 30-40 years ago. :slight_smile:
(and of course so even much much earlier)


* Yup, Google revised this. Storage actually costs money. Who knew?

My function takes the calculated wealth value for AHV so the complexities of capitalizing your income by 20 times etc. lies elsewhere.

In the end, I used table lookup for Federal tax.

For Baselland, I used a polynomial approximation:

Single without kids:

-1601 + 0.0568x + 2.28E-06x^2 + -1.18E-11x^3 + 3.27E-17x^4 + -4.54E-23x^5 + 2.49E-29x^6

Single with kids:

-1445 + -0.011x + 2.01E-06x^2 + -8E-12x^3 + 1.95E-17x^4 + -2.92E-23x^5 + 2.62E-29x^6 + -1.29E-35x^7 + 2.69E-42x^8

For those in BL wanting to use it, note that it includes a 1.58 multiplier for the Gemeinde; you will need to scale it if your multiplier is different.

Not really helpful I guess and has some terrible Spaghetti code too, but this is the function I ended up with for my convoluted “Jupyter notebook of life, the universe and everything”. Speaking of rabbit holes… But maybe somebody gets a kick out of it.

Note that the DA-1 code is not validated, while it gets close to the actual figure it is still somehow a bit off in a way I cannot explain. The rest gets quite close to the actual tax bill figures.

# calculate tax, based a) on previous year's income & assets (provisional) or b) on final income & assets.

def Tax(Year, TaxType='provisional'):
    import pandas as pd
    import datetime
    
    if TaxType == 'provisional':
        Offset = 1
    elif TaxType == 'final':
        Offset = 0
    
    ChildAge = (datetime.date(Year, 1, 1) - DoBChild).days / 365.2425
    
    # import relevant variables from DataFrame
    TotalTaxableIncomeSpouse1 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Type'] == 'Income Taxed') &
                                          (df_portfolio['Person'] == 'Spouse1'),
                                          'Value'].sum() + df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Type'] == 'Income Deductions') &
                                          (df_portfolio['Person'] == 'Spouse1'),
                                          'Value'].sum() + df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == 'Child Benefit'),
                                          'Value'].sum()

    NetDividendIncomeSpouse1 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == 'Dividends') &
                                          (df_portfolio['Person'] == 'Spouse1'),
                                          'Value'].sum()
    
    PensionIncomeSpouse1 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == '1st Pillar Pension') &
                                          (df_portfolio['Person'] == 'Spouse1'),
                                          'Value'].sum()
    
    VoluntaryDeductionsSpouse1 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == '2nd Pillar Voluntary Deduction') &
                                          (df_portfolio['Person'] == 'Spouse1'),
                                          'Value'].sum() + df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == '3rd Pillar Deduction') &
                                          (df_portfolio['Person'] == 'Spouse1'),
                                          'Value'].sum()

    TotalTaxableIncomeSpouse2 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Type'] == 'Income Taxed') &
                                          (df_portfolio['Person'] == 'Spouse2'),
                                          'Value'].sum() + df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Type'] == 'Income Deductions') &
                                          (df_portfolio['Person'] == 'Spouse2'),
                                          'Value'].sum()

    NetDividendIncomeSpouse2 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == 'Dividends') &
                                          (df_portfolio['Person'] == 'Spouse2'),
                                          'Value'].sum()
    
    PensionIncomeSpouse2 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == '1st Pillar Pension') &
                                          (df_portfolio['Person'] == 'Spouse2'),
                                          'Value'].sum()
    
    VoluntaryDeductionsSpouse2 = df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == '2nd Pillar Voluntary Deduction') &
                                          (df_portfolio['Person'] == 'Spouse2'),
                                          'Value'].sum() + df_portfolio.loc[(df_portfolio['Year'] == Year - Offset) &
                                          (df_portfolio['Name'] == '3rd Pillar Deduction') &
                                          (df_portfolio['Person'] == 'Spouse2'),
                                          'Value'].sum()

    TotalTaxableAssets = df_portfolio['Value'].loc[(df_portfolio['Year'] == Year - Offset) & 
                                              (df_portfolio['Type'] == 'Capital Taxable Assets')].sum()

    # remove dividends, voluntary 2nd & 3rd pillar deductions for tax assessment (item 100 / 101 in tax return)
    NetEmploymentIncomeSpouse1 = TotalTaxableIncomeSpouse1 - (NetDividendIncomeSpouse1 + 
                                                              VoluntaryDeductionsSpouse1 + 
                                                              PensionIncomeSpouse1)
    NetEmploymentIncomeSpouse2 = TotalTaxableIncomeSpouse2 - (NetDividendIncomeSpouse2 + 
                                                              VoluntaryDeductionsSpouse2 + 
                                                              PensionIncomeSpouse2)

    # total dividends with US withholding tax removed for CH tax assessment
    TotalDividendIncome = ((NetDividendIncomeSpouse1 + NetDividendIncomeSpouse2) / 0.85)

    # calculate and remove flat rate portfolio administration allowance, resulting in item 150 in tax return
    if TotalTaxableAssets < 3000000:
        PortfolioAdminFlatRateDeduction = TotalTaxableAssets * 0.003
    else:
        PortfolioAdminFlatRateDeduction = (TotalTaxableAssets - 3000000) * 0.001 + 9000

    TaxableIncomeFromPortfolio = TotalDividendIncome - PortfolioAdminFlatRateDeduction

    TotalNetTaxableIncome = (NetEmploymentIncomeSpouse1 + 
                             NetEmploymentIncomeSpouse2 + 
                             PensionIncomeSpouse1 + 
                             PensionIncomeSpouse2 + 
                             TaxableIncomeFromPortfolio) # item 199 in tax return
    
    # professional deductable expenditures (item 238 / 239 in tax return)
    if NetEmploymentIncomeSpouse1 > 0:
        Spouse1ProfessionalDeductions = 1490 + 3200 + min(max(2000, NetEmploymentIncomeSpouse1 * 0.03), 4000)
    else:
        Spouse1ProfessionalDeductions = 0

    if NetEmploymentIncomeSpouse2 > 0:
        Spouse2ProfessionalDeductions = 1490 + 3200 + min(max(2000, NetEmploymentIncomeSpouse2 * 0.03), 4000)
    else:
        Spouse2ProfessionalDeductions = 0

    # dual income deductions (assumes that spouse1 is having a higher income than spouse2, needs additional logic if not always the case)
    if NetEmploymentIncomeSpouse2 > 0:
        CantonalDualIncomeDeduction = 4700
        FederalDualIncomeDeduction = max(8100, min(13400, NetEmploymentIncomeSpouse2 * 0.5))
    else:
        CantonalDualIncomeDeduction = 0
        FederalDualIncomeDeduction = 0
    
    # intermediate sums of deductions to check against individual deduction cap
    
    IncomeRelatedDeductionsSpouse1 = Spouse1ProfessionalDeductions
    if (NetEmploymentIncomeSpouse1 + VoluntaryDeductionsSpouse1) < IncomeRelatedDeductionsSpouse1:
        IncomeRelatedDeductionsSpouse1 = max(0, (NetEmploymentIncomeSpouse1 + VoluntaryDeductionsSpouse1))
    
    CantonalIncomeRelatedDeductionsSpouse2 = Spouse2ProfessionalDeductions + CantonalDualIncomeDeduction
    if (NetEmploymentIncomeSpouse2 + VoluntaryDeductionsSpouse2) < CantonalIncomeRelatedDeductionsSpouse2:
        CantonalIncomeRelatedDeductionsSpouse2 = max(0, (NetEmploymentIncomeSpouse2 + VoluntaryDeductionsSpouse2))
        
    FederalIncomeRelatedDeductionsSpouse2 = Spouse2ProfessionalDeductions + FederalDualIncomeDeduction
    if (NetEmploymentIncomeSpouse2 + VoluntaryDeductionsSpouse2) < FederalIncomeRelatedDeductionsSpouse2:
        FederalIncomeRelatedDeductionsSpouse2 = max(0, (NetEmploymentIncomeSpouse2 + VoluntaryDeductionsSpouse2))
    
    # deduction for insurance premiums (item 270 in tax return)
    CantonalInsurancePremiumDeduction = 4900
    FederalInsurancePremiumDeduction = 3500
    if ChildAge < 21:
        CantonalInsurancePremiumDeduction = CantonalInsurancePremiumDeduction + 700
        FederalInsurancePremiumDeduction = FederalInsurancePremiumDeduction + 700
    
    # child deductions
    if ChildAge < 14:
        CantonalChildDeductions = 8200
        FederalChildDeductions = 6500
    elif ChildAge < 21:
        CantonalChildDeductions = 7200
        FederalChildDeductions = 6500
    else:
        CantonalChildDeductions = 0
        FederalChildDeductions = 0
    
    # federal only deduction for married people
    FederalDeductionMarriedStatus = 2600
    
    # cantonal taxable income calculation
    CantonalNetIncome = (TotalNetTaxableIncome + 
                             VoluntaryDeductionsSpouse1 + 
                             VoluntaryDeductionsSpouse2) - (IncomeRelatedDeductionsSpouse1 + 
                                                            CantonalIncomeRelatedDeductionsSpouse2 +
                                                            CantonalInsurancePremiumDeduction)
    CantonalNetIncome = max(0, CantonalNetIncome)
    
    CantonalTaxableIncome = CantonalNetIncome - CantonalChildDeductions
    CantonalTaxableIncome = max(0, CantonalTaxableIncome)
        
    # federal taxable income calculation
    FederalNetIncome = (TotalNetTaxableIncome + 
                             VoluntaryDeductionsSpouse1 + 
                             VoluntaryDeductionsSpouse2) - (IncomeRelatedDeductionsSpouse1 + 
                                                            FederalIncomeRelatedDeductionsSpouse2 +
                                                            FederalInsurancePremiumDeduction)
    FederalNetIncome = max(0, FederalNetIncome)
    
    FederalTaxableIncome = FederalNetIncome - (FederalChildDeductions + FederalDeductionMarriedStatus)
    FederalTaxableIncome = max(0, FederalTaxableIncome)
    
    # asset deductions
    if ChildAge < 21:
        AssetDeductions = 137000
    else:
        AssetDeductions = 125000
    NetTaxableAssets = TotalTaxableAssets - AssetDeductions

    # cantonal tax (married/with children, from https://swisstaxcalculator.estv.admin.ch/#/taxdata/tax-scales)
    
    if CantonalTaxableIncome < 18800:
        CantonalIncomeTax = 0
    elif CantonalTaxableIncome < 22700:
        CantonalIncomeTax = (CantonalTaxableIncome - 18800) * 0.005
    elif CantonalTaxableIncome < 23700:
        CantonalIncomeTax = (CantonalTaxableIncome - 22700) * 0.015 + 20
    elif CantonalTaxableIncome < 24800:
        CantonalIncomeTax = (CantonalTaxableIncome - 23700) * 0.025 + 35
    elif CantonalTaxableIncome < 26800:
        CantonalIncomeTax = (CantonalTaxableIncome - 24800) * 0.03 + 62
    elif CantonalTaxableIncome < 30800:
        CantonalIncomeTax = (CantonalTaxableIncome - 26800) * 0.035 + 122
    elif CantonalTaxableIncome < 93800:
        CantonalIncomeTax = (CantonalTaxableIncome - 30800) * 0.045 + 262
    elif CantonalTaxableIncome < 130600:
        CantonalIncomeTax = (CantonalTaxableIncome - 93800) * 0.05 + 3097
    elif CantonalTaxableIncome < 150600:
        CantonalIncomeTax = (CantonalTaxableIncome - 130600) * 0.055 + 4937
    else:
        CantonalIncomeTax = (CantonalTaxableIncome - 150600) * 0.058 + 6037
        
    # cantonal wealth tax
    CantonalWealthTax = NetTaxableAssets * 0.000875
    CantonalWealthTax = max(0, CantonalWealthTax)
    
    # apply combined cantonal + communal tax multipliers
    CantonalIncomeTax = round(CantonalIncomeTax * 3.45)
    CantonalWealthTax = round(CantonalWealthTax * 3.45)
    
    # add fire service duty to cantonal tax
    Spouse1Age = (datetime.date(Year, 1, 1) - DoBSpouse1).days / 365.2425
    Spouse2Age = (datetime.date(Year, 1, 1) - DoBSpouse2).days / 365.2425
    
    if (Spouse1Age < 51) and (Spouse2Age < 51):
        FireServiceDuty = round(CantonalTaxableIncome * 0.0045)
        FireServiceDuty = max(60, FireServiceDuty)
        FireServiceDuty = min(800, FireServiceDuty)
    elif (Spouse1Age < 51) or (Spouse2Age < 51):
        FireServiceDuty = round(CantonalTaxableIncome * 0.0015)
        FireServiceDuty = max(10, FireServiceDuty)
        FireServiceDuty = min(133, FireServiceDuty)
    else:
        FireServiceDuty = 0
    
    # 'personal' flat rate tax to cantonal tax
    PersonalFlatRateTax = 50
    
    # add up cantonal (& communal) level tax
    CantonalTax = CantonalIncomeTax + CantonalWealthTax + FireServiceDuty + PersonalFlatRateTax
    
 
    # federal tax (married/with children, from https://swisstaxcalculator.estv.admin.ch/#/taxdata/tax-scales)
    if FederalTaxableIncome < 28300:
        FederalTax = 0
    elif FederalTaxableIncome < 50900:
        FederalTax = (FederalTaxableIncome - 28300) * 0.01
    elif FederalTaxableIncome < 58400:
        FederalTax = (FederalTaxableIncome - 50900) * 0.02 + 226
    elif FederalTaxableIncome < 75300:
        FederalTax = (FederalTaxableIncome - 58400) * 0.03 + 376
    elif FederalTaxableIncome < 90300:
        FederalTax = (FederalTaxableIncome - 75300) * 0.04 + 883
    elif FederalTaxableIncome < 103400:
        FederalTax = (FederalTaxableIncome - 90300) * 0.05 + 1483
    elif FederalTaxableIncome < 114700:
        FederalTax = (FederalTaxableIncome - 103400) * 0.06 + 2138
    elif FederalTaxableIncome < 124200:
        FederalTax = (FederalTaxableIncome - 114700) * 0.07 + 2816
    elif FederalTaxableIncome < 131700:
        FederalTax = (FederalTaxableIncome - 124200) * 0.08 + 3481
    elif FederalTaxableIncome < 137300:
        FederalTax = (FederalTaxableIncome - 131700) * 0.09 + 4081
    elif FederalTaxableIncome < 141200:
        FederalTax = (FederalTaxableIncome - 137300) * 0.1 + 4585
    elif FederalTaxableIncome < 143100:
        FederalTax = (FederalTaxableIncome - 141200) * 0.11 + 4975
    elif FederalTaxableIncome < 145000:
        FederalTax = (FederalTaxableIncome - 143100) * 0.12 + 5184
    else:
        FederalTax = (FederalTaxableIncome - 145000) * 0.13 + 5412

    # child deduction from federal tax, if applicable
    if ChildAge < 21:
        FederalChildDeduction = 251
    else:
        FederalChildDeduction = 0
    
    FederalTax = FederalTax - FederalChildDeduction
    FederalTax = max(0, FederalTax)
    
    
    # DA-1 double taxation claim:
    DA1Claim = TotalDividendIncome * 0.15
    # Max. DA-1 claim, capped on maximum of CH tax on this income (max tax rate to be confirmed / calculated, TBD)
    # Also assumes that all dividends result in a DA-1 claim, e.g. no CH stocks
    
    if CantonalNetIncome > 0:
        EffectiveCantonalTaxRate = CantonalIncomeTax / CantonalNetIncome
    else:
        EffectiveCantonalTaxRate = 0
    
    if FederalNetIncome > 0:
        EffectiveFederalTaxRate = FederalTax / FederalNetIncome
    else:
        EffectiveFederalTaxRate = 0
    
    DA1Max = (TotalDividendIncome - PortfolioAdminFlatRateDeduction) * (EffectiveCantonalTaxRate + EffectiveFederalTaxRate) 
    
    DA1Credit = round(min(DA1Claim, DA1Max, (CantonalIncomeTax + FederalTax)))

    # invert tax since it is an outflow
    CantonalTax = round(CantonalTax * -1)
    FederalTax = round(FederalTax * -1)
    
    data = {
        'Year': [Year, Year, Year],
        'Type': ['Tax', 'Tax','Tax'],
        'Name': ['Cantonal Income and Capital Tax', 'Federal Tax', 'DA-1 Tax Credit'],
        'Person' : ['All', 'All', 'All'],
        'Value': [CantonalTax, FederalTax, DA1Credit]
    }
    
    df_Tax = pd.DataFrame(data)

    return df_Tax
2 Likes

Hi everyone, not sure if that has already been mentioned, but today I went through the pain of extracting all tax data from a PDF, only to find afterwards the following resource:

This is very simple, very comprehensive, easily download-able. I checked and it provides the same info than the PDF, in a much more condensed form.

4 Likes

Wow. Great find!

20 chars

1 Like

Maybe someone can help me. How do these formulas:

Relate to these tables? Maybe I’m doing the calculation wrong.