#!/usr/bin/python # -*- coding: iso-8859-1 -*- # cdnpayroll.py 2011 Payroll calculations for Canada # copyright 2001-2011 Paul Evans # # Recent Changes # Thu Jun 23, 2011 # Usable as a module by Ben McGunigle # Fri Jan 7, 2011 # Changes due to t4127-11e effective Jan 01 2011 by Russell Levy # Sat Jan 01, 2010 # Changes due to t4127-10e effective Jan 01 2010 # Wed Jul 01, 2009 # Changes due to t4127-jul-09e effective July 01 2009 # Tue Mar 31, 2009 # Changes due to t4127-4e effective Apr 01 2009 by Russell Levy # Wed Dec 31, 2008 # Changes due to t4127-09e # Wed Jan 2, 2008 # Changes going back to 2001 are in the changelog # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, # or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # You should have a copy of the GNU General Public License. # Probably you have *many* of them... # If not, write to the Free Software Foundation, # Inc., 675 Mass Ave, Cambridge, MA 02139, USA or visit # http://www.gnu.org/philosophy/free-sw.html import sys, getopt, time, os, os.path import string, getpass, re import cgi # This is the line you need to change to point to your own server # Only *if* you want to use the webform. home = 'http://localhost/payform.html' user = getpass.getuser() homedir = os.path.expanduser('~/') os.putenv('HOME',homedir) # example optional (lazy) defaults # commandline will over-ride these ##RESIDES = ('BC') ##P = 26 ##I = 1500 ##WCBrate = 0.1048 ##hp = 0.04 # You may play with these two notions depending on need CWD = os.getcwd() # # try: # os.chdir(sys.path[0]) #or CWD # except OSError: # pass version = 'version-Jan_2011.00' # Option 1 def usage(): print ''' Usage: [python] cdnpayroll.py -f [-p] [-P]-r -i [other args] [python] cdnpayroll.py -h (or --help) is this help message. Many arguments have both a long and a short form. The above shown are a minimal subset, but quite often sufficient. All the amount variables found in T4127(E) are listed below. The order doesn't matter. Where: -f, --td1f is the employee's Federal Claim Code (e.g. -f 2 or --td1f 2) -p, --td1p is the employee's Federal Claim Code (e.g. -p 2 or --td1p 2) -r, --resides is the province of residence for tax purposes. default: BC One of: 'AB':'Alberta', 'BC':'British Columbia', 'MB':'Manitoba', 'NB':'New Brunswick', 'NF':'Newfoundland', 'NT':'Northwest Territories', 'NS':'Nova Scotia', 'NU':'Nunavut', 'ON':'Ontario', 'PE':'Prince Edward Island', 'QC':'Quebec', 'SK':'Saskatchewan', 'YT':'Yukon', 'OC' or 'OT':'Outside Canada' (not case sensative e.g. 'bc' is fine for 'BC') -i, --income is the gross pay amount (except for commission employees) --advance The amount an employee may have been advanced earlier in the pay period -P, --periods is the number of payperiods in the year (defaults to bi-weekly: 26) --hp Holiday pay rate as a percentage -u, --union Union dues for the pay period -w, --wcb The WCB rate for the employee as a decimal rate e.g. 0.0974 -m, --months is the number of months of the year employee is between 18 and 70 years old. This is to handle the CPP requirement. Default is 12 (i.e. employee is of age all year) -l, --additional is the amount 'L' of additional tax to be deducted by the employer per TD1 request -y, --yamount as the allowable amount *NOT* number of deps. Optional Y factor for Ont. and Manitoba. --first First Name. Just to be passed through the script --last Last Name --sin --lcf is the Labour-sponsored funds federal tax credit for the year.Enter the amount deducted or withheld. --lcp is the Labour-sponsored funds provincial tax credit for the year.Enter the amount deducted or withheld. --hd is the annual deduction for living in a prescribed zone from TD1 --rsp Deductions at source for employee contributions to a (RPP), a (RRSP), or a (RCA) --child Annual deductions such as child care expenses and alimony payments --PR If F1 (--child) is implemented after the first payperiod of the year the formula (P × F1) / PR is used The --PR amount will also work to adjust the K3 amount given in --medical --alimony Alimony or maintenance payments required by a legal document to be deducted at source --medical Other federal tax credits, such as medical expenses and charitable donations --cppytd cpp contributions ytd --eiytd ei contributions ytd --eix ei exempt -C, --commiss commision calculations will be used. Call with a '1' to turn this opt on (e.g. --commiss 1) -G, --grosscom is Gross commission incl. any salary. -I, --icomm equals total remuneration for the year as reported by the employee on Form TD1X --Ac is the number of days since last commission payment --exp total expenses deduction as reported by the employee on Form TD1X --con Output some print to console --dict Output a dictionary containing all the vars to console --csv Output a csv list of vars to console --dump Dump all vars to console space delimited --spread Outputs to 'payroll.csv'. Includes labels,totals and will do inserts for new entries. --ppstart pass-through arg used with --spread --ppend pass-through arg used with --spread --rhrs Reg hours and rates. Will calculate 'I' if I==zero --rrate --ohrs OT hours and rates. Will calculate 'I' if I==zero --orate A brief example: An employee in AB who's a code 1 from both federal and provincial TD1 forms making 1000 in two weeks (26 pays a year): cdnpayroll.py -r AB -f 1 -p 1 -i 1000 -P 26 or, with long args: cdnpayroll.py --resides AB --td1f 1 --td1p 1 --income 1000 --periods 26 You can 'hard-code' some args in quite easily beginning at about line 53 of the script just by typing in some values. So, if you only need to do one province or territory set 'RESIDES' and you'll never have to enter it. Ditto for claim codes, wcb etc. In theory then, you could just call it with the -i argument for income: cndpayroll -i 1543.28 Please bear in mind that this script is intended to be run from either a GUI or another program. Still, ideas for making it a little friendlier from the command line are welcome. ''' ############################# # misc formatting functions # ############################# digits = string.digits lower = string.ascii_lowercase upper = string.ascii_uppercase letters = string.letters def datefmt(instr): instr = filter(instr, digits) #calendar.monthrange(year, month) -> (1stWkday, daysinmonth) tuple #time.strptime(string[, format]) -> timetuple or ValueError :-) l = len(instr) if l == 8: fmt = '^^^^-^^-^^' outstr = self.formatStr(instr, fmt) elif l == 1 or l == 2: if int(instr) > 31: pass outstr = time.strftime('%Y-%m-', time.localtime(time.time() ) ) + string.zfill(instr, 2) elif l == 3: instr = string.zfill(instr[:1], 2) + instr[1:] fmt = '^^-^^' outstr = time.strftime('%Y-', time.localtime(time.time() ) ) + formatStr(instr, fmt) elif l == 4: fmt = '^^-^^' outstr = time.strftime('%Y-', time.localtime(time.time() ) ) + formatStr(instr, fmt) elif l == 0: outstr = ds() else: outstr = instr try: time.strptime(outstr, '%Y-%m-%d') except ValueError: outstr = 'Invalid Date' return outstr def moneyfmt(instr): ''' inserts commas every 3: Michael P. Soulier ''' instr = (str(instr) ) instr = re.sub("^(-?\d+)(\d{3})", '\g<1>,\g<2>', instr) return instr def phonefmt(instr): instr = filter(instr, digits + '#*,') formatStr(instr, ' ^^^ ^^ (^^^) ^^^-^^^^') return outstr def postalfmt(instr): instr = filter(instr.upper(), digits + letters) l = len(instr) if l == 6: fmt = '^^^ ^^^' outstr = formatStr(instr, fmt) else: outstr = instr # maybe isn't Cdn postal code return outstr def ds(): stamp = time.strftime('%Y-%m-%d', time.localtime(time.time() ) ) return(stamp) def filter(instr, allowed): outstr = '' for c in instr: if c in allowed: outstr += c return outstr def formatStr(inStr, formatStr, p = '^'): inList = [x for x in inStr] #list from strings.. fmtList = [x for x in formatStr] inList.reverse(); fmtList.reverse() outList = [] i = 0 for c in fmtList: if c == p: try: outList.append(inList[i]) i += 1 # break if formatStr longer than inStr except IndexError: break else: outList.append(c) # handle inStr longer than formatStr while i < len(inList): outList.append(inList[i]) i += 1 outList.reverse() outStr = ''.join(outList) # remove stray parens/- etc while re.match('[)|-| ]', outStr[0]): outStr = outStr[1:] # match any legit parens while outStr.count(')') > outStr.count('('): outStr = '(' + outStr return outStr def uniq(alist): '''Fastest without order preserving -- copied from somewhere???''' set = {} map(set.__setitem__, alist, []) return set.keys() def floatstr(i): '''always return a float''' try: f = float(i) except ValueError: f = 0.0 return f def makesums(last_datarow): '''make the spreadsheet row for totals formatted to '=SUM(C2:C60)' style''' prefix = '=SUM(' suffix = '),' sums = '\012TOTALS, ' + ','*9 for n in range(11, 27): sums = sums + prefix + chr(n+64) +'2:' + chr(n+64) + last_datarow + suffix sums = sums +'\012' return sums # some recurring decision functions def neg2zero(amount): if amount < 0: return 0 else: return amount def max_val(amount, limit): if amount > limit: return limit else: return amount def min_val(amount, limit): if amount < limit: return limit else: return amount def lesser_val(amount1, amount2): if amount1 < amount2: return amount1 else: return amount2 #################### # output functions # #################### # a wretched old print2 def print2con(first, last, sin, feds, prov, ttldeducts, netpay, T, L, U1, F, F2, C, Cemp, EI, EIemp, WCB, I, G, Provinces, RESIDES, advance, hp): print "\nPay period Summary 2011" #test calcs against tables #this should be written out w/ proper formatting print if first != 'n/a' and last != 'n/a': print first + " " + last if sin != 'n/a': print sin if hp != 0: hptxt = " (incl. " + str(hp*100) +"% Holiday pay) :" else: hptxt = "" print Provinces[RESIDES] + " resident tax calculated." print "\nGross Pay = " + hptxt + str(I) print "\n\tFederal \t" + str(feds) print "\tProvincial\t" + str(prov) print "\t\t\t----------" print "\t Total Tax\t" + str(T) print "\n\t\tCPP\t\t" + str(C) print "\t\tEI \t\t" + str(EI) print "\t\tUnion \t" + str(U1) print " Requested extra tax\t" + str(L) print "\t Savings Plan\t" + str(F) print "\t Advance(s)\t" + str(advance) print " Required Other\t" + str(F2) print "\t\t\t==========" print "Total Deductions:\t" + str(ttldeducts) print "\nNet Pay Amount:\t\t" + str(netpay) print "\n\nEmployer Costs\n" print "Employer CPP\t" + str(Cemp) print "Employer EI \t" + str(EIemp) print "WCB\t\t" + str(WCB) print "\t\t----------" print "Total Costs:\t" + str(Cemp + EIemp + WCB) #if (I + G) != 0: # print "\nLoading: " + str( round( ( (Cemp + EIemp + WCB + I + G) / (I + G) ) + (hp) -1, 4 ) * 100) +"%" print "Revenue Canada Total: " +str(T + C + Cemp + EI + EIemp) print print "Cdnpayroll: " + version #################### # SIN Check Digit # #################### def checksin(S): '''returns a string object a) error msg if number of digits not == 8 or 9 b) formatted SIN if valid SIN c) check digit if only 8 digits supplied''' S = filter(S, digits) L = len(S) if L < 8: return "Too few digits for SIN: only %d." % len(S) if L > 9: return "Too many digits for SIN: %d is more than 9." % len(S) j = T = 0 for i in [1,2,3,4]: T += int(S[j]) * 1 j += 1 m = int(S[j]) * 2 k = str(m) if len(k) == 2: m = int(k[0]) + int(k[1]) T += int(m) j += 1 chkdigit = ( (int(T)/10)*10 + 10 ) - T if chkdigit == 10: chkdigit = 0 if L == 9: t = str(T) inlist = [x for x in S] #list from strings.. if int(inlist[8]) == chkdigit : return S else: return "Invalid SIN" else: S = formatStr(S, '^^^-^^^-^^') return "%s[%d]" % (S,chkdigit) ############################# # provincial tax functions # ############################# #2011 Alberta def ptax_ab(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDAB = [0.00, 16977.00, 18219.00, 20703.00, 23187.00, 25671.00, 28155.00, 30639.00, 33123.00, 35607.00, 38091.00] TCP = TDAB[int(TD1P)] V = 0.10 KP = 0 K1P = 0.10 * TCP K2P = (0.10 * max_val( (P * C), 2217.60) ) + (0.10 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P V1 = KP = LCP = S = Y = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 British Columbia def ptax_bc(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDBC = [0.00, 11088.00, 12155.00, 14290.50, 16425.50, 18560.50, 20695.50, 22830.50, 24965.50, 27100.50, 29235.50] TCP = TDBC[int(TD1P)] if A <= 36146: V, KP = 0.0506, 0 elif A > 36146 and A <= 72293: V, KP = 0.0770, 954 elif A > 72293 and A <= 83001: V, KP = 0.1050, 2978 elif A > 83001 and A <= 100787: V, KP = 0.1229, 4464 else: V, KP = 0.1470, 4893 K1P = 0.0506 * TCP K2P = (0.0506 * max_val( (P * C), 2217.60) ) + (0.0506 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P A1 = A + HD if A1 <= 17493: S = lesser_val(T4, 394) elif A1 > 17493 and A1 <= 29805.50: S = lesser_val(T4, 394 - ( (A1 - 17493) * 0.032) ) else: S = 0 LCP = lesser_val(2000, (0.15 * LCPin) ) V1 = Y = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Manitoba def ptax_mb(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDMB = [0.00, 8134.00, 8980.50, 10673.50, 12366.50, 14059.50, 15752.50, 17445.50, 19138.50, 20831.50, 22524.50] TCP = TDMB[int(TD1P)] if A <= 31000: V, KP = 0.1080, 0 elif A > 31000 and A <= 67000: V, KP = 0.1275, 605 else: V, KP = 0.1740, 3720 K1P = 0.108 * TCP K2P = (0.108 * max_val( (P * C), 2217.60) ) + (0.108 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P V1 = 0 A1 = A + HD S = 0 LCP = lesser_val(1800, LCPin * 0.15) return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 New Brunswick def ptax_nb(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDNB = [0.00, 8953.00, 9946.00, 11932.00, 13918.00, 15904.00, 17890.00, 19876.00, 21862.00, 23848.00, 25834.00] TCP = TDNB[int(TD1P)] if A <= 37150: V, KP = 0.091, 0 elif A > 37150 and A < 74300: V, KP = 0.121, 1115 elif A > 74300 and A < 120796: V, KP = 0.124, 1337 else: V, KP = 0.127, 1700 K1P = 0.091 * TCP K2P = (0.091 * max_val( (P * C), 2217.60) ) + (0.091 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P LCP = lesser_val(LCPin * 0.20, 2000) S = Y = V1 = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Newfoundland and Labrador def ptax_nf(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDNF = [0.00, 7989.00, 8851.50, 10576.50, 12301.50, 14026.50, 15751.50, 17476.50, 19201.50, 20926.50, 22651.50] TCP = TDNF[int(TD1P)] if A <= 31904: V, KP = 0.077, 0 elif A > 31904 and A <= 63807: V, KP = 0.125, 1531 else: V, KP = 0.133, 2042 K1P = 0.077 * TCP K2P = (0.077 * max_val( (P * C), 2217.60) ) + (0.077 * max_val( (P * EI), 786.76 ) ) T4 = (V * A) - KP - K1P - K2P - K3P V1 = 0 LCP = lesser_val(LCPin * 0.20, 2000) S = Y = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Northwest Territories def ptax_nt(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDNT = [0.00, 12919.00, 14029.50, 16250.50, 18471.50, 20692.50, 22913.50, 25134.50, 27355.50, 29576.50, 31797.50] TCP = TDNT[int(TD1P)] if A <= 37626: V, KP = 0.0590, 0 elif A > 37626 and A <= 75253: V, KP = 0.0860, 1016 elif A > 75253 and A <= 122345: V, KP = 0.1220, 3725 else: V, KP = 0.1405, 5988 K1P = 0.059 * TCP K2P = (0.059 * max_val( (P * C), 2217.60) ) + (0.059 * max_val( (P * EI), 786.76) ) K3P = 0 T4 = (V * A) - KP - K1P - K2P - K3P if LCPin <= 5000: LCPnwt = 0.15 * LCPin else: LCPnwt = (0.15 * 5000) + (0.30 * (LCPin - 5000) ) LCP = lesser_val(LCPin * 0.15, LCPnwt) V1 = S = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Nova Scotia def ptax_ns(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDNS = [0.00, 8231.00, 9031.00, 10631.00, 12231.00, 13831.00, 15431.00, 17031.00, 18631.00, 20231.00, 21831.00] TCP = TDNS[int(TD1P)] if A <= 29590: V, KP = 0.0879, 0 elif A > 29590 and A <= 59180: V, KP = 0.1495, 1823 elif A > 59180 and A <= 93000: V, KP = 0.1667, 2841 elif A > 9300 and A < 150000: V, KP = 0.1750, 3613 else: V, KP = 0.2100, 8863 K1P = 0.0879 * TCP K2P = (0.0879 * max_val( (P * C), 2217.60) ) + (0.0879 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P LCP = lesser_val(LCPin * 0.20, 2000) V1 = S = Y = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Nunavut def ptax_nu(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDNU = [0.00, 11878.00, 13006.50, 15263.50, 17520.50, 19777.50, 22034.50, 24291.50, 26548.50, 28805.50, 31062.50] TCP = TDNU[int(TD1P)] if A <= 39612: V, KP = 0.040, 0 elif A > 39612 and A <= 79224: V, KP = 0.070, 1188 elif A > 79224 and A <= 128800: V, KP = 0.090, 2773 else: V, KP = 0.115, 5993 K1P = 0.040 * TCP K2P = (0.040 * max_val( (P * C), 2217.60) ) + (0.040 * max_val( (P * EI), 786.76) ) K3P = 0 T4 = (V * A) - KP - K1P - K2P - K3P S = V1 = LCP = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Ontario def ptax_on(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDON = [0.00, 9104.00, 10084.50, 12045.50, 14006.50, 15967.50, 17928.50, 19889.50, 21850.50, 23811.50, 25772.50] TCP = TDON[int(TD1P)] if A <= 37774: V, KP = 0.0505, 0 elif A > 37774 and A <= 75550 : V, KP = 0.0915, 1549 else: V, KP = 0.1116, 3067 K1P = 0.0505 * TCP K2P = (0.0505 * max_val( (P * C), 2217.60) ) + (0.0505 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P if T4 <= 4078: V1 = 0 elif T4 > 4078 and T4 <= 5219: V1 = 0.20 * (T4 - 4078) else: V1 = (0.20 * (T4 - 4078) ) + (0.36 * (T4 - 5219) ) if A <= 20000: V2 = 0 elif A > 20000 and A <= 36000: V2 = lesser_val(300, (0.06 * (A - 20000) ) ) elif A > 36000 and A <= 48000: V2 = lesser_val(450, (300 + (0.06) * (A - 36000) ) ) elif A > 48000 and A <= 72000: V2 = lesser_val(600, (450 + (0.25) * (A - 48000) ) ) elif A > 72000 and A <= 200000: V2 = lesser_val(750, (600 + (0.25) * (A - 72000) ) ) else: V2 = lesser_val(900, (750 + (0.25) * (A - 200000) ) ) S = neg2zero(lesser_val( (2 * (210 + Y) - (T4 + V1) ), T4 + V1 ) ) LCP = lesser_val(375, LCPin * 0.05) return(V, V1+V2, KP, K1P, K2P, T4, S, LCP, Y) #For outside Canada and in Canada beyond the limits of any province only: def ptax_ot(A, TD1P, P, C, EI, K3P, LCPin, Y, HD, T3): V = V1 = V2 = S = LCP = T4 = 0 return(V, V1, V2, S, LCP, T4) #2011 Prince Edward Island def ptax_pe(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDPE = [0.00, 7708.00, 8508.00, 10108.00, 11708.00, 13308.00, 14908.00, 16508.00, 18108.00, 19708.00, 21308.00] TCP = TDPE[int(TD1P)] if A <= 31984: V, KP = 0.098, 0 elif A > 31984 and A <= 63969: V, KP = 0.138, 1279 else: V, KP = 0.167, 3134 K1P = 0.098 * TCP K2P = (0.098 * max_val( (P * C), 2217.60) ) + (0.098 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P if T4 <= 12500: V1 = 0 elif T4 > 12500: V1 = 0.10 * (T4-12500) S = LCP = Y = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Saskatchewan def ptax_sk(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDSK = [0.00, 13535.00, 14470.50, 16341.50, 18212.50, 20083.50, 21954.50, 23825.50, 25696.50, 27567.50, 29438.50] TCP = TDSK[int(TD1P)] if A <= 40919: V, KP = 0.11, 0 elif A > 40919 and A <= 116911: V, KP = 0.13, 818 else: V, KP = 0.15, 3157 K1P = 0.11 * TCP K2P = (0.11 * max_val( (P * C), 2217.605) ) + (0.11 * max_val( (P * EI), 786.76) ) T4 = (V * A) - KP - K1P - K2P - K3P V1 = S = Y = 0 # SK gets special credit for this unique tangle #LCPsk = lesser_val(1000, LCPin * 0.20) #LCPca = lesser_val(525, LCFin * 0.15) #LCP = lesser_val(1000, LCPsk + LCPca) # 2009 July this has now changed to the more sensible: LCP = lesser_val(LCPin * 0.20, 1000) # Sadly, several other places have since copied their former practice... return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) #2011 Yukon def ptax_yt(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD): TDYT = [0.00, 10527.00, 11532.50, 13543.50, 15554.50, 17565.50, 19576.50, 21587.50, 23598.50, 25609.50, 27620.50] TCP = TDYT[int(TD1P)] if A <= 41544: V, KP = 0.0704, 0 elif A > 41544 and A <= 83088: V, KP = 0.0968, 1097 elif A > 83088 and A <= 1288800: V, KP = 0.1144, 2559 else: V, KP = 0.1276, 4259 K1P = 0.0704 * TCP K2P = (0.0704 * max_val( (P * C), 2217.60) ) + (0.0704 * max_val( (P * EI), 786.76) ) #K3P, some kind of secret... K3P = 0 # 2007 introduced K4P for the Yukon only K4P = lesser_val( (0.0704 * A),(0.0704 * 1065) ) T4 = (V * A) - KP - K1P - K2P - K3P - K4P if T4 <= 6000: V1 = 0 elif T4 > 6000: V1 = 0.05 * (T4 - 6000) LCP = lesser_val(1250, LCPin * 0.25) S = 0 return(V, V1, KP, K1P, K2P, T4, S, LCP, Y) if __name__ == "__main__": referer = os.environ.get("HTTP_REFERER") ############################### # parse cmd line or POST args # ############################### args = {} args['out'] = 'con' if referer == None: # is this test good enough to see if we're cgi? longopts = ['help', 'td1f=', 'td1p=', 'resides=', 'income=', 'periods=' , 'months=', 'additional=', 'lcf=', 'lcp=', 'hd=', 'rsp=', 'child=', 'alimony=', 'union=', 'medical=', 'cppytd=', 'eiytd=', 'yamount=', 'PR=', 'commiss=', 'grosscom=', 'icomm=', 'exp=', 'Ac=', 'wcb=', 'first=', 'last=', 'sin=', 'advance=', 'con', 'dict', 'csv', 'dump', 'spread=', 'hp=', 'eix', 'ppstart=', 'ppend=', 'rhrs=', 'rrate=', 'ohrs=', 'orate='] try: opts, argmts = getopt.getopt(sys.argv[1:], 'hf:p:r:i:P:l:u:m:y:C:G:I:E:w:', longopts) input = opts except getopt.error: print 'some kind of option error...' usage() sys.exit(2) for z, val in opts: if z in ('-h', '--help'): usage() sys.exit() if z == '--advance':args['advance'] = float(val) if z == '--alimony':args['F2'] = float(val) if z == '--child':args['F1'] = float(val) if z == '--con':args['out'] = 'con' if z == '--cppytd':args['Dc'] = float(val) if z == '--csv':args['out'] = 'csv' if z == '--dict':args['out'] = 'dict' if z == '--dump':args['out'] = 'dump' if z == '--eix':args['eix'] = 1 if z == '--eiytd':args['De'] = float(val) if z == '--first':args['first'] = val.capitalize() if z == '--hd':args['HD'] = float(val) if z == '--hp':args['hp'] = float(val) if z == '--Ac':args['Ac'] = int(val) if z in ('-C', '--commiss'):args['COMMISS'] = int(val) if z in ('-E', '--exp'):args['E'] = float(val) if z in ('-f', '--td1f'):args['TD1F'] = float(val) if z in ('-G', '--grosscom'):args['G'] = float(val) if z in ('-I', '--icomm'):args['I1'] = float(val) if z in ('-i', '--income'):args['I'] = float(val) if z in ('-l', '--additional'):args['L'] = float(val) if z in ('-m', '--months'):args['REQUIRED_MONTHS'] = int(val) if z in ('-P', '--periods'):args['P'] = float(val) if z in ('-p', '--td1p'):args['TD1P'] = float(val) if z in ('-r', '--resides'): args['RESIDES'] = string.upper(val) if z in ('-u', '--union'):args['U1'] = float(val) if z in ('-w', '--wcb'):args['WCBrate'] = float(val) if z in ('-y', '--yamount'):args['Y'] = float(val) if z == '--last':args['last'] = val.capitalize() if z == '--lcf':args['LCFin'] = float(val) if z == '--lcp':args['LCPin'] = float(val) if z == '--medical':args['K3'] = float(val) if z == '--ohrs':args['ohrs'] = val if z == '--orate':args['orate'] = val if z == '--ppend':args['ppend'] = val if z == '--ppstart':args['ppstart'] = val if z == '--PR':args['PR'] = int(val) if z == '--rhrs':args['rhrs'] = val if z == '--rrate':args['rrate'] = val if z == '--rsp':args['F'] = float(val) if z == '--sin': args['sin'] = (val) # validated and formatted later in this script if z == '--spread': args['out'] = 'spread' args['fname'] = val else: # we're being called via web form ###def template(file, vars): ###return open(templatedir.template, 'r').read() % varsdict # template has

%(title)s

import cgitb; cgitb.enable() #(display=0, logdir="/tmp") form = cgi.FieldStorage(keep_blank_values=1) # FieldStorage object #ppstart, ppend unckecked and not formatted thus far args['last'] = form['last'].value = form['last'].value.capitalize() args['first'] = form['first'].value = form['first'].value.capitalize() args['RESIDES'] = form['RESIDES'].value = form['RESIDES'].value.upper() args['sin'] = form['sin'].value if args['sin'] != 'n/a' and args['sin'] != None: S = sin outstr = checksin(S) if outstr.isdigit(): form['sin'].value = formatStr(outstr, '^^^-^^^-^^^') else: # some kind of error or validation happened form['sin'].value = outstr for var in numvars: if form.has_key(var): try: form[var].value = floatstr(form[var].value) except ValueError: if referer == None: print "%s must be a number." % var sys.exit(2) else: print 'Content-type: text/html\n\n' print '' print 'cdnpayroll output' print '' print '

Payroll Output

\n
    ' print "Error: %s must be a number." % var print '' sys.exit(2) try: form['P'].value = int(form['P'].value) except ValueError: if referer == None: print "%s must be an integer." % "PR" sys.exit(2) else: print 'Content-type: text/html\n\n' print '' print 'cdnpayroll output' print '' print '

    Payroll Output

    \n
      ' print "Error: %s must be an integer." % "PR" print '' sys.exit(2) try: form['PR'].value = int(form['PR'].value) except ValueError: if referer == None: print "%s must be an integer." % "PR" sys.exit(2) else: print 'Content-type: text/html\n\n' print '' print 'cdnpayroll output' print '' print '

      Payroll Output

      \n
        ' print "Error: %s must be an integer." % "PR" print '' sys.exit(2) args['last'] = form['last'].value args['first'] = form['first'].value args['RESIDES'] = form['RESIDES'].value args['sin'] = form['sin'].value args['ppstart'] = form['ppstart'].value args['ppend'] = form['ppend'].value args['rhrs'] = form['rhrs'].value args['rrate'] = form['rrate'].value args['ohrs'] = form['ohrs'].value args['orate'] = form['orate'].value args['I'] = form['I'].value args['advance'] = form['advance'].value args['TD1P'] = form['TD1P'].value args['TD1F'] = form['TD1F'].value args['P'] = form['P'].value args['hp'] = form['hp'].value if form.has_key('eix'): args['eix'] = form['eix'].value args['F1'] = form['F1'].value args['F2'] = form['F2'].value args['HD'] = form['HD'].value args['LCFin'] = form['LCFin'].value args['L'] = form['L'].value args['LCPin'] = form['LCPin'].value args['F'] = form['F'].value args['K3'] = form['K3'].value args['U1'] = form['U1'].value args['REQUIRED_MONTHS'] = form['REQUIRED_MONTHS'].value args['WCBrate'] = form['WCBrate'].value args['PR'] = form['PR'].value args['Y'] = form['Y'].value args['Dc'] = form['Dc'].value args['De'] = form['De'].value if form.has_key('C'): args['C'] = form['C'].value args['G'] = form['G'].value args['I1'] = form['I1'].value args['E'] = form['E'].value args['Ac'] = form['Ac'].value args['out'] = 'html' #maybe send other output types in future via text/html ##print 'Content-type: text/html\n\n' def cdnpayroll(args): # a dictionary containing the above functions for later # e.g. ptax = provincial_functions[RESIDES], ptax(args...) provincial_functions = {'AB': ptax_ab, 'BC': ptax_bc, 'MB': ptax_mb, 'NB': ptax_nb, 'NF': ptax_nf, 'NS': ptax_ns, 'NT': ptax_nt, 'NU': ptax_nu, 'ON': ptax_on, 'OT': ptax_ot, 'PE': ptax_pe, 'SK': ptax_sk, 'YT': ptax_yt} # an alternate version for my amusement: # func_name = 'prov_' + str(RESIDES) # ptax = getattr(func_name) # e.g. ptax(args...) # method used prior to v2006 for importing the func from a module # def importprov(name): # mod = __import__(name) # g = globals() # for var in dir(mod): # if var[0] != '_': # g[var] = getattr(mod, var) ###################### # Preset some vars # ###################### # required defaults F = F1 = F2 = HD = U1 = K2 = K3 = LCFin = LCP = L = V = V1 = S = LCPin = Y = Dc = De = 0 KP = K2P = K3P = T3 = PR = Acpp = E = G = I = I1 = Ac = hp = K4P = 0 ppstart = ppend = rhrs = rrate = ohrs = orate = '' first = last = sin = "n/a" COMMISS = eix = EI = EIemp = WCBrate = advance = 0 out = 'dict' REQUIRED_MONTHS = 12 # to handle age requirement of >=18 yrs and <70 yrs on CPP P = 26 TD1F = '1' TD1P = '1' RESIDES = 'OT' fname = 'payroll.csv' Provinces = {'AB':'Alberta', 'BC':'British Columbia', 'MB':'Manitoba', 'NB':'New Brunswick', 'NF':'Newfoundland', 'NT':'Northwest Territories', 'NS':'Nova Scotia', 'NU':'Nunavut', 'ON':'Ontario', 'PE':'Prince Edward Island', 'QC':'Quebec', 'SK':'Saskatchewan', 'YT':'Yukon', 'OT':'Outside'} # This used to hold NWT etc. too until v.092 2002... Outside = {'QC':'Quebec', 'OT':'Outside'} # bool C, eix #int P, PR txtvars = [last, first, RESIDES, sin, ppstart, ppend] numvars = ['rhrs', 'rrate', 'ohrs', 'orate', 'I', 'advance', 'TD1P', 'TD1F', 'hp', 'F1', 'F2', 'HD', 'LCFin', 'L', 'LCPin', 'F', 'K3', 'U1', 'REQUIRED_MONTHS', 'WCBrate', 'Y', 'Dc', 'De', 'G', 'I1', 'E', 'Ac'] if RESIDES == 'PQ': print 'Sorry, PQ was never supported by this script.' sys.exit(1) if RESIDES == 'QC': print 'Sorry, PQ was never supported by this script.' sys.exit(1) if RESIDES == 'OC': # silently support OC as synonym for OT RESIDES = 'OT' for key in args.keys(): exec(key + " = args['" + key + "']") I = float(I) ########################################### # Finally, do some actual calculations... # ########################################### miscincome = I try: I = (float(rhrs) * float(rrate) ) + (float(ohrs) * float(orate) ) + I except: pass if F1 > 0 and PR > 0: F1 = (P * F1) / PR if K3 > 0 and PR > 0: K3 = (P * K3) / PR if COMMISS and (G == 0 or I1 == 0 or Ac == 0): print "G, Ac and I1 must have values from TD1X form" sys.exit(2) if COMMISS: grossb4hp = round( G, 2) holpay = round( G * hp, 2) G = grossb4hp + holpay else: grossb4hp = round( I, 2) holpay = round( I * hp, 2) I = grossb4hp + holpay if I > 0 and G > 0: print "There can only be one!" print "*either* I or G" sys.exit(2) # [TC, K1] pairs matching the federal claim codes for the year TDCA = [ [0.00, 0.00], [10527.00, 1579.05], [11532.50, 1729.88], [13543.50, 2031.53], [15554.50, 2333.18], [17565.50, 2634.83], [19576.50, 2936.48], [21587.50, 3238.13], [23598.50, 3539.78], [25609.50, 3841.43], [27620.50, 4143.08] ] TC, K1 = TDCA[int(TD1F)] # Annual Income #nb: if neg L gets added later if COMMISS: A = neg2zero(I1 - F - F2 - HD - F1 - E) else: A = neg2zero( (P * (I - F - F2 - U1 ) ) - HD - F1) A = round(A, 2) #--- WCB # WCB = WCBrate * I #--- CPP # # FIXME: implement max pensionable for use when attached to database # At 2010 January for example, max pensionable earnings were $47200.00 Cmax = 2217.60 * (REQUIRED_MONTHS / 12) # to handle age requirement of >18 yrs and <70 yrs if COMMISS: C = neg2zero(0.0495 * (G - min_val( (3500.0 * Ac / 365), 67.30 ) ) ) else: C = neg2zero(0.0495 * (I - (3500.0 / P) ) ) C = round(lesser_val(C, (Cmax - Dc) ), 2) Cemp = C #--- EI # if not eix: # FIXME: implement max insurable for use when attached to database # At 2010 January for example, max insurable earnings were $43200.00 EI = round(max_val( (0.0178 * (I +G) ), (786.76 - De) ), 2) EIemp = round(EI*1.4, 2) #--- Federal Rate and Konstant # if A <= 41544: #Federal Rate and Konstant R, K = 0.15, 0 elif A > 41544 and A <= 83088: R, K = 0.22, 2908 elif A > 83088 and A < 128800: R, K = 0.26, 6232 else: R, K = 0.29, 10096 ###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###--## #--- Formula to calculate basic federal tax (T3) # # K2 is on Canada or Quebec Pension Plan and Employment Insurance premium for year # # could use a YTD method instead # ###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###--## if COMMISS: first_amnt = neg2zero( 0.15 * (0.0495 * (I1 - 3500) ) ) first_amnt = max_val( first_amnt, 2217.60 ) second_amnt = 0.15 * (0.0178 * I1) second_amnt = max_val( second_amnt, 786.76 ) K2 = first_amnt + second_amnt else: pension_amnt = 0.15 * max_val( (P * C), 2217.60 ) uic_amnt = 0.15 * max_val( (P * EI), 786.76 ) K2 = pension_amnt + uic_amnt K2 = round(K2, 2) K1 = round(0.15 * TC, 2) # Non-refundable personal tax credit # Canada Employment Credit implemented by Mark Jenkins Aug 2006 # I had totally missed its introduction in July :-( K4 = round(lesser_val( 0.15 * A, 0.15*1065), 2) ###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-#-## #--- Formulas to calculate provincial (T2) and territorial tax # ###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-#-## if RESIDES == 'QC': T4 = 0 T3 = round(neg2zero( (R * A) - K - K1 - K2 - K3 - K4), 2) elif Outside.has_key(RESIDES): ptax = provincial_functions[RESIDES] V, V1, V2, S, LCP, T4 = ptax(A, TD1P, P, C, EI, K3P, LCPin, Y, HD, T3) T3 = round(neg2zero( (R * A) - K - K1 - K2 - K3 - K4), 2) else: ptax = provincial_functions[RESIDES] # ptax_xx() where xx is AB, BC etc. # Those functions contain all provincially (and now territorially) specific formula V, V1, KP, K1P, K2P, T4, S, LCP, Y = ptax(A, TD1P, P, C, EI, K3P, LCPin, LCFin, Y, HD) T3 = round(neg2zero( (R * A) - K - K1 - K2 - K3 - K4), 2) T2 = round(neg2zero( T4 + V1 - S - LCP ), 2) ###-###-###-###-###-###-###-###-###-###-###-###-###-#-## #--- Formula to calculate the federal tax payable (T1) # ###-###-###-###-###-###-###-###-###-###-###-###-###-#-## LCF = round(lesser_val(LCFin * 0.15, 750), 2) if RESIDES == 'QC': T1 = neg2zero( (T3 - LCF) - (0.165 * T3) ) T2 = 0 elif RESIDES == 'OT': T1 = neg2zero( T3 + (0.48 * T3) - LCF ) T2 = 0 else: T1 = neg2zero( (T3 - LCF) ) T1 = round(T1, 2) ###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-## #--- Formula to calculate the tax deduction (T) for a pay period # ###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-###-## if RESIDES == 'OT': T = (T1 / P) + L elif COMMISS: T = ( (T1 + T2) / (I1 / G) ) + L else: T = ( (T1 + T2) / P ) + L T = round(T, 2) #gather some variables up prov = round(T2/P, 2) feds = T - prov #rounding C = round(C, 2) EI = round(EI, 2) WCB = round(WCB, 2) ttldeducts = round(T + C + EI + U1 + F + F2 + advance, 2) netpay = I + G - ttldeducts #dict = {'C':C, 'Cemp':Cemp, 'EI':EI, 'EIemp':EIemp,'F':F,'F2':F2, 'G':G, 'I':I, 'L':L, # 'RESIDES':RESIDES, 'T':T, 'U1':U1, 'WCB':WCB, 'advance':advance, 'feds':feds, 'first':first, 'last':last, # 'netpay':netpay, 'prov':prov, 'sin':sin, 'ttldeducts':ttldeducts, 'holpay':holpay, 'grossb4hp':grossb4hp} #return dict return locals() #FIXME: This function should use a dict for returnables, not local namespace. def print_csv(args): #FIXME: Use dict instead of locals del args['args'] for key in args.keys(): exec(key + " = args['" + key + "']") var_list = ['C', 'Cemp', 'EI', 'EIemp', 'F', 'F2', 'G', 'I', 'L', 'RESIDES', 'T', 'U1', 'WCB', 'advance', 'feds', 'first', 'grossb4hp', 'last', 'holpay', 'netpay', 'prov', 'sin', 'ttldeducts'] csv = [args[x] for x in var_list] dump = '' for x in csv: dump = dump + str(x) + " " labels = 'Name, Date, PP_Start, PP_End, Hours, Rate, Reg_Pay, OT, OT_Rate, OT_Pay, Misc_Inc, Subtotal, HP, Gross, CPP, EI, Tax, Addtl_Tax, Misc_Deduct, Adv, Net, Chequenum, WCB, CPPemp, EIemp, FedTtl\012' csv2a = "%s %s" % (last, first) #upto and including OT_Pay csv2b = "%s,%s,%s,%s,%s,%s,%s,%s,%s" % (time.ctime(time.time() ), ppstart, ppend, rhrs, rrate, '=E%s*F%s', ohrs, orate, '=H%s*I%s') #Misc_Inc, Subtotal, HP, Gross, CPP, UI, Tax, Addtl_Tax, Misc_Deduct, Adv csv2c = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" % (str(miscincome), str(grossb4hp), str(holpay), str(I), str(C), str(EI), str(T), str(L), str(U1+F+F2), str(advance) ) #Net, Chequenum, WCB, CPPemp, EIemp, FedTtl, csv2d = "%s, ,%s,%s,%s,%s" % (str(netpay), str(WCB), str(Cemp), str(EIemp), str(T + C + Cemp + EI + EIemp) ) csv2 = "%s,%s,%s,%s,%s" % (csv2a, csv2b, csv2c, csv2d,"\012") if out == 'con': print2con(first, last, sin, feds, prov, ttldeducts, netpay, T, L, U1, F, F2, C, Cemp, EI, EIemp, WCB, I, G, Provinces, RESIDES, advance, hp) elif out == 'dict': return dict elif out == 'csv': print csv elif out == 'dump': print dump elif out == 'spread': if os.path.exists(CWD + "/" + fname): f = open(CWD + "/" + fname, 'r') lines = f.readlines() #read previous entries f.close() if len(lines): n = lines.pop() #remove last line - the sums n = lines.pop() #remove the blank line last_datarow = str(len(lines)+1 ) else: lines = [labels] last_datarow = '2' sums = makesums(last_datarow) csv2 = csv2 % (last_datarow, last_datarow, last_datarow, last_datarow) lines.append(csv2) lines.append(sums) f = open(CWD + "/" + fname, 'w') for n in lines: f.write(n) f.close() elif out == 'html': if hp != 0: hptxt = " (incl. " + str(hp*100) +"% Holiday pay) :" else: hptxt = "" print 'Content-type: text/html\n\n' print '' print 'cdnpayroll output' print '' print '

        Payroll Output using %s

        \n
          ' % (version) referer = os.environ.get("HTTP_REFERER") print '

          Reset to full form and do another?

          ' % home print '

          (Use your "back" button to keep your values.)

          ' print '' print '' print '' % Provinces[RESIDES] print '' print '' % (last, first, sin) print '' % (ppstart, ppend) print '' print '' % (hptxt, I) print '' % (feds) print '' % (prov) print '' % ( T) print '' % (C) print '' % (EI) print '' % (U1) print '' % (L) print '' % (F) print '' % (advance) print '' % (F2) print '' % (ttldeducts) print '' print '' % (netpay) print '
          %s resident tax calculated.
          Name: %s, %sSIN: %s
          Pay Period:%s to %s
          Gross Pay %s%.2f
          Federal Tax:%.2f
          Provincial Tax:%.2f
          Subtotal of taxes:%.2f
          CPP:%.2f
          EI:%.2f
          Union:%.2f
          Requested extra tax:%.2f
          Savings Plan:%.2f
          Advance(s):%.2f
          Required Other:%.2f
          Total Deductions:(%.2f)
          Net Pay Amount:%.2f
          ' print "Employer Costs
          " print "Employer CPP: %.2f
          " % (Cemp) print "Employer EI: %.2f
          " % (EIemp) print "WCB: %.2f
          " % (WCB) print "Total Additional Costs: %.2f
          " % (Cemp + EIemp + WCB) print "
          Revenue Canada Total: %.2f
          " % (T + C + Cemp + EI + EIemp) print "
          Cdnpayroll: %s
          " % (version) pre = '''
                  This program is distributed in the hope that it will be useful,
                  but WITHOUT ANY WARRANTY; without even the implied warranty of
                  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
                  General Public License for more details:GNU General Public License

          ''' print pre #cgi.print_form(form) print '
          ' else: print "No output option used" print input if __name__ == "__main__": print_csv(cdnpayroll(args))