Skip to content

Instantly share code, notes, and snippets.

@hendragunz
Forked from ziweizhou/clock.rb
Last active August 29, 2015 14:15
Show Gist options
  • Save hendragunz/f89f1fe744a2ebe7bb03 to your computer and use it in GitHub Desktop.
Save hendragunz/f89f1fe744a2ebe7bb03 to your computer and use it in GitHub Desktop.

Design Idea

inspired by this design strategy

http://stackoverflow.com/questions/3585428/programmatically-managing-a-balance-of-time-sick-vacation

  • we use utilize task table to track how many days an employee have at one point of time.
  • any time user submit a timeoff_task, we document the total_hours field with negative number of hours.
  • any time the system runs an accrue task, we document the total_hours with a positive number of hours the employee accrued during this period.
  • any time the system runs an adjustment task, we document the total_hours with a positive or negative number of hours the employee. (it could be an adjustment from his manager or because of the cap rule they set inside a policy)

Background

currently we allow company to have accrue rate of following enum accrue_rate:[:weekly, :bi_weekly, :monthly, :yearly]. As a result we need to have a background process to run on weekly, biweekly, monthly and yearly to accrue vacation and sick days to user.

current solution

we currently use clockwork lib\clock.rb to run this task on all policies. i need you to double check its logic and make sure it works as design.

reference task_hrdate.png

a. for any accrue task should be Task.create(:type=>:system, :name=> "accrue", :amount => xxxx) b. for any adjustment task (which is trigger by cap policy.
Task.create(:type=>system, :name=> "adjustment", :amount => xxxx) c. The amount should be negative if it is deducting from user, positive if it is adding to user d. so if u want to calculate how many days this employee can spend (task_type==system) e. so if u want to calculate how many days this employee already spent (task_type==:timeoff)

TASK

  1. code the accrue vacation and sick day.
  2. code the adjustment (adjustment comes in when there is a cap set on carry over)
  3. code the tenture (the way of calculating user's total vacation days for a year).

Example

Company uses time off policy to decide how many dates an employee should have as vacation days and sick days, etc. Please check the details documentation in doc/TimeOffPolicy.html to details

#Every first day of the month, this accrue method will run and it will add the accrued amount to each user. On Nov 1st, 2014
policy = TimeoffPolicy.create!(
  :hours_per_year => 160,
  :accrue_rate => :accrue_by_time,
  :accrue_rate => :monthly,
  :allow_negative => false,
  :carry_over => :yearly_cap,
  :carry_over_cap => 10,
  :total_personal_days => 10,
  :total_sick_days => 5,
  :work_hours_per_day => 8,
  :created_at => '10/1/2014'
)
user1 = User.find(1)
user1.start_date = '10/15/2014'
hours_per_month = 160 / 12 
if user1.start_date - Date.today < 1_month
  hours_per_month = hours_per_month / weeks_of_worked
end
TimeoffTask.create!(:sub_type => :accrue, :user => user, :total_hours=> hours_per_month) 
*********If it runs on Jan 1st, 2105************
if user.total_accrue_hours > policy.carry_over_cap
  TimeoffTask.create!(:sub_type => :adjustment, :user => user, :amount=> policy.carry_over_cap - user.total_accrue_hours)
end

Example 2

Suppose we have a policy

  • Total per year: 12 days
  • Accural Type: accrue by time adv
  • Accrue Rate: yearly
  • Do you allow vacation days to be carried over to next year? YES
  • How many hours do you allow them to be carried over per year?: 5 days
  • Does the carried over the vacation days ever expire?: YES
  • Expired date: Mar 31
  • (Ignore tenure rules)

On Jan 1, Ken has 10 remaining days of last year and 12 days of this year.

So total vacation days of Ken to Jan 01 is 17

On Feb he takes 2 days off.

So, total vacation days of Ken to Mar is 17 - 2 = 15

On Apr 01, total vacation days of Ken is 12 (because Expired date for carrying over = Mar 31)

Example 3

Suppose we have a policy

  • Total per year: 12 days
  • Accural Type: accrue by time adv
  • Accrue Rate: monthly
  • Do you allow vacation days to be carried over to next year? YES
  • How many hours do you allow them to be carried over per year?: 12 days
  • Does the carried over the vacation days ever expire?: YES
  • Expired date: Mar 31
  • (Ignore tenure rules)

On Jan 01, Ken has 12 remaining days of last year and 1 days of this year

So total vacation days of Ken to Jan 01 is 13 Total vacation days of Ken to Feb 01 is 14 Total vacation days of Ken to Mar 01 is 15

So, Total vacation days of Ken to Apr 01 is 4 (because Expired date for carrying over = Mar 31)

Example 4

Suppose we have a policy

  • Total per year: 12 days
  • Accural Type: accrue by time
  • Accrue Rate: monthly
  • Do you allow vacation days to be carried over to next year? YES
  • How many hours do you allow them to be carried over per year?: 12 days
  • Does the carried over the vacation days ever expire?: NO
  • (Ignore tenure rules)

On Jan 01, Ken has 12 remaining days of last year and 0 days of this year

So total vacation days of Ken to Jan 01 is 12 Total vacation days of Ken to Feb 01 is 13 Total vacation days of Ken to Mar 01 is 14 ... Total vacation days of Ken to Jan 01 next year is 12

FAQ

  1. A policy will apply for multiple years?

the accrue task will only start after you have activated in our system. it doesn't care how long the employee has been with the company, if you want to move the employee's vacation days over from previous system, you can create an adjustment.

  1. "Are employees allowed to borrow PTO days from the future?": future means "next year only"?

Yes, but there is a cap, if no cap is set, then he can borrow as many.

  1. If user is not assigned to a policy? How can calculate total vacation days for this user (i.e. this user is a new employee)?

You cannot in this case. every employee has to have a policy.

  1. In case "yearly" the clockwork runs on the first date of a year, a new employee join in Mar and is assigned to this policy. How can this policy apply for the new employee?

The new employee's vacation days will be prorated.

  1. With tab 2, in working time configuration area, this configuration will impact to where?

No the scope of this, it will impact the tasks module.

  1. Follow up the question No.5, the working time is configured "From" and "To". How can we know the lunch break in working time configuration?

we ignore that for now.

  1. How can relate employee to a policy? From tab 4 (leave it for now as you mentioned) and from employee page?

this can be set in employee module.

  1. In case "edit a policy", it could break the rules of old policy which was run? What should we do?

nothing. accrue happens with the new policy at the next run cycle.

9.In case "Accrual Type" = "Accrue By Time", "Accrue Rate" = "Yearly", "Total days" = 12 days per year. It means that CLOCKWORK runs on the first day of year, total will be 0 days as definition of "Accrual By Time" or be equal to 12.

zero

  1. Please explain if the number is 12

With tab 2, configure anniversary of hire
User only added 1st = 1 day
2nd = 3 days
5th = 6 days
Please correct my understanding
3rd & 4th = 3 days? (following rule of 2nd)

No, it means after first anniversary, user has 13 days, after second user has 16 days, after 5th, then user has 22 days

  1. With anniversary rules, Ken joined in the company on Feb 29, 2014, 1st anniversary he will be added 1 extra day, next year 2015 has Feb 28, 2015 (the last day of Feb) is the anniversary date? Is it correct?

Yes.

  1. With accrue rate = yearly, in tab 2, admin only configures 1st = 1 day, 2nd = 2 days, total vacation days = 12 and not carry over to next year.

On Feb 03, 2014 an employee joined in company
On Feb 03, 2015 total vacation days = 13
On Jan 01, 2016 total vacation days = 13
On Feb 03, 2016 total vacation days = 14
Is it correct?

yes if its accrue_type == :accrue_by_time_adv

  1. Adjustment will run according to hours or days?

Day

  1. In case Monthly/Accrue By Time Adv/ Total days per year = 15.

May 25, 2014, Ken joined in company
Jan 01, 2015, plus-days for this month is (15/12) days
Feb 01, 2015, plus-days for this month is (15/12) days
Jun 01, 2015, plus-days for this month is (16/12) days

Yes if tenture is set to 1st anniversery to 1 day

require File.expand_path('../../config/boot', __FILE__)
require File.expand_path('../../config/environment', __FILE__)
require 'clockwork'
include Clockwork
every(1.day, 'Yearly Carry Job', :if => lambda { |t| t.day == 1 and t.month == 1}, :at => '0:00'){
now = Time.zone.now
if now.month == 1 and now.day == 1
# clean sick days and personal days
User.all.each do |user|
next if not user.policy.present?
Task.create(sub_type: :personal, user: user,
total_hours: user.policy.total_personal_days * user.policy.work_hours_per_day - user.total_remain_personal_hours)
Task.create(sub_type: :sick, user: user,
total_hours: user.policy.total_sick_days * user.policy.work_hours_per_day - user.total_remain_sick_hours)
end
TimeoffPolicy.where(carry_over: :co_no_cap).each do |policy|
policy.users.each do |user|
Task.create(sub_type: :accrue, user: user, total_hours: -user.total_remain_vacation_hours)
end
end
TimeoffPolicy.where(carry_over: :co_total_cap).each do |policy|
policy.users.each do |user|
remain = user.total_remain_vacation_hours
if remain > user.policy.carry_over_cap
Task.create(sub_type: :accrue, user: user, total_hours: -(remain - user.policy.carry_over_cap))
end
end
end
# @todo calculate yearly_cap, take vacation days of the year before last year into account
# carry_over = nolimit, sick days and personal days have been cleaned, do nothing to vacation days
end
}
# use multi job to make job info log more clear
every(1.day, 'Yearly Accrue Job', :if => lambda { |t| t.day == 1 and t.month == 1}, :at => '0:00'){
TimeoffPolicy.where(accrue_rate: :yearly).each do |policy|
policy.users.each do |user|
Task.create(sub_type: :accrue, user: user, total_hours: user.total_vacation_hours)
end
end
}
every(1.day, 'Monthly Accrue Job', :if => lambda { |t| t.day == 1 }, :at => '0:00'){
# @todo differentiate :accrue_by_time and :accrue_by_time_adv
TimeoffPolicy.where(accrue_rate: :monthly).each do |policy|
policy.users.each do |user|
Task.create(sub_type: :accrue, user: user, total_hours: (user.total_vacation_hours / 12.0 - 0.005).round(2))
end
end
}
class CreateTasks < ActiveRecord::Migration
def change
create_table :tasks do |t|
t.string :name
t.text :description
t.references :author, index: true
t.references :user, index: true
t.references :assignee, index: true
t.datetime :start_at
t.datetime :end_at
t.integer :task_type
t.string :ancestry
t.string :workflow_state
t.text :attachments
t.text :details
t.float :total_hours
t.timestamps
end
add_index :tasks, :ancestry
add_index :tasks, :task_type
add_index :tasks, :name
end
end
class CreateTimeoffPolicies < ActiveRecord::Migration
def change
create_table :timeoff_policies do |t|
t.string :name
t.integer :base_vacation_days, :null => false, :default => 20
t.integer :accrue_type, :null => false, :default => 0
t.integer :accrue_rate, :null => false, :default => 0
t.boolean :allow_negative
t.integer :negative_cap
t.boolean :carry_over,:null => false, :default => false
t.integer :carry_over_cap
t.date :carry_over_expire_on
t.integer :total_sick_days
t.integer :total_personal_days
t.integer :work_hours_per_day, :null => false, :default => 8
t.string :beginning_of_workday,:null => false, :default => "9:00 AM"
t.string :end_of_workday,:null => false, :default => "5:00 PM"
t.text :work_week,:null => false, :default => %w(mon tue wed thu fri)
t.text :work_hours, :default => {}
t.text :tentures, :default => []
t.text :holidays, :default => []
t.text :work_dates, :default => []
t.text :timeoff_types,:null => false, :default => [{:name => "Vacation", :type => "PTO"}]
t.references :company, index: true
t.string :zone,:null => false, :default => 'UTC'
t.timestamps
end
end
end
# coding: utf-8
class Task < ActiveRecord::Base
belongs_to :author, class_name:"User", foreign_key: :author_id
belongs_to :user, class_name:"User", foreign_key: :user_id
belongs_to :assignee, class_name:"User", foreign_key: :assignee_id
after_update :calculate_hours
validate :validate_total_hours
# @!attribute sub_type
# @return [Enumerable<String>] task's sub types to further distinguish them
# @example
# :start => starting balance
# :accrue => accrue basing on the accrue rate
# :adjustment => manual adjustment by manager or admin
# :vacation => Vacations
# :sick => Sick Leave
# :personal => Personal Holidays
# :others => Misc ones, employee won't be in office, but it doesn't consider his vacation/sick/personal days. such as jury duty or working from home.
enum sub_type:[:start, :accrue, :adjustment, :vacation, :sick, :personal, :others]
# @!attribute total_hours
# @return [Integer] it is a calculated field when user made changes on the start_at or the end_at field
# @!attribute author
# @return [User] the person who opened task
# @!attribute assignee
# @return [User] task's assignee
# @!attribute user
# @return [User] who is this task for. like user1 create a task for user2.
# @!attribute name
# @return [String] the task's title, but it is possible to be used to hold the descriptions of user defined sub-types
# @!attribute desciption
# @return [Text] user's memo/reasons if it is a sick leave
# @!attribute start_at
# @return [DateTime] when is this task start
# @!attribute end_at
# @return [DateTime] when is this task ends
# @!attribute attachment
# @return [Array<Object>] attachments for this task.
serialize :attachments
mount_uploaders :attachments, FileUploader
# @!attribute status
# @return [Enumerable<String>] task's status
# @example
# :pending => pending for level1 approval
# :pending2 => pending for secondary approval
# :approved => approved by all manager
# :rejected => rejected by manager.
# :paid => Use in expense request
# :booked => Used in travel request.
# :cancelled => cancelled, could be from the requester
# enum status:[:pending, :pending2, :approved, :rejected, :paid, :booked, :cancelled]
include Workflow
workflow do
state :pending do
event :approve, :transitions_to => :approved, :if => :all_approved?
event :reject, :transitions_to => :rejected
event :cancel, :transitions_to => :cancelled
end
state :approved do
event :pay, :transitions_to => :paid
event :book, :transitions_to => :booked
end
state :booked
state :paid
state :cancelled
state :approved
state :rejected
end
# @!attribute details
# depending the type of the task, the content of the data field is different.
# @return [Object] details in Hash/Openstruct format
serialize :details
# @!attribute followers
# @return [Array<User>]
# array of people who will be notified and possibly all the followers should have this task on their calendar.
# 如果是出公差,从深圳分公司到上海总公司,那么这个Task,应该出现在所有followers的日历上面。
acts_as_followable
acts_as_votable
has_ancestry
# comments and public activity
include PublicActivity::Model
tracked
# tracked owner: Proc.new{ |controller, model| controller.current_user }
# @todo calculate the total hours used for this task.
def calculate_hours
end
def validate_total_hours
end
# @!method all_approved?
# @return [Boolean]
# has all the user's manager approved this task?
def all_approved?
self.user.following_users.count == self.get_upvotes.size
end
def approve
self.vote_by :voter => self.assignee, :vote => 'yes'
# auto change the assignee to the manager who has not approved this task yet.
next_assignee = self.user.following_users.select {|f| !self.votes_for.up.voters.include?(f) }.first
if next_assignee.present?
self.assignee = next_assignee
self.save!
end
end
def on_approved_exit(new_state, event, *args)
# notify followers for its approval
self.followers.map {|f| self.create_activity(owner: f, key: 'task.change', action: :mood_changed)}
# mark all sub-tasks as approved
self.descendants.each { |t| t.workflow_state = 'approved'}
end
end
# coding: utf-8
class TimeoffPolicy < ActiveRecord::Base
# @!attribute name
# @return [String] the name of the policy
# @!attribute base_vacation_days
# @return [Integer] total vacation days per year
# @!attribute accrue_rate
# @return [Enumerable<String>] indicate how often this policy should be ran to calculate employees vacation days balance.
enum accrue_rate:[:weekly, :bi_weekly, :monthly, :yearly]
# @!attribute accrue_type
# @return [Enumerable<String>] Vacation Data Accural Type
# @example Assuming you have 20 vacation days per year (假设你每年有20天休假)
# :accrue_by_time => if it is accrue per month, u will have 20/12 = 1.66 days on Feb 1st
# :accrue_by_time_adv => if it is accrue per month, u will have 20/12 = 1.66 days on January 1st
# @note if accrue_by_time_adv and :yearly, means all days will be given to user at once
enum accrue_type: [:accrue_by_time, :accrue_by_time_adv]
# @!attribute tentures
# We’ll automatically increase your employees’ vacation accrual rate based on the milestones you set below. (These bonuses will be your default policy, but you can turn this feature off for individual employees.)
# 根据您设置下面的里程碑,我们将自动增加员工的假期累积率。 (这些奖金将是你的默认策略,但您可以关闭此功能对于员工个人。)
# @return [Array<Object>] List of tenture. 年假逐年递增 年假递增率
# @example
# self.tentures = [{anniversary:2, extra_days: 2}, {anniversary:4, extra_days: 2}]
# means if you worked for 2 years, then you have 22 days vacation. If 4 years then u have 24 days.
# 如果你工作了2年指,那么你有22天假期。如果4年那么你有24天假期。
serialize :tentures
# @!attribute carry_over
# @return [Boolean] Carry Over ? (跨年累积)
# @!attribute carry_over_cap
# @return [Integer] total carry over hours
# @note if carry_over_cap is ZERO, it means cannot carry over any hours.
# @!attribute carry_over_expire_on
# @return [Date] total carry over hours expiration date
# @example
# carry_over:true,carry_over_cap:16, carry_over_expire_on:nil


# with this setting, every year carry_over 16 hours and it never expires。

# 如果 这样就是 每年carry_over 16小时,永不过期。
# carry_over:true,carry_over_cap:16, carry_over_expire_on:"12/31/1999"
# with this setting, every year carry_over 16 hours,it means all the accumulated vacations days will be expired at the end of the year. 

# 如果 这样就是 每年carry_over 16小时,前年都不算,相当于yearly。
# 
carry_over:true,carry_over_cap:16, carry_over_expire_on:“03/01/2000”
# with this setting, every year carry_over 16 hours,it means all the accumulated vacations days will be expired at the end of Feburary.
# 如果 这样就是 每年carry_over 16小时,以前累积的每年3月过期。
# carry_over:true,carry_over_cap:16, carry_over_expire_on:“06/01/2000”
# with this setting, every year carry_over 16 hours,it means all the accumulated vacations days will be expired at the end of May.
# 如果 这样就是 每年carry_over 16小时,以前累积的每年6月过期。
# @!attribute allow_negative
# @return [Boolean] Do you allow user to borrow vacation days in advance?
# @!attribute negative_cap
# @return [Integer] how many vacation hours that user can borrow in advance
# @!attribute total_sick_days
# @return [Integer] total allowed sick days (病假)
# @!attribute total_personal_days
# @return [Integer] total allowed personal days (个人年假)
# @!attribute work_hours_per_day
# @return [Integer] total work hours per day
# @!attribute beginning_of_workday
# @return [String] start time for every working day. '9:00 am'
# @!attribute end_of_workday
# @return [Integer] end time for every working day. '5:00 pm'
# @example We can adjust the start and end time of our business hours
# self.beginning_of_workday = "8:30 am"
# self.end_of_workday = "5:30 pm"
# @!attribute work_week
# @return [Array<String>] list of working week days. '%w(mon tue wed thu fri)'
# @example
# self.work_week = '%w(mon tue wed thu fri)'
serialize :work_week
# @!attribute work_hours
# @return [Hash] list different working hours per day. Not sure if it is necessary.
# @example as alternative we also can change the business hours for each work day:
# self.work_hours = {
# :mon=>["9:00","17:00"],
# :fri=>["9:00","17:00"],
# :sat=>["10:00","15:00"]
# }
serialize :work_hours
# @!attribute work_dates
# @return [Array<Hash>] a list of working dates, which addition to the work_week setting.
# @example
# work_dates = ["02/01/2015","02/14/2015"]
serialize :work_dates
# @!attribute holidays
# @return [Array<Hash>] a list of holidays
# @example
# holidays = [{:name=>"Indepedence Day", :date=>"10/01/2014", :repeat => true}, {:name=>"New Year", :date=>"1/01/2015", :repeat => true}]
serialize :holidays
# @!attribute types
# @return [Array<Hash>] a list of types
# @example
# self.timeoff_types = [{:name=>"Sick Day", :type=>"sick_leave"}, {:name=>"Personal Day", :type=>"personal_leave"}]
serialize :timeoff_types
belongs_to :company, class_name:"Company"
has_many :users, class_name:"User"
# calculate how many hours has user accrued in this policies period.
# @todo calculate how many hours has user accrued in this policies period.
# @example Every first day of the month, this accrue method will run and it will add the accrued amount to each user. On Nov 1st, 2014
# policy = TimeoffPolicy.create!(
# :hours_per_year => 160,
# :accrue_rate => :accrue_by_time,
# :accrue_rate => :monthly,
# :allow_negative => false,
# :carry_over => :yearly_cap,
# :carry_over_cap => 10,
# :total_personal_days => 10,
# :total_sick_days => 5,
# :work_hours_per_day => 8,
# :created_at => '10/1/2014'
# )
# user1 = User.find(1)
# user1.start_date = '10/15/2014'
# hours_per_month = 160 / 12
# if user1.start_date - Date.today < 1_month
# hours_per_month = hours_per_month / weeks_of_worked
# end
# TimeoffTask.create!(:sub_type => :accrue, :user => user, :amount=> hours_per_month)
# *********If it runs on Jan 1st, 2105************
# if user.total_accrue_hours > policy.carry_over_cap
# TimeoffTask.create!(:sub_type => :adjustment, :user => user, :amount=> policy.carry_over_cap - user.total_accrue_hours)
# end
def accrue
users.each do |user|
TimeoffTask.create(:sub_type => :accrue, :user => user, :amount=> amount)
end
end
def work_hours_hash
hash = {}
self.work_hours.each do |item|
hash[item[:day]] = item
end
hash
end
# compatible with business_time
def each (&block)
# XXX: _weekdays
keys = ['beginning_of_workday', 'end_of_workday', 'work_week', 'work_hours']
keys.each do |k|
yield k, self.attributes[k]
end
# reformat work_dates
yield 'work_dates', self.work_dates.map{|i| Date.parse(i)}
# reformat holidays
yield 'holidays', self.holidays.inject([]){|res, i|
d = Date.parse(i[:date])
if i[:repeat]
res << d.change(year: Date.today.year)
res << d.change(year: Date.today.year + 1)
else
res << d
end
}
end
end
class User < ActiveRecord::Base
# @!attribute role
# @return [Enumberable<String>] companyadmin role is assign to the company's admin
enum role: [:user, :corp_admin, :admin]
# @!attribute employment_type
# @return [Enumberable<String>] companyadmin role is assign to the company's admin
enum employment_type: [:full_time, :part_time, :contractor]
# @!attribute status
# @return [Enumberable<String>] if he is still a part of this company
enum status: [:active, :terminated]
# @!attribute email
# @!attribute join_on
# the date joined this company.
# calculate the total vacation days this user has in this calendar year.
# It can include days from previous years, depending on the accural type.
# @return [Integer] the total vacation days
def total_vacation_hours
total_vacation_days * self.policy.work_hours_per_day
end
# calculate total vacation days of this year
def total_vacation_days
self.policy.tentures.inject(self.policy.base_vacation_days) do |res , i|
res + (Time.zone.now - i[:anniversary].to_i.years > self.join_on ? i[:extra_days]: 0)
end
end
# calculate the total vacation days that is used for in this calendar year
# @return [Integer] the total used vacation days
def total_used_vacation_days
total_used_vacation_hours / self.policy.work_hours_per_day
end
def total_used_vacation_hours
total_vacation_hours - total_remain_vacation_hours
end
# total_vacation_days - total_used_vacation_days
# @return [Integer] total left over vacation days
def total_remain_vacation_days
total_remain_vacation_hours / self.policy.work_hours_per_day
end
def total_remain_vacation_hours
TimeoffTask.where(user: self, sub_type: [:accrue, :adjustment, :vacation].map {|i| TimeoffTask.sub_types[i]}).sum(:total_hours)
end
def total_remain_personal_hours
TimeoffTask.total_personal_hours_for_user(self)
end
def total_remain_sick_hours
TimeoffTask.total_sick_hours_for_user(self)
end
# TODO:
# check if user is on vacation, depending on his vacation requests
# @return [Boolean] true or false
def on_vacation?
false
end
# TODO:
# check if user is sick
# @return [Boolean] true or false
def is_sick?
false
end
def self.policy_class
EmployeePolicy
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment