-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathexplain_payslip.rb
469 lines (394 loc) · 19.3 KB
/
explain_payslip.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
#!/usr/bin/env ruby
require 'bigdecimal'
require 'bigdecimal/util'
require 'open-uri'
require 'openssl'
require 'optparse'
require 'ostruct'
require 'rbconfig'
require 'yaml'
# For 1.8 we need to redefine the SSL::VERIFY_PEER constant so that clients can reach
# the version file on Github (mainly a problem on Windows). Modifying this constant is
# only applicable for Ruby versions =< 1.8 so we don't bother doing it for any others (they
# are passed an arg to the open() method). Redefining this constant results in a warning on the
# output. Hopefully users will just ignore it.
OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE unless RUBY_VERSION.to_f > 1.8
REPO_LINK = "http://github.com/nathanshox/PayslipExplainator"
SCRIPT_VERSION = 1.2
SCRIPT_VERSION_FILE_URL = "https://raw.github.com/nathanshox/PayslipExplainator/master/version"
class OptionsParser
#
# Return a structure describing the options.
#
def self.parse(args)
# The options specified on the command line will be collected in *options*.
# Set default values here.
options = OpenStruct.new
options.config_file_path = File.join(File.dirname(File.expand_path(__FILE__)), 'payslip_config.yml')
options.pause = false
options.check_for_update = true
opts = OptionParser.new do |opts|
opts.banner = "Usage: explain_payslip.rb [options]"
opts.separator ""
opts.separator "Specific options:"
opts.on("-c", "--config PATH", "Specify path to a config file") do |path|
options.config_file_path = path
end
opts.on("-p", "--[no-]pause", "Pause after each calculation") do |p|
options.pause = p
end
opts.on("-n", "--no-update-check", "Do not check for new version of script") do
options.check_for_update = false
end
opts.separator ""
opts.separator "Common options:"
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
opts.on_tail("-v", "--version", "Show script version") do
puts "PayslipExplainator"
puts "Version: #{SCRIPT_VERSION}"
exit
end
end
opts.parse!(args)
options
end
end
# Misc methods
# Print section header
def print_header(headline)
puts "\n\n" + "#" * 80
puts "### #{headline} " + ( "#" * (80 - 5 - headline.length))
end
# Load key from config and return value as Hash
def load_hash_from_config(config, key)
value = config[key]
if value.nil?
abort "Did not find #{key} in config file"
end
return value
end
# Load key from config and return value as BigDecimal
def load_bd_from_config(config, key)
value = config[key]
if value.nil?
abort "Did not find #{key} in config file"
end
return BigDecimal.new(value.to_s)
end
# puz (print unless zero)
def puz(string, value)
puts string unless value.zero?
end
# Print out hash values, and return sum of values
def print_and_total_hash(input_hash)
total = BigDecimal.new("0")
input_hash.each do |key, value|
v = BigDecimal.new(value.to_s)
puz "\t#{v.to_digits}\t(#{key.gsub("_", " ").capitalize})", v
total = total + v
end
return total
end
def open_browser(url)
host_os = RbConfig::CONFIG['host_os']
if host_os =~ /mswin|mingw|cygwin/
command = "start #{url}"
elsif host_os =~ /darwin/
command = "open #{url}"
elsif host_os =~ /linux/
command = "xdg-open #{url}"
end
system command
end
# This function doesn't return until it reads something from input, thereby pausing execution
# of the script
def pause
print "\n<<SCRIPT PAUSED. PRESS ANY KEY TO CONTINUE...>>"
gets.chomp
end
def calculate_paye(taxable_amount, standard_cutoff_rate, tax_credits)
result = Hash.new
result['taxable_at_lower_rate_amount'] = taxable_amount > standard_cutoff_rate ? standard_cutoff_rate : taxable_amount
result['taxable_at_higher_rate_amount'] = taxable_amount > standard_cutoff_rate ? (taxable_amount - standard_cutoff_rate) : BigDecimal.new("0")
result['tax_payable_at_lower_rate'] = (result['taxable_at_lower_rate_amount'] * BigDecimal.new("0.20"))
result['tax_payable_at_higher_rate'] = (result['taxable_at_higher_rate_amount'] * BigDecimal.new("0.40"))
result['paye_pre_tax_credits_deduction'] = result['tax_payable_at_lower_rate'] + result['tax_payable_at_higher_rate']
result['paye'] = (result['paye_pre_tax_credits_deduction'] - tax_credits).round 2
return result
end
def calculate_usc(taxable_amount, point_five_percent_band, two_percent_band, four_point_five_percent_band)
result = Hash.new
already_charged = 0
result['chargable_at_point_five'] = taxable_amount > point_five_percent_band ? point_five_percent_band : taxable_amount
already_charged += point_five_percent_band
result['chargable_at_two'] = taxable_amount > (already_charged + two_percent_band) ? two_percent_band : ( taxable_amount > already_charged ? (taxable_amount - already_charged) : BigDecimal.new("0"))
already_charged += two_percent_band
result['chargable_at_four_point_five'] = taxable_amount > (already_charged + four_point_five_percent_band) ? four_point_five_percent_band : ( taxable_amount > already_charged ? (taxable_amount - already_charged) : BigDecimal.new("0"))
already_charged += four_point_five_percent_band
result['chargable_at_eight'] = taxable_amount > already_charged ? (taxable_amount - already_charged) : BigDecimal.new("0")
result['usc_payable_at_point_five'] = (result['chargable_at_point_five'] * BigDecimal.new("0.005"))
result['usc_payable_at_two'] = (result['chargable_at_two'] * BigDecimal.new("0.02"))
result['usc_payable_at_four_point_five'] = (result['chargable_at_four_point_five'] * BigDecimal.new("0.045"))
result['usc_payable_at_eight'] = (result['chargable_at_eight'] * BigDecimal.new("0.08"))
result['usc'] = (result['usc_payable_at_point_five'] + result['usc_payable_at_two'] + result['usc_payable_at_four_point_five'] + result['usc_payable_at_eight']).round 2
return result
end
## START OF MAIN #########################################################################
# Parse arguments
options = OptionsParser.parse(ARGV)
puts "#" * 80
puts "# PAYSLIP EXPLAINATOR"
puts "#" * 80
puts ""
puts "Welcome to the Payslip Explainator. Let's try decrypt your payslip."
puts ""
# Check script version
if options.check_for_update
begin
puts "Checking for new version of the script..."
# TODO Add a timeout here
if RUBY_VERSION.to_f < 1.9
latest_version = open(SCRIPT_VERSION_FILE_URL).read.to_f
else
latest_version = open(SCRIPT_VERSION_FILE_URL, :ssl_verify_mode => OpenSSL::SSL::VERIFY_NONE).read.to_f
end
if latest_version > SCRIPT_VERSION
puts "Version #{latest_version} of script is available. You are currently using version #{SCRIPT_VERSION}."
puts "You can download the latest script from #{REPO_LINK}"
print "Would you like to download the latest version now? (yes/no) >: "
if gets.downcase.strip == 'yes'
puts "Opening browser..."
exit open_browser(REPO_LINK)
else
puts "Continuing execution of this version (#{SCRIPT_VERSION}) of the script"
end
else
puts "You have the latest version of the script"
end
rescue
puts "Couldn't check for latest version of script."
puts "Are you connected to the internet? Can you access #{REPO_LINK} in your web browser?"
puts "Continuing execution of script"
end
end
# Attempt to load config file ############################################################
if File.exists? options.config_file_path
config_file = YAML::load_file options.config_file_path
if config_file
print "\nYour payslip.yml file has been found and loaded\n\n"
regular_salary = load_bd_from_config config_file, "regular_salary"
pension_contribution_percentage = load_bd_from_config config_file, "pension_contribution_percentage"
espp_contribution_percentage = load_bd_from_config config_file, 'espp'
car_allowance_hash = load_hash_from_config config_file, "car_allowance"
salary_sacrifice_hash = load_hash_from_config config_file, "salary_sacrifice"
benefit_in_kind_hash = load_hash_from_config config_file, "benefit_in_kind"
misc_deductions_hash = load_hash_from_config config_file, "misc_deductions"
standard_cutoff_rate = load_bd_from_config config_file, "standard_cutoff_rate"
tax_credit = load_bd_from_config config_file, "tax_credit"
usc_point_five_percent_band = load_bd_from_config config_file, "point_five_percent_band"
usc_two_percent_band = load_bd_from_config config_file, "two_percent_band"
usc_four_point_five_percent_band = load_bd_from_config config_file, "four_point_five_percent_band"
end
else
abort "Couldn't find #{options.config_file_path}. Does this file exist?"
end
# Get other input values #################################################################
print 'Did you receive overtime or on-call pay this month? Enter 0 for no extra pay. >: '
extra_pay = gets.to_d
print 'Did you receive a PL&I bonus this month? Enter 0 for no bonus. >: '
pli_bonus = gets.to_d
print 'Did you receive a CAP Award this month? Enter 0 for no award. >: '
gross_bonus_award = gets.to_d
print 'Did you receive a Connected Recognition Award this month? (yes or no) >: '
if gets.strip == 'yes'
print 'What is the total voucher amount? >: '
cr_voucher_amount = gets.to_d
print 'what is the total gross amount on your payslip? >: '
cr_gross_amount = gets.to_d
else
cr_voucher_amount = BigDecimal.new("0")
cr_gross_amount = BigDecimal.new("0")
end
bik_to_enter = true
print 'Do you have a variable benefit in kind you want to enter? (yes or no) >: '
bik_to_enter = gets.strip == 'yes' ? true : false
while bik_to_enter
print 'What is the benefit in kind? >: '
var_bik_key = gets.strip
print 'How much is the benefit in kind? >: '
var_bik_value = gets.strip
benefit_in_kind_hash[var_bik_key] = var_bik_value
print 'Do you have another variable benefit in kind you want to enter? (yes or no) >: '
bik_to_enter = gets.strip == 'yes' ? true : false
end
print 'Did you receive any refunds this pay period? Enter 0 for no refunds >: '
refund = gets.to_d
print 'Did you receive an ESPP gain this pay period? Enter 0 for no gain >: '
espp_gain = gets.to_d
print_header "Input Values"
puts "These are the values the script is using to calculate your payslip\n"
puts "-Regular Salary: #{regular_salary.to_digits}"
puts "-Overtime/On-Call Pay: #{extra_pay.to_digits}"
puts "-PL&I Bonus: #{pli_bonus.to_digits}"
puts "-CAP Award: #{gross_bonus_award.to_digits}"
puts "-Connected Recognition Award"
puts "\tVoucher Amount: #{cr_voucher_amount.to_digits}"
puts "\tGross Amount: #{cr_gross_amount.to_digits}"
puts "-Pension Contribution: #{pension_contribution_percentage.to_digits}%"
puts "-ESPP: #{espp_contribution_percentage.to_digits}%"
puts "-Car Allowance: #{car_allowance_hash}"
puts "-Salary Sacrifice: #{salary_sacrifice_hash}"
puts "-Refund: #{refund.to_digits}"
puts "-Benefit in Kind: #{benefit_in_kind_hash}"
puts "-Misc Deductions: #{misc_deductions_hash}"
puts "-ESPP Gain: #{espp_gain.to_digits}"
puts "-PAYE Standard rate cutoff: #{standard_cutoff_rate.to_digits}"
puts "-PAYE Tax credit: #{tax_credit.to_digits}"
puts "-USC 0.5% Band: #{usc_point_five_percent_band.to_digits}"
puts "-USC 2% Band: #{usc_two_percent_band.to_digits}"
puts "-USC 4.5% Band: #{usc_four_point_five_percent_band.to_digits}"
pause unless !options.pause
# Salary Sacrifices ######################################################################
print_header "Salary Sacrfices"
salary_sacrifice_total = print_and_total_hash salary_sacrifice_hash
puts ""
puts "TOTAL SALARY SACRIFICE = #{salary_sacrifice_total.to_digits}"
pause unless !options.pause
# Car Allowance ##########################################################################
print_header "Car Allowance"
if car_allowance_hash["type"] == "cash"
puts "Your car allowance is paid in cash so is considered in your Gross Income below"
elsif car_allowance_hash["type"] == "bik"
puts "Your Car Allowance is a Benefit in Kind so it has been added to that section below"
benefit_in_kind_hash["car_allowance"] = car_allowance_hash["value"].to_s
else
puts "You have not specified you are in receipt of Car Allowance."
puts "If you are, change your config to 'cash' or 'bik' as appropriate"
end
pause unless !options.pause
# Gross Income ###########################################################################
print_header "Gross Income"
puz "Gross Income\t+ #{regular_salary.to_digits}\t\t(Regular salary)", regular_salary
puz "\t\t+ #{extra_pay.to_digits}\t\t(Overtime/On-Call Pay)", extra_pay
puz "\t\t+ #{gross_bonus_award.to_digits}\t\t(CAP Award)", gross_bonus_award
puz "\t\t+ #{pli_bonus.to_digits}\t\t(PL&I Bonus)", pli_bonus
puz "\t\t+ #{cr_gross_amount.to_digits}\t\t(Connected Recognition Gross Amount)", cr_gross_amount
if car_allowance_hash["type"] == "cash"
car_allowance = BigDecimal.new(car_allowance_hash["value"].to_s)
puts "\t\t+ #{car_allowance.to_digits}\t\t(Car Allowance)"
else
car_allowance = BigDecimal.new("0")
end
puz "\t\t- #{salary_sacrifice_total.to_digits}\t\t(Salary Sacrifices)", salary_sacrifice_total
gross_income = regular_salary + extra_pay + gross_bonus_award + pli_bonus + cr_gross_amount + car_allowance - salary_sacrifice_total
puts ""
puts "TOTAL GROSS INCOME = #{gross_income.to_digits}"
pause unless !options.pause
# Calculate Benefit in Kind ##############################################################
print_header "Benefit in Kind"
bik_total = print_and_total_hash benefit_in_kind_hash
puts ""
puts "TOTAL BENEFIT IN KIND = #{bik_total.to_digits}"
pause unless !options.pause
# Calculate Pension Contribution #########################################################
if pension_contribution_percentage > 0
print_header "Pension Contribution"
puts "Your pension contribution is #{pension_contribution_percentage.to_digits}%"
puts ""
puts "Input for pension contribution = #{regular_salary.to_digits} (Regular Salary)"
puts ""
pension_contribution = ((regular_salary / 100) * pension_contribution_percentage).round(2)
puts "#{regular_salary.to_digits} @ #{pension_contribution_percentage.to_digits}% = #{pension_contribution.to_digits}"
puts ""
puts "TOTAL PENSION CONTRIBUTION = #{pension_contribution.to_digits}"
else
pension_contribution = BigDecimal.new("0")
end
pause unless !options.pause
# Calculate PAYE #########################################################################
print_header "PAYE"
puz "Input for PAYE\t+ #{gross_income.to_digits}\t\t(Gross Income)", gross_income
puz "\t\t+ #{bik_total.to_digits}\t\t(Benefit In Kind)", bik_total
puz "\t\t+ #{espp_gain.to_digits}\t\t(ESPP Gain)", espp_gain
puz "\t\t- #{pension_contribution.to_digits}\t\t(Pension Contribution)", pension_contribution
paye_input = gross_income + bik_total + espp_gain - pension_contribution
puts "Total Input\t= #{paye_input.to_digits}"
puts ""
paye_result = calculate_paye paye_input, standard_cutoff_rate, tax_credit
puts "#{paye_result['taxable_at_lower_rate_amount'].to_digits} @ 20%\t #{paye_result['tax_payable_at_lower_rate'].round(2).to_digits}\t(#{paye_result['tax_payable_at_lower_rate'].to_digits})"
puts "#{paye_result['taxable_at_higher_rate_amount'].to_digits} @ 40%\t #{paye_result['tax_payable_at_higher_rate'].round(2).to_digits}\t(#{paye_result['tax_payable_at_higher_rate'].to_digits})"
puts "\t\t= #{paye_result['paye_pre_tax_credits_deduction'].round(2).to_digits}\t(#{paye_result['paye_pre_tax_credits_deduction'].to_digits})"
puts "\t\t- #{tax_credit.to_digits} (Monthly tax credit)"
puts ""
puts "TOTAL PAYE\t= #{paye_result['paye'].to_digits}"
pause unless !options.pause
# Calculate USC ##########################################################################
print_header "USC"
puz "Input for USC\t+ #{gross_income.to_digits}\t\t(Gross Income)", gross_income
puz "\t\t+ #{bik_total.to_digits}\t\t(Benefit In Kind)", bik_total
puz "\t\t+ #{espp_gain.to_digits}\t\t(ESPP Gain)", espp_gain
usc_input = gross_income + bik_total + espp_gain
puts "Total Input\t= #{usc_input.to_digits}"
puts ""
usc_result = calculate_usc usc_input, usc_point_five_percent_band, usc_two_percent_band, usc_four_point_five_percent_band
puts "#{usc_result['chargable_at_point_five'].to_digits} @ 0.5%\t #{usc_result['usc_payable_at_point_five'].round(2).to_digits}\t(#{usc_result['usc_payable_at_point_five'].to_digits})"
puts "#{usc_result['chargable_at_two'].to_digits} @ 2%\t #{usc_result['usc_payable_at_two'].round(2).to_digits}\t(#{usc_result['usc_payable_at_two'].to_digits})"
puts "#{usc_result['chargable_at_four_point_five'].to_digits} @ 4.5%\t #{usc_result['usc_payable_at_four_point_five'].round(2).to_digits}\t(#{usc_result['usc_payable_at_four_point_five'].to_digits})"
puts "#{usc_result['chargable_at_eight'].to_digits} @ 8%\t #{usc_result['usc_payable_at_eight'].round(2).to_digits}\t(#{usc_result['usc_payable_at_eight'].to_digits})"
puts ""
puts "TOTAL USC\t= #{usc_result['usc'].to_digits}"
pause unless !options.pause
# Calculate PRSI #########################################################################
print_header "PRSI"
puz "Input for PRSI\t+ #{gross_income.to_digits}\t\t(Gross Income)", gross_income
puz "\t\t+ #{bik_total.to_digits}\t\t(Benefit In Kind)", bik_total
puz "\t\t+ #{espp_gain.to_digits}\t\t(ESPP Gain)", espp_gain
prsi_input = gross_income + bik_total + espp_gain
puts "Total Input\t= #{prsi_input.to_digits}"
puts ""
total_prsi = (prsi_input * BigDecimal.new("0.04")).round(2)
puts "#{prsi_input.to_digits} @ 4% = #{total_prsi.to_digits}"
puts ""
puts "TOTAL PRSI\t= #{total_prsi.to_digits}"
pause unless !options.pause
# Calculate ESPP #########################################################################
if espp_contribution_percentage > 0
print_header "ESPP"
puz "Input for ESPP\t+ #{regular_salary.to_digits}\t(Regular Salary)", regular_salary
puz "\t\t+ #{extra_pay.to_digits}\t(Overtime/On-Call Pay)", extra_pay
puz "\t\t+ #{pli_bonus.to_digits}\t(PL&I Bonus)", pli_bonus
espp_input = regular_salary + extra_pay + pli_bonus
puts "Total Input\t= #{espp_input.to_digits}"
puts ""
espp = ((espp_input / 100) * espp_contribution_percentage).round(2)
puts "#{espp_input.to_digits} @ #{espp_contribution_percentage.to_digits}% = #{espp.to_digits}"
puts ""
puts "TOTAL ESPP CONTRIBUTION = #{espp.to_digits}"
else
espp = BigDecimal.new("0")
end
pause unless !options.pause
# Misc Deductions ########################################################################
print_header "Misc Deductions"
misc_deductions_total = print_and_total_hash misc_deductions_hash
puts ""
puts "TOTAL MISC DEDUCTIONS = #{misc_deductions_total.to_digits}"
pause unless !options.pause
# Net Pay ################################################################################
print_header "Net Pay"
puz "\t #{gross_income.to_digits}\t(Gross Income)", gross_income
puz "\t+ #{refund.to_digits}\t(Refund)", refund
puz "\t- #{cr_voucher_amount.to_digits}\t(Connected Recognition Voucher)", cr_voucher_amount
puz "\t- #{paye_result['paye'].to_digits}\t(PAYE)", paye_result['paye']
puz "\t- #{usc_result['usc'].to_digits}\t(USC)", usc_result['usc']
puz "\t- #{total_prsi.to_digits}\t(PRSI)", total_prsi
puz "\t- #{pension_contribution.to_digits}\t(Pension Contribution)", pension_contribution
puz "\t- #{espp.to_digits}\t(ESPP)", espp
puz "\t- #{misc_deductions_total.to_digits}\t(Misc Deductions)", misc_deductions_total
net_income = gross_income + refund - cr_voucher_amount - paye_result['paye'] - usc_result['usc'] - total_prsi - pension_contribution - espp - misc_deductions_total
puts "\t= #{net_income.to_digits}"
puts ""
puts "TOTAL NET INCOME = #{net_income.to_digits}"