hr_lite 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +5 -2
- data/app/assets/javascripts/hr_lite/confirm.js +9 -5
- data/app/assets/javascripts/hr_lite/countdown.js +9 -2
- data/app/assets/javascripts/hr_lite/emoji_pick.js +4 -4
- data/app/assets/javascripts/hr_lite/search.js +1 -1
- data/app/assets/stylesheets/hr_lite/hr_lite.css +24 -14
- data/app/controllers/hr_lite/admin/attendances_controller.rb +12 -3
- data/app/controllers/hr_lite/admin/audit_logs_controller.rb +1 -0
- data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/documents_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/employees_controller.rb +11 -4
- data/app/controllers/hr_lite/admin/expenses_controller.rb +17 -1
- data/app/controllers/hr_lite/admin/hr_requests_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +5 -2
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +9 -5
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/reports_controller.rb +3 -2
- data/app/controllers/hr_lite/admin/salary_structures_controller.rb +2 -0
- data/app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +15 -4
- data/app/controllers/hr_lite/admin/work_locations_controller.rb +1 -1
- data/app/controllers/hr_lite/approvals_controller.rb +1 -0
- data/app/controllers/hr_lite/home_controller.rb +2 -11
- data/app/controllers/hr_lite/loans_controller.rb +1 -1
- data/app/controllers/hr_lite/regularization_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/tax_declarations_controller.rb +9 -5
- data/app/helpers/hr_lite/application_helper.rb +34 -9
- data/app/jobs/hr_lite/attendance_close_job.rb +1 -1
- data/app/jobs/hr_lite/payroll_compute_job.rb +12 -0
- data/app/models/concerns/hr_lite/audited.rb +3 -1
- data/app/models/concerns/hr_lite/encrypted_money.rb +8 -5
- data/app/models/hr_lite/attendance_record.rb +21 -4
- data/app/models/hr_lite/comp_off_request.rb +2 -0
- data/app/models/hr_lite/document.rb +1 -1
- data/app/models/hr_lite/expense.rb +3 -0
- data/app/models/hr_lite/hr_request.rb +2 -0
- data/app/models/hr_lite/kudo.rb +2 -2
- data/app/models/hr_lite/leave_request.rb +25 -6
- data/app/models/hr_lite/payroll_line_item.rb +8 -0
- data/app/models/hr_lite/payroll_run.rb +15 -6
- data/app/models/hr_lite/regularization_request.rb +2 -2
- data/app/models/hr_lite/resignation.rb +5 -1
- data/app/models/hr_lite/role.rb +5 -0
- data/app/models/hr_lite/salary_slip.rb +2 -1
- data/app/models/hr_lite/statutory_rate_card_record.rb +6 -0
- data/app/models/hr_lite/tax_declaration.rb +5 -0
- data/app/services/hr_lite/attendance_puncher.rb +4 -0
- data/app/services/hr_lite/overview_query.rb +0 -12
- data/app/services/hr_lite/work_status.rb +5 -2
- data/app/views/hr_lite/admin/assets/index.html.erb +9 -2
- data/app/views/hr_lite/admin/attendances/index.html.erb +4 -4
- data/app/views/hr_lite/admin/attendances/show.html.erb +4 -2
- data/app/views/hr_lite/admin/attendances/summary.html.erb +5 -2
- data/app/views/hr_lite/admin/audit_logs/index.html.erb +6 -4
- data/app/views/hr_lite/admin/benefits/index.html.erb +10 -4
- data/app/views/hr_lite/admin/ca_reviews/index.html.erb +3 -3
- data/app/views/hr_lite/admin/checklists/index.html.erb +3 -3
- data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +1 -2
- data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +6 -6
- data/app/views/hr_lite/admin/designation_changes/new.html.erb +4 -3
- data/app/views/hr_lite/admin/documents/index.html.erb +9 -4
- data/app/views/hr_lite/admin/documents/show.html.erb +1 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +12 -5
- data/app/views/hr_lite/admin/employees/show.html.erb +40 -33
- data/app/views/hr_lite/admin/expense_categories/index.html.erb +1 -1
- data/app/views/hr_lite/admin/expenses/index.html.erb +7 -4
- data/app/views/hr_lite/admin/holidays/index.html.erb +7 -5
- data/app/views/hr_lite/admin/hr_requests/index.html.erb +3 -3
- data/app/views/hr_lite/admin/hr_requests/show.html.erb +5 -4
- data/app/views/hr_lite/admin/leave_balances/index.html.erb +6 -4
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +1 -2
- data/app/views/hr_lite/admin/leave_requests/show.html.erb +6 -4
- data/app/views/hr_lite/admin/leave_types/edit.html.erb +4 -4
- data/app/views/hr_lite/admin/leave_types/index.html.erb +5 -5
- data/app/views/hr_lite/admin/loans/index.html.erb +1 -1
- data/app/views/hr_lite/admin/loans/new.html.erb +1 -1
- data/app/views/hr_lite/admin/loans/show.html.erb +8 -12
- data/app/views/hr_lite/admin/office_locations/edit.html.erb +4 -4
- data/app/views/hr_lite/admin/office_locations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/overview/index.html.erb +10 -31
- data/app/views/hr_lite/admin/payroll_line_items/new.html.erb +2 -1
- data/app/views/hr_lite/admin/payroll_runs/index.html.erb +3 -3
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +15 -10
- data/app/views/hr_lite/admin/performance_plans/_form.html.erb +2 -1
- data/app/views/hr_lite/admin/policies/index.html.erb +2 -2
- data/app/views/hr_lite/admin/policies/new.html.erb +4 -3
- data/app/views/hr_lite/admin/regularization_requests/index.html.erb +3 -4
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +5 -6
- data/app/views/hr_lite/admin/reports/index.html.erb +3 -6
- data/app/views/hr_lite/admin/reports/show.html.erb +16 -2
- data/app/views/hr_lite/admin/roles/_form.html.erb +8 -6
- data/app/views/hr_lite/admin/roles/edit.html.erb +4 -2
- data/app/views/hr_lite/admin/roles/index.html.erb +1 -1
- data/app/views/hr_lite/admin/salary_slips/show.html.erb +3 -2
- data/app/views/hr_lite/admin/salary_structures/_form.html.erb +1 -0
- data/app/views/hr_lite/admin/settings/edit.html.erb +5 -1
- data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +14 -10
- data/app/views/hr_lite/admin/statutory_rate_cards/index.html.erb +3 -1
- data/app/views/hr_lite/admin/tax_declarations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/tax_declarations/show.html.erb +8 -4
- data/app/views/hr_lite/admin/wfh_requests/index.html.erb +17 -16
- data/app/views/hr_lite/admin/wfh_requests/show.html.erb +3 -5
- data/app/views/hr_lite/admin/work_locations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/work_modes/index.html.erb +1 -1
- data/app/views/hr_lite/approvals/index.html.erb +5 -4
- data/app/views/hr_lite/attendance/_custom_status.html.erb +7 -9
- data/app/views/hr_lite/attendance/_month_grid.html.erb +9 -9
- data/app/views/hr_lite/attendance/_penalties.html.erb +2 -2
- data/app/views/hr_lite/attendance/_punch_card.html.erb +10 -4
- data/app/views/hr_lite/attendance/_slot.html.erb +9 -6
- data/app/views/hr_lite/attendance/_trail.html.erb +1 -1
- data/app/views/hr_lite/attendance/_work_mode.html.erb +1 -1
- data/app/views/hr_lite/attendance/show.html.erb +7 -5
- data/app/views/hr_lite/calendar/show.html.erb +2 -2
- data/app/views/hr_lite/comp_off_requests/index.html.erb +1 -1
- data/app/views/hr_lite/comp_off_requests/new.html.erb +1 -1
- data/app/views/hr_lite/documents/_upload_form.html.erb +1 -1
- data/app/views/hr_lite/employee_profiles/show.html.erb +4 -2
- data/app/views/hr_lite/home/index.html.erb +6 -5
- data/app/views/hr_lite/id_cards/show.html.erb +5 -1
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +3 -2
- data/app/views/hr_lite/leave_requests/new.html.erb +17 -8
- data/app/views/hr_lite/leave_requests/show.html.erb +2 -0
- data/app/views/hr_lite/loans/index.html.erb +1 -1
- data/app/views/hr_lite/org/show.html.erb +1 -1
- data/app/views/hr_lite/regularization_requests/index.html.erb +7 -7
- data/app/views/hr_lite/regularization_requests/new.html.erb +7 -5
- data/app/views/hr_lite/resignations/show.html.erb +2 -1
- data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +7 -7
- data/app/views/hr_lite/salary_slips/pdf.html.erb +2 -1
- data/app/views/hr_lite/tax/_computation.html.erb +1 -1
- data/app/views/hr_lite/tax_declarations/show.html.erb +4 -4
- data/app/views/hr_lite/team/show.html.erb +8 -7
- data/app/views/hr_lite/timelines/show.html.erb +1 -0
- data/app/views/hr_lite/wfh_requests/index.html.erb +3 -3
- data/app/views/hr_lite/work_time/_bars.html.erb +5 -4
- data/app/views/hr_lite/work_time/_donut.html.erb +1 -1
- data/app/views/hr_lite/work_time/_report.html.erb +14 -8
- data/app/views/layouts/hr_lite/application.html.erb +1 -0
- data/app/views/layouts/hr_lite/mailer.html.erb +10 -2
- data/app/views/layouts/hr_lite/mailer.text.erb +4 -1
- data/config/routes.rb +1 -1
- data/lib/hr_lite/amount_in_words.rb +7 -5
- data/lib/hr_lite/notifications.rb +5 -2
- data/lib/hr_lite/statutory_seeds.rb +1 -1
- data/lib/hr_lite/version.rb +1 -1
- metadata +2 -1
|
@@ -52,7 +52,8 @@ module HrLite
|
|
|
52
52
|
def next_financial_year(newest)
|
|
53
53
|
return FinancialYear.start_for(Date.current) if newest.nil?
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
# A mid-year card (1 Oct) must not push the next one to 1 Oct: years start on 1 April.
|
|
56
|
+
[ FinancialYear.start_for(newest.effective_from.next_year), FinancialYear.start_for(Date.current) ].max
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# The figures arrive as flat strings from number fields and are stored
|
|
@@ -11,7 +11,7 @@ module HrLite
|
|
|
11
11
|
@status = params[:status].presence_in(TaxDeclaration::STATUSES) || "submitted"
|
|
12
12
|
@declarations = paginate(
|
|
13
13
|
hr_scope(TaxDeclaration.includes(:user, :tax_declaration_items), "tax.view")
|
|
14
|
-
.where(status: @status).order(created_at: :desc)
|
|
14
|
+
.where(status: @status).where.not(user_id: hr_current_user.id).order(created_at: :desc)
|
|
15
15
|
)
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -45,7 +45,7 @@ module HrLite
|
|
|
45
45
|
|
|
46
46
|
AuditLog.record!(action: "tax_proof.downloaded", subject: declaration, actor: hr_current_user,
|
|
47
47
|
changes: { "file" => file.filename.to_s, "owner" => HrLite.display_name(declaration.user) })
|
|
48
|
-
redirect_to Rails.application.routes.url_helpers.rails_blob_path(file, disposition: "attachment", only_path: true),
|
|
48
|
+
redirect_to Rails.application.routes.url_helpers.rails_blob_path(file, disposition: "attachment", only_path: true, expires_in: 5.minutes),
|
|
49
49
|
allow_other_host: false
|
|
50
50
|
end
|
|
51
51
|
|
|
@@ -57,6 +57,8 @@ module HrLite
|
|
|
57
57
|
declaration.tax_declaration_items.each do |item|
|
|
58
58
|
item.update!(verified_amount: item.declared_amount) if item.verified_amount.nil?
|
|
59
59
|
end
|
|
60
|
+
AuditLog.record!(action: "tax_declaration.accepted_all", subject: declaration, actor: hr_current_user,
|
|
61
|
+
changes: { "owner" => HrLite.display_name(declaration.user) })
|
|
60
62
|
declaration.verify!(actor: hr_current_user, note: "Proof accepted")
|
|
61
63
|
end
|
|
62
64
|
redirect_to admin_tax_declarations_path, notice: "Accepted and verified — payroll uses these amounts now."
|
|
@@ -66,9 +68,17 @@ module HrLite
|
|
|
66
68
|
|
|
67
69
|
def update
|
|
68
70
|
@declaration = find_manageable
|
|
71
|
+
return redirect_to admin_tax_declaration_path(@declaration), alert: "Only a submitted declaration can be checked." unless @declaration.submitted?
|
|
72
|
+
|
|
69
73
|
# Recording what the proof supported is a separate act from deciding
|
|
70
74
|
# — an amount can be corrected while the decision is still pending.
|
|
71
|
-
|
|
75
|
+
recorded = TaxDeclaration.transaction do
|
|
76
|
+
@declaration.update(verification_params) &&
|
|
77
|
+
AuditLog.record!(action: "tax_declaration.amounts_recorded", subject: @declaration, actor: hr_current_user,
|
|
78
|
+
changes: { "owner" => HrLite.display_name(@declaration.user) })
|
|
79
|
+
end
|
|
80
|
+
if recorded
|
|
81
|
+
return verify if params[:verify] # "Record and verify": the typed amounts are saved first
|
|
72
82
|
redirect_to admin_tax_declaration_path(@declaration), notice: "Amounts recorded."
|
|
73
83
|
else
|
|
74
84
|
@tax = TaxComputation.new(user: @declaration.user, declaration: @declaration)
|
|
@@ -81,7 +91,8 @@ module HrLite
|
|
|
81
91
|
declaration.verify!(actor: hr_current_user, note: params[:note])
|
|
82
92
|
redirect_to admin_tax_declarations_path, notice: "Verified — payroll will use the checked amounts."
|
|
83
93
|
rescue ActiveRecord::RecordInvalid
|
|
84
|
-
redirect_to admin_tax_declaration_path(declaration),
|
|
94
|
+
redirect_to admin_tax_declaration_path(declaration),
|
|
95
|
+
alert: declaration.errors.full_messages.first || "Only a submitted declaration can be verified."
|
|
85
96
|
end
|
|
86
97
|
|
|
87
98
|
def reject
|
|
@@ -5,7 +5,7 @@ module HrLite
|
|
|
5
5
|
# Coordinates are only shown here, inside attendance.view reach.
|
|
6
6
|
class WorkLocationsController < BaseController
|
|
7
7
|
def index
|
|
8
|
-
@status = params[:status].presence_in(WorkLocation::STATUSES) || "
|
|
8
|
+
@status = params[:status].presence_in(WorkLocation::STATUSES) || "pending"
|
|
9
9
|
@locations = paginate(hr_scope(WorkLocation.where(status: @status), "attendance.view")
|
|
10
10
|
.includes(:user).order(created_at: :desc))
|
|
11
11
|
end
|
|
@@ -9,6 +9,7 @@ module HrLite
|
|
|
9
9
|
class ApprovalsController < ApplicationController
|
|
10
10
|
def index
|
|
11
11
|
@approvals = paginate(Approval.pending_for(hr_current_user).includes(:step, :subject).order(:created_at))
|
|
12
|
+
@in_flow = Approval.pending_for(hr_current_user).pluck(:subject_type, :subject_id) # their queue rows would repeat them
|
|
12
13
|
@delegations = ApprovalDelegation.live_on(Date.current)
|
|
13
14
|
.where(from_user_id: hr_current_user.id)
|
|
14
15
|
.includes(:to_user)
|
|
@@ -3,27 +3,18 @@ module HrLite
|
|
|
3
3
|
def index
|
|
4
4
|
@latest_kudos = Kudo.recent.includes(:giver, kudo_mentions: :user).limit(3)
|
|
5
5
|
@awards = Award.current
|
|
6
|
-
@out_week = LeaveRequest.approved.overlapping_range(Date.current
|
|
6
|
+
@out_week = LeaveRequest.approved.overlapping_range(Date.current, Date.current.end_of_week)
|
|
7
7
|
.includes(:user).where(user_id: HrLite.active_employees.map(&:id))
|
|
8
8
|
.sort_by { |leave| hr_display_name(leave.user).downcase }
|
|
9
9
|
@attention = attention_days
|
|
10
10
|
@waiting = Approval.pending_for(hr_current_user).count
|
|
11
|
-
@
|
|
12
|
-
@mine_pending = [ LeaveRequest.includes(:leave_type), RegularizationRequest, CompOffRequest ]
|
|
11
|
+
@mine_pending = [ LeaveRequest.includes(:leave_type), RegularizationRequest, CompOffRequest, WfhRequest ]
|
|
13
12
|
.flat_map { |scope| scope.pending.where(user_id: hr_current_user.id).order(created_at: :desc).limit(5) }
|
|
14
13
|
.max_by(5, &:created_at)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
private
|
|
18
17
|
|
|
19
|
-
# Requests with no routed flow are decided straight off the HR queues, scoped as those queues decide them.
|
|
20
|
-
def queue_count
|
|
21
|
-
leave = ApprovalFlow.for(LeaveRequest.name) ? 0 : decidable(LeaveRequest, "leave.approve")
|
|
22
|
-
leave + decidable(CompOffRequest, "leave.approve") + decidable(RegularizationRequest, "attendance.manage")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def decidable(model, key) = hr_scope(model.pending, key).where.not(user_id: hr_current_user.id).count
|
|
26
|
-
|
|
27
18
|
# Last week's days that still need a fix (absent, no check-out, flagged, short), minus those with a pending ticket.
|
|
28
19
|
def attention_days
|
|
29
20
|
range = (Date.current - 7)..(Date.current - 1)
|
|
@@ -4,7 +4,7 @@ module HrLite
|
|
|
4
4
|
class LoansController < ApplicationController
|
|
5
5
|
def index
|
|
6
6
|
@loans = Loan.where(user_id: hr_current_user.id).order(starts_on: :desc)
|
|
7
|
-
@outstanding = @loans.sum(BigDecimal(0), &:outstanding)
|
|
7
|
+
@outstanding = @loans.active.sum(BigDecimal(0), &:outstanding)
|
|
8
8
|
@next_month = Date.current.next_month.beginning_of_month
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -22,19 +22,19 @@ module HrLite
|
|
|
22
22
|
def cancel
|
|
23
23
|
request = RegularizationRequest.where(user_id: hr_current_user.id).find(params[:id])
|
|
24
24
|
request.cancel!(actor: hr_current_user)
|
|
25
|
-
redirect_to regularization_requests_path, notice: "
|
|
25
|
+
redirect_to regularization_requests_path, notice: "Day fix cancelled."
|
|
26
26
|
rescue ActiveRecord::RecordInvalid
|
|
27
|
-
redirect_to regularization_requests_path, alert: "Only pending
|
|
27
|
+
redirect_to regularization_requests_path, alert: "Only pending day fixes can be cancelled."
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def create_notice
|
|
33
33
|
rule = HrLite.config.self_regularization
|
|
34
|
-
return "
|
|
34
|
+
return "Sent for approval — your manager or HR will review it." unless rule
|
|
35
35
|
return "Fixed. #{helpers.pluralize(@request.self_fixes_left, 'self-fix')} left this week." if @request.approved?
|
|
36
36
|
|
|
37
|
-
"Sent
|
|
37
|
+
"Sent for approval — you can fix only a missed punch or short day on a working day in the last #{rule[:within_days]} days yourself, " \
|
|
38
38
|
"#{rule[:per_week]} times a week."
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -28,10 +28,11 @@ module HrLite
|
|
|
28
28
|
TaxDeclaration.new(user_id: hr_current_user.id,
|
|
29
29
|
financial_year: financial_year, regime: employee_regime)
|
|
30
30
|
# A verified declaration is what payroll is deducting against; changing
|
|
31
|
-
# it would silently re-open a decision HR has already made.
|
|
32
|
-
|
|
31
|
+
# it would silently re-open a decision HR has already made. A submitted
|
|
32
|
+
# one is what HR is checking, so it cannot shift under them either.
|
|
33
|
+
if @declaration.verified? || @declaration.submitted?
|
|
33
34
|
return redirect_to tax_declaration_path,
|
|
34
|
-
alert: "This year's declaration is
|
|
35
|
+
alert: "This year's declaration is #{@declaration.status}. Ask HR to reopen it."
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
if @declaration.update(declaration_params)
|
|
@@ -59,10 +60,13 @@ module HrLite
|
|
|
59
60
|
|
|
60
61
|
private
|
|
61
62
|
|
|
63
|
+
# This year or last (late proof); any other year falls back to this one.
|
|
62
64
|
def financial_year
|
|
63
|
-
FinancialYear.start_for(
|
|
65
|
+
current = FinancialYear.start_for(Date.current)
|
|
66
|
+
asked = FinancialYear.start_for(Date.parse(params[:financial_year].to_s))
|
|
67
|
+
asked.between?(current.prev_year, current) ? asked : current
|
|
64
68
|
rescue Date::Error
|
|
65
|
-
|
|
69
|
+
current
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def current_declaration
|
|
@@ -41,14 +41,16 @@ module HrLite
|
|
|
41
41
|
queue: -> { hrl_queue(HrLite::LeaveRequest.where(status: "pending"), "leave.approve") } },
|
|
42
42
|
{ label: "Comp-off", path: :admin_comp_off_requests_path, match: [ "/admin/comp_off_requests" ], if: OPS,
|
|
43
43
|
queue: -> { hrl_queue(HrLite::CompOffRequest.pending, "leave.approve") } },
|
|
44
|
-
{ label: "
|
|
44
|
+
{ label: "Day fixes", path: :admin_regularization_requests_path, match: [ "/admin/regularization_requests" ], if: OPS,
|
|
45
45
|
queue: -> { hrl_queue(HrLite::RegularizationRequest.pending, "attendance.manage") } },
|
|
46
46
|
{ label: "WFH", path: :admin_wfh_requests_path, match: [ "/admin/wfh_requests" ], if: OPS,
|
|
47
|
-
queue: -> { hrl_queue(HrLite::WfhRequest.pending, "attendance.manage") } },
|
|
47
|
+
queue: -> { hrl_queue(HrLite::WfhRequest.pending.overlapping(Date.current, Date.current + HrLite::WfhRequest::MAX_DAYS_AHEAD), "attendance.manage") } },
|
|
48
48
|
{ label: "Home locations", path: :admin_work_locations_path, match: [ "/admin/work_locations" ], if: OPS,
|
|
49
49
|
queue: -> { hrl_queue(HrLite::WorkLocation.pending, "attendance.manage") } },
|
|
50
50
|
{ label: "Claims", path: :admin_expenses_path, match: [ "/admin/expenses" ], if: -> { hr_can?("expense.approve", scope: :team) },
|
|
51
51
|
queue: -> { hrl_queue(HrLite::Expense.where(status: "submitted"), "expense.approve") } },
|
|
52
|
+
{ label: "Resignations", path: :admin_employees_path, match: [ "/admin/resignations" ], if: LEAD,
|
|
53
|
+
queue: -> { hrl_queue(HrLite::Resignation.pending, "profile.manage") } },
|
|
52
54
|
{ label: "Documents to verify", path: :admin_documents_path, match: [ "/admin/documents" ], if: LEAD,
|
|
53
55
|
queue: -> { hrl_queue(HrLite::Document.where(verification: "pending"), "document.view") } },
|
|
54
56
|
{ label: "CA review", path: :admin_ca_reviews_path, match: [ "/admin/ca_reviews" ],
|
|
@@ -69,11 +71,11 @@ module HrLite
|
|
|
69
71
|
{ label: "Team benefits", path: :admin_benefits_path, match: [ "/admin/benefits" ], if: MANAGE }
|
|
70
72
|
] },
|
|
71
73
|
{ label: "People", match: [ "/people" ], tabs: [
|
|
72
|
-
{ label: "
|
|
74
|
+
{ label: "Team board", path: :team_path, match: [ "/team" ] },
|
|
73
75
|
{ label: "Org chart", path: :org_path, match: [ "/org" ] },
|
|
74
76
|
{ label: "Kudos", path: :kudos_path, match: [ "/kudos" ] },
|
|
75
77
|
{ label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ], if: LEAD },
|
|
76
|
-
{ label: "Joining & exits", path: :admin_checklists_path, match: [ "/admin/checklists" ], if:
|
|
78
|
+
{ label: "Joining & exits", path: :admin_checklists_path, match: [ "/admin/checklists" ], if: -> { hr_can?("checklist.manage", scope: :all) } },
|
|
77
79
|
{ label: "Awards", path: :admin_awards_path, match: [ "/admin/awards" ], if: LEAD },
|
|
78
80
|
{ label: "PIPs", path: :admin_performance_plans_path, match: [ "/admin/performance_plans" ], if: SUPER }
|
|
79
81
|
] },
|
|
@@ -87,15 +89,16 @@ module HrLite
|
|
|
87
89
|
{ label: "Help", tabs: [
|
|
88
90
|
{ label: "Policies", path: :policies_path, match: [ "/policies" ], if: -> { hr_can?("policy.view", scope: :all) } },
|
|
89
91
|
{ label: "Ask HR", path: :hr_requests_path, match: [ "/hr_requests" ], if: -> { hr_can?("hr_request.raise") } },
|
|
90
|
-
{ label: "Help desk", path: :admin_hr_requests_path, match: [ "/admin/hr_requests" ], if:
|
|
92
|
+
{ label: "Help desk", path: :admin_hr_requests_path, match: [ "/admin/hr_requests" ], if: -> { hr_can?("hr_request.manage", scope: :all) } },
|
|
91
93
|
{ label: "Manage policies", path: :admin_policies_path, match: [ "/admin/policies" ], if: -> { hr_can?("policy.manage", scope: :all) } }
|
|
92
94
|
] },
|
|
93
95
|
{ label: "Admin", divider: true, tabs: [
|
|
94
|
-
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/
|
|
96
|
+
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/expense_categories" ], if: LEAD },
|
|
97
|
+
{ label: "HR settings", path: :edit_admin_setting_path, match: [ "/admin/setting" ], if: LEAD },
|
|
95
98
|
{ label: "Roles", path: :admin_roles_path, match: [ "/admin/roles" ], if: -> { hr_can?("role.manage", scope: :all) } },
|
|
96
99
|
{ label: "Statutory rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ], if: SUPER },
|
|
97
100
|
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ], if: -> { hr_can?("expense.approve", scope: :all) || %w[leave.view attendance.view profile.view].any? { |key| hr_can?(key, scope: :team) } } },
|
|
98
|
-
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ], if:
|
|
101
|
+
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ], if: -> { hr_can?("asset.manage", scope: :all) } },
|
|
99
102
|
{ label: "Audit trail", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ], if: LEAD }
|
|
100
103
|
] }
|
|
101
104
|
].freeze
|
|
@@ -151,6 +154,28 @@ module HrLite
|
|
|
151
154
|
# What a queue page lets this viewer decide: their reach, never their own requests.
|
|
152
155
|
def hrl_queue(relation, key) = hr_access.scope_relation(relation, key).where.not(user_id: hr_current_user.id)
|
|
153
156
|
|
|
157
|
+
# The Inbox badge's number; Home and the overview board show the same one.
|
|
158
|
+
def hrl_inbox_count
|
|
159
|
+
inbox = hrl_sections.find { |s| s[:label] == "Inbox" }
|
|
160
|
+
inbox ? hrl_waiting(inbox).to_i : 0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# One queued request in words, and where it is decided.
|
|
164
|
+
def hrl_inbox_item(tab, record)
|
|
165
|
+
case record
|
|
166
|
+
when HrLite::LeaveRequest
|
|
167
|
+
[ "#{record.leave_type.name}, #{record.date_range_label} · #{pluralize(hrl_days(record.days_count), 'day')}", hr_lite.admin_leave_request_path(record) ]
|
|
168
|
+
when HrLite::CompOffRequest then [ "Comp-off for #{record.date_worked.strftime('%a %-d %b')}", hr_lite.admin_comp_off_request_path(record) ]
|
|
169
|
+
when HrLite::RegularizationRequest then [ "Day fix, #{record.date.strftime('%a %-d %b')}", hr_lite.admin_regularization_request_path(record) ]
|
|
170
|
+
when HrLite::WfhRequest then [ "WFH, #{record.dates_label}", hr_lite.admin_wfh_request_path(record) ]
|
|
171
|
+
when HrLite::Expense then [ "Claim: #{record.category.name}, #{hrl_money(record.amount)}, #{record.spent_on.strftime('%a %-d %b')}", hr_lite.admin_expenses_path ]
|
|
172
|
+
when HrLite::Resignation
|
|
173
|
+
profile = HrLite::EmployeeProfile.find_by(user_id: record.user_id)
|
|
174
|
+
[ "Resignation, last day #{record.proposed_last_day.strftime('%-d %b %Y')}", profile ? hr_lite.admin_employee_path(profile) : hr_lite.admin_employees_path ]
|
|
175
|
+
else [ tab[:label], hr_lite.public_send(tab[:path]) ]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
154
179
|
# Everything waiting in this viewer's queues, oldest first, as [tab, record].
|
|
155
180
|
def hrl_inbox_rows(limit = 50)
|
|
156
181
|
inbox = hrl_sections.find { |s| s[:label] == "Inbox" } or return []
|
|
@@ -281,8 +306,8 @@ module HrLite
|
|
|
281
306
|
# Status pill for a request lifecycle string — one colour map for every
|
|
282
307
|
# list and show screen.
|
|
283
308
|
def hrl_request_status_badge(status)
|
|
284
|
-
css = { "approved" => "hrl-badge--ok", "
|
|
285
|
-
"cancelled" => "hrl-badge--muted" }[status]
|
|
309
|
+
css = { "approved" => "hrl-badge--ok", "reimbursed" => "hrl-badge--ok", "verified" => "hrl-badge--ok",
|
|
310
|
+
"rejected" => "hrl-badge--bad", "cancelled" => "hrl-badge--muted", "draft" => "hrl-badge--muted" }[status]
|
|
286
311
|
content_tag(:span, status.humanize, class: "hrl-badge #{css}")
|
|
287
312
|
end
|
|
288
313
|
|
|
@@ -58,7 +58,7 @@ module HrLite
|
|
|
58
58
|
"Fix it yourself by #{(date + rule[:within_days]).strftime('%d %b')} (up to #{rule[:per_week]} a week), " \
|
|
59
59
|
"or raise a ticket for HR."
|
|
60
60
|
else
|
|
61
|
-
"
|
|
61
|
+
"Use Fix a day to get it fixed."
|
|
62
62
|
end
|
|
63
63
|
Notifications.publish(event, title: title, body: body, path: "/regularization_requests/new?date=#{date.iso8601}",
|
|
64
64
|
bell_to: [ user ], email_to: [ user ])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Compute off the request (PayrollRun#compute_later!). A failure restores
|
|
3
|
+
# the run's status and lands as a run warning instead of a 500.
|
|
4
|
+
class PayrollComputeJob < ApplicationJob
|
|
5
|
+
def perform(run, from, actor)
|
|
6
|
+
run.compute!(actor: actor, from: from)
|
|
7
|
+
rescue => e
|
|
8
|
+
Rails.error.report(e)
|
|
9
|
+
run.reload.update_columns(warnings: run.warnings | [ "Compute failed (#{e.message.truncate(150)}) — nothing changed; try again." ]) # rubocop:disable Rails/SkipsModelValidations
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -8,6 +8,8 @@ module HrLite
|
|
|
8
8
|
|
|
9
9
|
SKIPPED_ATTRIBUTES = %w[updated_at created_at].freeze
|
|
10
10
|
REDACTED = "[changed]".freeze
|
|
11
|
+
# Personal text stored in the clear: the trail says it changed, never to what.
|
|
12
|
+
PII_COLUMNS = { "HrLite::EmployeeProfile" => %w[date_of_birth], "HrLite::HrRequest" => %w[subject body] }.freeze
|
|
11
13
|
|
|
12
14
|
# *_commit, not *_create/_update/_destroy: these callbacks write an audit
|
|
13
15
|
# row and email leadership a diff. Fired inside the transaction they
|
|
@@ -40,7 +42,7 @@ module HrLite
|
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def hr_lite_audited_changes(action)
|
|
43
|
-
encrypted = self.class.try(:encrypted_attributes)&.map(&:to_s) || []
|
|
45
|
+
encrypted = (self.class.try(:encrypted_attributes)&.map(&:to_s) || []) + PII_COLUMNS.fetch(self.class.name, [])
|
|
44
46
|
|
|
45
47
|
case action
|
|
46
48
|
when "destroy"
|
|
@@ -8,6 +8,10 @@ module HrLite
|
|
|
8
8
|
module EncryptedMoney
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
|
+
included do
|
|
12
|
+
validate { @unreadable_money&.each { |attribute| errors.add(attribute, "is not a number") } }
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
class_methods do
|
|
12
16
|
def encrypted_money(*attributes)
|
|
13
17
|
attributes.each do |attribute|
|
|
@@ -19,11 +23,10 @@ module HrLite
|
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
define_method("#{attribute}=") do |value|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
26
|
+
digits = value.to_s.gsub(/[,₹\s]/, "") # "1,50,000" and "₹ 1200" are how people type money
|
|
27
|
+
super(digits.empty? ? nil : BigDecimal(digits).round(2).to_s("F"))
|
|
28
|
+
rescue ArgumentError
|
|
29
|
+
(@unreadable_money ||= []) << attribute
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
end
|
|
@@ -35,6 +35,7 @@ module HrLite
|
|
|
35
35
|
validates :status, inclusion: { in: STATUSES }
|
|
36
36
|
validates :half_day_part, inclusion: { in: %w[first second] }, allow_blank: true
|
|
37
37
|
validate :check_out_after_check_in
|
|
38
|
+
validate :times_on_the_day, if: -> { will_save_change_to_date? || will_save_change_to_check_in_at? || will_save_change_to_check_out_at? }
|
|
38
39
|
|
|
39
40
|
scope :for_date, ->(date) { where(date: date) }
|
|
40
41
|
scope :for_month, ->(month) { where(date: month.beginning_of_month..month.end_of_month) }
|
|
@@ -57,9 +58,11 @@ module HrLite
|
|
|
57
58
|
EmployeeProfile.day_minutes_for(user_id, on: date) unless HrLite.config.work_hours.nil? || date.saturday?
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
# Hours worked, not the clock: an early start counts.
|
|
61
|
+
# Hours worked, not the clock: an early start counts. The day's penalty
|
|
62
|
+
# minutes count too, the same rule as required_check_out_at, so the card
|
|
63
|
+
# never says "day done" above "short by 20m".
|
|
61
64
|
def short?
|
|
62
|
-
worked_under?(required_minutes)
|
|
65
|
+
worked_under?(required_minutes && required_minutes + penalty_minutes)
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def below_half_day?
|
|
@@ -70,10 +73,11 @@ module HrLite
|
|
|
70
73
|
def required_check_out_at
|
|
71
74
|
return unless HrLite.config.attendance_slots && check_in_at
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
check_in_at + (EmployeeProfile.day_minutes_for(user_id, on: date) + extra).minutes
|
|
76
|
+
check_in_at + (EmployeeProfile.day_minutes_for(user_id, on: date) + penalty_minutes).minutes
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
def penalty_minutes = AttendancePenalty.active.where(user_id: user_id, date: date).sum(:minutes)
|
|
80
|
+
|
|
77
81
|
def worked_under?(minutes) = !!(minutes && worked_duration && worked_duration < minutes.minutes)
|
|
78
82
|
|
|
79
83
|
# Check in again after checking out: once a day, inside the hours HR sets in
|
|
@@ -115,5 +119,18 @@ module HrLite
|
|
|
115
119
|
|
|
116
120
|
errors.add(:check_out_at, "must be after check-in")
|
|
117
121
|
end
|
|
122
|
+
|
|
123
|
+
# No future days; check-out may run into the next morning (night shift).
|
|
124
|
+
# Only the fields being changed, in the company's zone, so re-saving an old row never fails.
|
|
125
|
+
def times_on_the_day
|
|
126
|
+
return unless date
|
|
127
|
+
|
|
128
|
+
day = ->(time) { time.in_time_zone(HrLite.config.time_zone).to_date }
|
|
129
|
+
errors.add(:date, "can't be in the future") if will_save_change_to_date? && date > day.(Time.current)
|
|
130
|
+
errors.add(:check_in_at, "must be on #{date.strftime('%d %b')}") if will_save_change_to_check_in_at? && check_in_at && day.(check_in_at) != date
|
|
131
|
+
if will_save_change_to_check_out_at? && check_out_at && !(date..date + 1).cover?(day.(check_out_at))
|
|
132
|
+
errors.add(:check_out_at, "must be on #{date.strftime('%d %b')} or the next morning")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
118
135
|
end
|
|
119
136
|
end
|
|
@@ -50,6 +50,8 @@ module HrLite
|
|
|
50
50
|
# (A duplicate live request for the same date cannot exist — the
|
|
51
51
|
# partial unique index on [user_id, date_worked] guarantees it.)
|
|
52
52
|
credit_balance!(type)
|
|
53
|
+
AuditLog.record!(action: "comp_off.approved", subject: self, actor: actor,
|
|
54
|
+
changes: { "date_worked" => date_worked.to_s, "days" => credit_days.to_s("F") })
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
Notifications.publish(
|
|
@@ -79,7 +79,7 @@ module HrLite
|
|
|
79
79
|
def default_visibility_from_category
|
|
80
80
|
return if visibility.present?
|
|
81
81
|
|
|
82
|
-
self.visibility = SENSITIVE_CATEGORIES.include?(category.to_s) ? "money" : "hr"
|
|
82
|
+
self.visibility = SENSITIVE_CATEGORIES.include?(category.to_s.downcase.strip) ? "money" : "hr"
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def file_is_an_allowed_kind
|
|
@@ -39,6 +39,7 @@ module HrLite
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def approve!(actor:, note: nil)
|
|
42
|
+
raise ActiveRecord::RecordInvalid.new(self), "not submitted" unless submitted?
|
|
42
43
|
return record_routed!(actor, note, :approved) if awaiting?(actor)
|
|
43
44
|
|
|
44
45
|
settle!("approved", actor, note)
|
|
@@ -46,6 +47,7 @@ module HrLite
|
|
|
46
47
|
|
|
47
48
|
def reject!(actor:, note:)
|
|
48
49
|
raise ArgumentError, "a rejection needs a reason" if note.blank?
|
|
50
|
+
raise ActiveRecord::RecordInvalid.new(self), "not submitted" unless submitted?
|
|
49
51
|
return record_routed!(actor, note, :rejected) if awaiting?(actor)
|
|
50
52
|
|
|
51
53
|
settle!("rejected", actor, note)
|
|
@@ -90,6 +92,7 @@ module HrLite
|
|
|
90
92
|
|
|
91
93
|
def settle!(new_status, actor, note)
|
|
92
94
|
update!(status: new_status, decision_note: note.presence)
|
|
95
|
+
approval_route.cancel_all! # decided outside the flow: nobody is left waiting to be asked
|
|
93
96
|
Notifications.publish(
|
|
94
97
|
new_status == "approved" ? "expense.approved" : "expense.rejected",
|
|
95
98
|
title: "Your #{category.name} claim was #{new_status}",
|
|
@@ -30,6 +30,7 @@ module HrLite
|
|
|
30
30
|
def category_label = self.class.category_label(category)
|
|
31
31
|
|
|
32
32
|
def assign!(actor:, assignee:)
|
|
33
|
+
raise ActiveRecord::RecordInvalid.new(self), "already settled" unless open? || in_progress?
|
|
33
34
|
update!(assigned_to_id: assignee.id, status: "in_progress")
|
|
34
35
|
Notifications.publish(
|
|
35
36
|
"hr_request.assigned",
|
|
@@ -41,6 +42,7 @@ module HrLite
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def resolve!(actor:, resolution:)
|
|
45
|
+
raise ActiveRecord::RecordInvalid.new(self), "already settled" unless open? || in_progress?
|
|
44
46
|
raise ArgumentError, "a resolution needs an answer" if resolution.blank?
|
|
45
47
|
|
|
46
48
|
update!(status: "resolved", resolution: resolution, resolved_at: Time.current,
|
data/app/models/hr_lite/kudo.rb
CHANGED
|
@@ -33,10 +33,10 @@ module HrLite
|
|
|
33
33
|
BADGES[badge]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Creates mention rows from the message markers (
|
|
36
|
+
# Creates mention rows from the message markers (employees only,
|
|
37
37
|
# giver excluded) and notifies them. Called once, right after create.
|
|
38
38
|
def register_mentions!
|
|
39
|
-
ids = HrLite::MentionParser.user_ids(message) - [ giver_id ]
|
|
39
|
+
ids = (HrLite::MentionParser.user_ids(message) - [ giver_id ]) & HrLite.employees.map(&:id)
|
|
40
40
|
users = HrLite.user_klass.where(id: ids)
|
|
41
41
|
users.each { |u| kudo_mentions.create!(user_id: u.id) }
|
|
42
42
|
|
|
@@ -5,12 +5,15 @@ module HrLite
|
|
|
5
5
|
STATUSES = %w[pending approved rejected cancelled].freeze
|
|
6
6
|
|
|
7
7
|
belongs_to :user, class_name: HrLite.config.user_class
|
|
8
|
-
belongs_to :leave_type
|
|
8
|
+
belongs_to :leave_type, optional: true # required below, in words ("must exist" is fixed)
|
|
9
9
|
belongs_to :decided_by, class_name: HrLite.config.user_class, optional: true
|
|
10
10
|
# Set when HR records leave the employee forgot to apply for.
|
|
11
11
|
belongs_to :created_by, class_name: HrLite.config.user_class, optional: true
|
|
12
12
|
|
|
13
|
-
validates :
|
|
13
|
+
validates :leave_type, presence: { message: "is missing — pick one" }
|
|
14
|
+
validates :start_date, presence: { message: "is missing — pick the first day" }
|
|
15
|
+
# With no start the start error says it all (a blank end means one day).
|
|
16
|
+
validates :end_date, presence: { message: "is missing — pick the last day" }, if: :start_date
|
|
14
17
|
validates :status, inclusion: { in: STATUSES }
|
|
15
18
|
validates :half_day_part, inclusion: { in: %w[first second] }, allow_blank: true
|
|
16
19
|
|
|
@@ -50,7 +53,10 @@ module HrLite
|
|
|
50
53
|
return 0 if leave_type.unlimited?
|
|
51
54
|
return paid_days ? LeaveDayCounter.count(self) - paid_days : 0 if approved?
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
# Pending requests of this type and leave year filed first draw on the same balance (warning only).
|
|
57
|
+
queued = self.class.pending.where(user_id: user_id, leave_type_id: leave_type_id, id: ...id,
|
|
58
|
+
start_date: LeaveYear.range(LeaveYear.key_for(start_date))).sum(:days_count)
|
|
59
|
+
[ LeaveDayCounter.count(self) - covered_by(balance.available(as_of: start_date) - queued), 0 ].max
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
# Unpaid share (0, 0.5 or 1) of `date`: the balance covered the leave's
|
|
@@ -113,6 +119,7 @@ module HrLite
|
|
|
113
119
|
# And nobody should still be asked to decide a request that has been
|
|
114
120
|
# called off — it would sit in their inbox for ever.
|
|
115
121
|
approval_route.cancel_all!
|
|
122
|
+
flag_computed_runs! if was_approved
|
|
116
123
|
end
|
|
117
124
|
|
|
118
125
|
Notifications.publish(
|
|
@@ -229,6 +236,15 @@ module HrLite
|
|
|
229
236
|
self.decision_note = note
|
|
230
237
|
save!
|
|
231
238
|
audit!(new_status, actor, note)
|
|
239
|
+
approval_route.cancel_all! # decided outside the flow too: nobody else is still asked
|
|
240
|
+
flag_computed_runs! if approved?
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# A slip computed before this still pays the old days; a recompute clears the note.
|
|
245
|
+
def flag_computed_runs!
|
|
246
|
+
PayrollRun.where(status: "review", period_month: start_date.beginning_of_month..end_date).find_each do |run|
|
|
247
|
+
run.update!(warnings: run.warnings + [ "#{HrLite.display_name(user)}'s leave (#{date_range_label}) changed after compute — recompute" ])
|
|
232
248
|
end
|
|
233
249
|
end
|
|
234
250
|
|
|
@@ -270,8 +286,8 @@ module HrLite
|
|
|
270
286
|
|
|
271
287
|
# Everyone should know a colleague will be away, the moment they apply —
|
|
272
288
|
# bell + email to the whole team (matrix row "leave.team_notice"; hosts can mute either
|
|
273
|
-
# channel). Deliberately excludes the reason: dates are
|
|
274
|
-
# the why is not.
|
|
289
|
+
# channel). Deliberately excludes the reason and the type: dates are
|
|
290
|
+
# team-relevant, the why is not.
|
|
275
291
|
def notify_team
|
|
276
292
|
return if end_date < Date.current # nobody needs to hear who was out
|
|
277
293
|
|
|
@@ -280,7 +296,7 @@ module HrLite
|
|
|
280
296
|
|
|
281
297
|
Notifications.publish(
|
|
282
298
|
"leave.team_notice",
|
|
283
|
-
title: "#{HrLite.display_name(user)} is on leave
|
|
299
|
+
title: "#{HrLite.display_name(user)} is on leave (#{date_range_label})",
|
|
284
300
|
path: "/team",
|
|
285
301
|
bell_to: team,
|
|
286
302
|
email_to: team
|
|
@@ -336,6 +352,9 @@ module HrLite
|
|
|
336
352
|
clash = self.class.where(user_id: user_id, status: %w[pending approved])
|
|
337
353
|
.where.not(id: id)
|
|
338
354
|
.overlapping_range(start_date, end_date)
|
|
355
|
+
# A first half and a second half of the same day do not overlap.
|
|
356
|
+
opposite = { "first" => "second", "second" => "first" }[half_day_part]
|
|
357
|
+
clash = clash.where.not(half_day_part: opposite).or(clash.where(half_day_part: nil)) if opposite
|
|
339
358
|
errors.add(:base, "Leave already overlaps these dates") if clash.exists?
|
|
340
359
|
end
|
|
341
360
|
|
|
@@ -20,6 +20,9 @@ module HrLite
|
|
|
20
20
|
validate :period_is_not_already_paid, on: :create
|
|
21
21
|
# Removing a line from a paid month rewrites a settled payslip just the same.
|
|
22
22
|
before_destroy { throw :abort if paid_month? }
|
|
23
|
+
# A run already computed for the month still shows the old pay.
|
|
24
|
+
after_create { flag_stale_run("added") }
|
|
25
|
+
after_destroy { flag_stale_run("removed") }
|
|
23
26
|
|
|
24
27
|
scope :for_month, ->(month) { where(period_month: month) }
|
|
25
28
|
|
|
@@ -55,5 +58,10 @@ module HrLite
|
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def paid_month? = SalarySlip.settled.exists?(user_id: user_id, period_month: period_month)
|
|
61
|
+
|
|
62
|
+
def flag_stale_run(verb)
|
|
63
|
+
run = PayrollRun.find_by(period_month: period_month, status: "review")
|
|
64
|
+
run&.update!(warnings: run.warnings + [ "#{HrLite.display_name(user)}'s #{component.label} was #{verb} after compute — recompute" ])
|
|
65
|
+
end
|
|
58
66
|
end
|
|
59
67
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module HrLite
|
|
2
2
|
# Monthly run lifecycle: draft -> processing -> review -> finalized ->
|
|
3
|
-
# published (terminal).
|
|
4
|
-
#
|
|
3
|
+
# published (terminal). The page computes through PayrollComputeJob
|
|
4
|
+
# (compute_later!); compute! itself is synchronous.
|
|
5
5
|
class PayrollRun < ApplicationRecord
|
|
6
6
|
STATUSES = %w[draft processing review finalized published].freeze
|
|
7
7
|
|
|
@@ -12,8 +12,9 @@ module HrLite
|
|
|
12
12
|
belongs_to :first_approved_by, class_name: HrLite.config.user_class, optional: true
|
|
13
13
|
|
|
14
14
|
# A run with one of these is not paid on the 3rd without a person looking:
|
|
15
|
-
# someone missing, a state levying ₹0,
|
|
16
|
-
|
|
15
|
+
# someone missing, a state levying ₹0, last year's / unverified rates, or
|
|
16
|
+
# a change after compute ("… after compute — recompute") the slips lack.
|
|
17
|
+
BLOCKING_WARNING = /\ANo (salary structure|professional-tax slabs)|statutory card|after compute/
|
|
17
18
|
|
|
18
19
|
validates :period_month, presence: true, uniqueness: true
|
|
19
20
|
validates :status, inclusion: { in: STATUSES }
|
|
@@ -37,9 +38,9 @@ module HrLite
|
|
|
37
38
|
# `processing` is allowed back in so a run stranded there by a killed
|
|
38
39
|
# process (deploy roll, timeout) can be recomputed instead of blocking
|
|
39
40
|
# that month forever.
|
|
40
|
-
def compute!(actor:)
|
|
41
|
+
def compute!(actor:, from: status)
|
|
41
42
|
raise_unless %w[draft review processing]
|
|
42
|
-
previous =
|
|
43
|
+
previous = from
|
|
43
44
|
|
|
44
45
|
begin
|
|
45
46
|
update!(status: "processing")
|
|
@@ -61,6 +62,14 @@ module HrLite
|
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
# ~1.2k queries at 54 people: the request only flips the run to processing.
|
|
66
|
+
def compute_later!(actor:)
|
|
67
|
+
raise_unless %w[draft review processing]
|
|
68
|
+
from = status
|
|
69
|
+
update!(status: "processing")
|
|
70
|
+
PayrollComputeJob.perform_later(self, from, actor)
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
def finalize!(actor:)
|
|
65
74
|
raise_unless %w[review]
|
|
66
75
|
raise ActiveRecord::RecordInvalid.new(self), "no slips" if salary_slips.none?
|