Skip to content

Instantly share code, notes, and snippets.

@masasin
Last active August 9, 2016 13:45
Show Gist options
  • Save masasin/f0c56881a141e594085ee5da8db98a0f to your computer and use it in GitHub Desktop.
Save masasin/f0c56881a141e594085ee5da8db98a0f to your computer and use it in GitHub Desktop.
Salary calculator for Alberta - Hourly wage to yearly net
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Convert an hourly wage to weekly, monthly, and (gross and net) yearly incomes.
Assumes that work is being done in Alberta, Canada.
"""
from __future__ import division, print_function
import argparse
from decimal import Decimal
import json
AB_TAX_BRACKETS = {
# Ceiling: Marginal tax rate
125000: 10,
150000: 12,
200000: 13,
300000: 14,
float("inf"): 15,
}
AB_BASIC_PERSONAL_AMOUNT = 18451
AB_OVERTIME_BONUS = 1.5
AB_OVERTIME_THRESH = 40 # hours (8 hours a day at 5 days a week)
AB_OVERTIME_THRESH_WEEKEND = 44 # hours (if working weekends)
FEDERAL_TAX_BRACKETS = {
# Ceiling: Marginal tax rate
45282: 15,
90563: 20.5,
140388: 26,
200000: 29,
float("inf"): 33,
}
FEDERAL_BASIC_PERSONAL_AMOUNT = 11474
CPP_RANGE = (3500, 51100)
CPP_RATE = 4.95
EI_RATE = 1.88
EI_MAX = 50800
def _tax_calculator_marginal(taxable_income, brackets=AB_TAX_BRACKETS):
"""
Calculate the total tax on a given salary assuming a marginal method.
Parameters
----------
taxable_income : Decimal
The yearly taxable income.
brackets : Optional[dict]
The tax brackets. Format should be the ceiling mapped to the tax rate,
in (decimal) percent. Default is Alberta's official tax brackets.
Returns
-------
total_tax : Decimal
The total tax for the year.
Examples
--------
>>> brackets = {125000: 10, 150000: 12, 200000: 13, 300000: 14,
... float("inf"): 15}
>>> _tax_calculator_marginal(Decimal(3000), brackets)
Decimal('300.0')
>>> _tax_calculator_marginal(Decimal(125000), brackets)
Decimal('12500.0')
>>> _tax_calculator_marginal(Decimal(130000), brackets)
Decimal('13100.00')
"""
total_tax = 0
prev_ceiling = 0
ceilings = sorted(brackets.keys())
for curr_ceiling in ceilings:
rate_pct = Decimal(brackets[curr_ceiling]) / 100
if taxable_income > curr_ceiling:
total_tax += (curr_ceiling - prev_ceiling) * rate_pct
prev_ceiling = curr_ceiling
else:
total_tax += (taxable_income - prev_ceiling) * rate_pct
return total_tax
def _cpp_contribution(yearly_salary, cpp_range=CPP_RANGE, cpp_rate=CPP_RATE):
"""
Calculate the Canadian Pension Plan contribution.
Parameters
----------
yearly_salary : Decimal
The gross yearly salary.
cpp_range : Optional[tuple]
The minimum and maximum of the income which counts towards CPP. Default
are the official values.
cpp_rate : Optional[float]
The rate at which the contribution happens. Default is official value.
Returns
-------
Decimal
The total CPP contributions for the year.
"""
cpp_min, cpp_max = sorted(cpp_range)
return max(0,
min(cpp_max, yearly_salary) - cpp_min) * Decimal(cpp_rate) / 100
def _equiv_hourly_wage(base_hourly_wage, weekly_hours, overtime_bonus,
overtime_thresh):
"""
Get the equivalent hourly wage including overtime.
The equivalent wage is the wage that would yield the same pay over a given
period if there was no overtime.
Parameters
----------
base_hourly_wage : Decimal
The hourly wage before overtime is reached.
weekly_hours : float
The number of hours worked per week.
overtime_bonus : float
The multiplier for the hourly wage once overtime is reached.
overtime_thresh : float
The number of hours necessary for overtime to be reached.
Returns
-------
Decimal
The equivalent hourly wage.
Examples
--------
>>> _equiv_hourly_wage(Decimal(15), 37.5, 1.5, 40)
Decimal('15')
>>> _equiv_hourly_wage(Decimal(15), 60, 1.5, 40)
Decimal('17.5')
"""
if weekly_hours < overtime_thresh:
return base_hourly_wage
else:
normalized_hours = Decimal(weekly_hours * overtime_bonus
+ overtime_thresh * (1 - overtime_bonus))
return normalized_hours * base_hourly_wage / Decimal(weekly_hours)
def salary_report(base_hourly_wage, weekly_hours,
overtime_bonus=AB_OVERTIME_BONUS,
overtime_thresh=AB_OVERTIME_THRESH,
tax_brackets_provincial=AB_TAX_BRACKETS,
tax_brackets_federal=FEDERAL_TAX_BRACKETS,
):
"""
Print a report of the salaries and net income.
Parameters
----------
base_hourly_wage : float
The hourly wage before overtime is reached.
weekly_hours : float
The number of hours worked per week.
overtime_bonus : Optional[float]
The multiplier for the hourly wage once overtime is reached. Default is
the minimum bonus required in Alberta.
overtime_thresh : Optional[float]
The number of hours necessary for overtime to be reached. Default is the
smaller 8 hours per day assuming work happens five days a week.
tax_brackets_provincial : Optional[dict]
The provincial tax brackets. Format should be the ceiling mapped to the
tax rate, in (decimal) percent. Default is Alberta's official tax
bracket table for 2016.
tax_brackets_federal : Optional[dict]
The federal tax brackets. Format should be the ceiling mapped to the tax
rate, in (decimal) percent. Default is the Canadian government's
official federal tax bracket table for 2016.
Examples
--------
>>> brackets = {125000: 10, 150000: 12, 200000: 13, 300000: 14,
... float("inf"): 15}
>>> salary_report(15, 40, 1.5, 40, brackets)
40 hours per week @ $15.00/hr
1.5 times ($22.50) after 40 hours
<BLANKLINE>
equiv. hourly wage: $15.00
--------------------------
per week: $ 600.00
per paycheque: $ 1,200.00
per month: $ 2,600.00
per year: $ 31,200.00
--------------------------
tax: -$ 3,120.00
--------------------------
NET INCOME: $ 28,080.00
>>> salary_report(15, 50, 1.5, 40, brackets)
50 hours per week @ $15.00/hr
1.5 times ($22.50) after 40 hours
<BLANKLINE>
equiv. hourly wage: $16.50
--------------------------
per week: $ 825.00
per paycheque: $ 1,650.00
per month: $ 3,575.00
per year: $ 42,900.00
--------------------------
tax: -$ 4,290.00
--------------------------
NET INCOME: $ 38,610.00
"""
base_hourly_wage = Decimal(base_hourly_wage)
hourly_wage = _equiv_hourly_wage(base_hourly_wage, weekly_hours,
overtime_bonus, overtime_thresh)
weekly_wage = hourly_wage * Decimal(weekly_hours)
monthly_wage = weekly_wage * Decimal(4 + 1/3)
yearly_wage = monthly_wage * 12
cpp = _cpp_contribution(yearly_wage)
ei = min(yearly_wage, Decimal(EI_MAX)) * Decimal(EI_RATE) / 100
deductions_total = cpp + ei
taxes_provincial = _tax_calculator_marginal(
yearly_wage - AB_BASIC_PERSONAL_AMOUNT - deductions_total,
tax_brackets_provincial)
taxes_federal = _tax_calculator_marginal(
yearly_wage - FEDERAL_BASIC_PERSONAL_AMOUNT - deductions_total,
tax_brackets_federal)
taxes_total = taxes_provincial + taxes_federal
net_income = yearly_wage - taxes_total - deductions_total
net_hourly = net_income / Decimal(4 + 1/3) / Decimal(weekly_hours) / 12
print("{weekly_hours} hours per week @ ${base_hourly_wage:.2f}/hr"
.format(
weekly_hours=weekly_hours,
base_hourly_wage=base_hourly_wage,
))
print("{mult} times (${ot_hourly_wage:.2f}) after {overtime_thresh} hours"
.format(
mult=overtime_bonus,
ot_hourly_wage=base_hourly_wage * Decimal(overtime_bonus),
overtime_thresh=overtime_thresh,
))
print()
print("equiv. hourly wage: ${:.2f}".format(hourly_wage))
print("-"*26)
print("per week: ${: >10,.2f}".format(weekly_wage))
print("per paycheque: ${: >10,.2f}".format(weekly_wage * 2))
print("per month: ${: >10,.2f}".format(monthly_wage))
print("per year: ${: >10,.2f}".format(yearly_wage))
print("-"*26)
print(u"(deductions ±5%)")
print("fed. tax: -${: >10,.2f}".format(taxes_federal))
print("prov. tax: -${: >10,.2f}".format(taxes_provincial))
print("{: ^26}".format("-"*6))
print("subsubtotal: -${: >10,.2f}".format(taxes_total))
print("{: ^26}".format("-"*10))
print("cpp: -${: >10,.2f}".format(cpp))
print("ei: -${: >10,.2f}".format(ei))
print("{: ^26}".format("-"*6))
print("subsubtotal: -${: >10,.2f}".format(deductions_total))
print("{: ^26}".format("-"*10))
print("subtotal: -${: >10,.2f}".format(taxes_total + deductions_total))
print("-"*26)
print("net hourly: ${: >10,.2f}".format(net_hourly))
print("NET INCOME: ${: >10,.2f}".format(net_income))
def test():
"""
Run doctests.
The tests have run correctly if there is no output.
"""
import doctest
doctest.testmod()
def main():
parser = argparse.ArgumentParser(
prog="salary_report",
description="""
Convert an hourly wage to weekly, monthly, and (gross and net) yearly incomes in
Alberta, using the 2016 tax brackets by default.
""",
epilog = "Doctests are available for this script."
)
# group = parser.add_mutually_exclusive_group()
# group.add_argument("-t", "--test", action="store_true",
# help="run doctests; silent if no errors")
gen_group = parser.add_argument_group(
"report generation",
"""
Generate the report. The base hourly wage and the hours worked per week are
required.
"""
)
gen_group.add_argument(
"-b",
"--base",
action="store",
dest="base_hourly_wage",
type=float,
help="hourly wage before overtime, in dollars; required",
required=True,
)
gen_group.add_argument(
"-w", "--hours",
action="store",
dest="weekly_hours",
type=float,
help="number of hours worked per week; required",
required=True,
)
gen_group.add_argument(
"-m",
"--multiplier",
action="store",
dest="overtime_bonus",
type=float,
help="salary multiplier once overtime is reached; default is {}"
.format(AB_OVERTIME_BONUS),
default=AB_OVERTIME_BONUS,
)
gen_group.add_argument(
"-t",
"--threshold",
action="store",
dest="overtime_thresh",
type=float,
help="""
number of hours until overtime is reached; default is {}. If working more than
five days a week, but not more than eight hours a day, the Alberta laws
stipulate 44 hours as the threshold.
"""
.format(AB_OVERTIME_THRESH),
default=AB_OVERTIME_THRESH,
)
gen_group.add_argument(
"--bracketsprov",
action="store",
type=str,
help="""
the provincial tax brackets; should be a json string of format
'{{"ceiling or inf": marginal tax rate}}'.
Default is the 2016 Alberta tax bracket table, with {}
"""
.format(AB_TAX_BRACKETS),
)
gen_group.add_argument(
"--bracketsfed",
action="store",
type=str,
help="""
the federal tax brackets; should be a json string of format
'{{"ceiling or inf": marginal tax rate}}'.
Default is the 2016 Federal tax bracket table, with {}
"""
.format(FEDERAL_TAX_BRACKETS),
)
args = parser.parse_args()
if args.bracketsprov:
tax_brackets_provincial = {float(k): v for k, v
in json.loads(args.bracketsprov).items()}
else:
tax_brackets_provincial = AB_TAX_BRACKETS
if args.bracketsfed:
tax_brackets_federal = {float(k): v for k, v
in json.loads(args.bracketsfed).items()}
else:
tax_brackets_federal = FEDERAL_TAX_BRACKETS
salary_report(
base_hourly_wage=args.base_hourly_wage,
weekly_hours=args.weekly_hours,
overtime_bonus=args.overtime_bonus,
overtime_thresh=args.overtime_thresh,
tax_brackets_provincial=tax_brackets_provincial,
tax_brackets_federal=tax_brackets_federal,
)
if __name__ == "__main__":
main()
@masasin
Copy link
Author

masasin commented Aug 9, 2016

An online version of this code can be found here: https://repl.it/ClHe/12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment