Last active
August 9, 2016 13:45
-
-
Save masasin/f0c56881a141e594085ee5da8db98a0f to your computer and use it in GitHub Desktop.
Salary calculator for Alberta - Hourly wage to yearly net
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
An online version of this code can be found here: https://repl.it/ClHe/12