hr_lite 0.8.0 → 0.11.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 +149 -1
- data/app/controllers/hr_lite/approvals_controller.rb +31 -0
- data/app/helpers/hr_lite/application_helper.rb +21 -0
- data/app/jobs/hr_lite/approval_escalation_job.rb +39 -0
- data/app/jobs/hr_lite/document_expiry_job.rb +28 -0
- data/app/models/concerns/hr_lite/approvable.rb +48 -0
- data/app/models/hr_lite/approval.rb +46 -0
- data/app/models/hr_lite/approval_delegation.rb +38 -0
- data/app/models/hr_lite/approval_flow.rb +39 -0
- data/app/models/hr_lite/approval_step.rb +62 -0
- data/app/models/hr_lite/document.rb +95 -0
- data/app/models/hr_lite/leave_request.rb +72 -17
- data/app/models/hr_lite/loan.rb +65 -0
- data/app/models/hr_lite/loan_repayment.rb +14 -0
- data/app/models/hr_lite/payroll_line_item.rb +57 -0
- data/app/models/hr_lite/payroll_run.rb +34 -0
- data/app/models/hr_lite/salary_component.rb +59 -0
- data/app/models/hr_lite/tax_declaration.rb +93 -0
- data/app/models/hr_lite/tax_declaration_item.rb +32 -0
- data/app/services/hr_lite/approval_route.rb +109 -0
- data/app/services/hr_lite/slip_builder.rb +76 -3
- data/app/views/hr_lite/approvals/index.html.erb +66 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20260818001151_create_hr_lite_approvals.rb +79 -0
- data/db/migrate/20260818002627_create_hr_lite_payroll_components_and_loans.rb +72 -0
- data/db/migrate/20260818003616_create_hr_lite_documents_and_declarations.rb +73 -0
- data/lib/hr_lite/notifications.rb +10 -0
- data/lib/hr_lite/permissions.rb +4 -0
- data/lib/hr_lite/role_seeds.rb +6 -3
- data/lib/hr_lite/version.rb +1 -1
- data/lib/tasks/hr_lite_tasks.rake +2 -1
- metadata +21 -1
|
@@ -33,8 +33,17 @@ module HrLite
|
|
|
33
33
|
earnings = Calculators::Proration.call(
|
|
34
34
|
structure: @structure, payable_days: payable, days_in_month: days_in_month
|
|
35
35
|
)
|
|
36
|
+
# One-off heads for this month: a bonus, arrears after a backdated
|
|
37
|
+
# revision, a reimbursement. A prorated head is scaled like basic; a
|
|
38
|
+
# bonus is not halved because somebody joined mid-month.
|
|
39
|
+
extra_earnings, extra_deductions = one_off_lines(payable, days_in_month)
|
|
40
|
+
earnings += extra_earnings
|
|
41
|
+
|
|
36
42
|
gross_earned = earnings.sum(BigDecimal(0)) { |row| row[:amount] }
|
|
37
43
|
basic_earned = earnings.find { |row| row[:code] == "basic" }&.fetch(:amount) || BigDecimal(0)
|
|
44
|
+
# ESI is assessed on wages, and a reimbursement is not one. Heads that
|
|
45
|
+
# opt out are excluded from the gross it reads.
|
|
46
|
+
esi_gross = gross_earned - earnings.sum(BigDecimal(0)) { |row| row[:excluded_from_esi] ? row[:amount] : 0 }
|
|
38
47
|
|
|
39
48
|
deductions = []
|
|
40
49
|
employer_costs = {}
|
|
@@ -50,7 +59,7 @@ module HrLite
|
|
|
50
59
|
)
|
|
51
60
|
end
|
|
52
61
|
|
|
53
|
-
esi = Calculators::Esi.call(monthly_gross: esi_reference_gross, gross_earned:
|
|
62
|
+
esi = Calculators::Esi.call(monthly_gross: esi_reference_gross, gross_earned: esi_gross,
|
|
54
63
|
applicable: @structure.esi_applicable, rates: @rates[:esi])
|
|
55
64
|
if esi.applicable?
|
|
56
65
|
deductions << { code: "esi_employee", label: "ESI", amount: esi.employee }
|
|
@@ -61,9 +70,13 @@ module HrLite
|
|
|
61
70
|
period_month: @run.period_month, rates: @rates[:pt])
|
|
62
71
|
deductions << { code: "pt", label: "Professional tax", amount: pt } if pt.positive?
|
|
63
72
|
|
|
73
|
+
deductions.concat(extra_deductions)
|
|
74
|
+
loan = loan_instalment
|
|
75
|
+
deductions << loan if loan
|
|
76
|
+
|
|
64
77
|
fy = SalarySlip.fy_to_date(@user, @run.period_month)
|
|
65
78
|
tds = Calculators::Tds.call(
|
|
66
|
-
regime:
|
|
79
|
+
regime: tax_regime,
|
|
67
80
|
structure_monthly_gross: @structure.monthly_gross,
|
|
68
81
|
gross_earned_this_month: gross_earned,
|
|
69
82
|
# Plus anything paid this FY that this install never ran — a previous
|
|
@@ -73,7 +86,7 @@ module HrLite
|
|
|
73
86
|
fy_gross_paid: fy[:gross] + Money.d(@profile.fy_opening_gross),
|
|
74
87
|
fy_tds_paid: fy[:tds] + Money.d(@profile.fy_opening_tds),
|
|
75
88
|
months_remaining: months_remaining_in_fy,
|
|
76
|
-
declared_annual_deductions:
|
|
89
|
+
declared_annual_deductions: annual_deductions,
|
|
77
90
|
rates: @rates[:income_tax],
|
|
78
91
|
override: @tds_override
|
|
79
92
|
)
|
|
@@ -107,6 +120,66 @@ module HrLite
|
|
|
107
120
|
|
|
108
121
|
private
|
|
109
122
|
|
|
123
|
+
# The declaration if the employee filed one, else the single figure an
|
|
124
|
+
# admin typed on the profile. Old-regime deductions came from that one
|
|
125
|
+
# opaque number, with nothing to show what it was made of and nowhere to
|
|
126
|
+
# keep the proof; a filed declaration supersedes it, and once HR has
|
|
127
|
+
# verified the proof only what the proof supported counts.
|
|
128
|
+
# A declaration names the regime the employee chose for that year, which
|
|
129
|
+
# is a per-YEAR decision; the profile column is only the fallback for
|
|
130
|
+
# somebody who never filed one.
|
|
131
|
+
def tax_regime
|
|
132
|
+
TaxDeclaration.for(@user, @run.period_month)&.regime || @profile.tax_regime
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def annual_deductions
|
|
136
|
+
declaration = TaxDeclaration.for(@user, @run.period_month)
|
|
137
|
+
return @profile.declared_annual_deductions if declaration.nil?
|
|
138
|
+
return @profile.declared_annual_deductions if declaration.draft?
|
|
139
|
+
|
|
140
|
+
declaration.allowable_total
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Splits this month's one-off lines into earnings and deductions, in the
|
|
144
|
+
# component order an install configured. Returns [earnings, deductions].
|
|
145
|
+
def one_off_lines(payable, days_in_month)
|
|
146
|
+
rows = PayrollLineItem.for_slip(@user, @run.period_month)
|
|
147
|
+
.sort_by { |item| [ item.component.position, item.component.label ] }
|
|
148
|
+
earnings = []
|
|
149
|
+
deductions = []
|
|
150
|
+
|
|
151
|
+
rows.each do |item|
|
|
152
|
+
component = item.component
|
|
153
|
+
amount = if component.prorated
|
|
154
|
+
Money.round2(item.amount * (Money.d(payable) / Money.d(days_in_month)))
|
|
155
|
+
else
|
|
156
|
+
item.amount
|
|
157
|
+
end
|
|
158
|
+
next unless amount.positive?
|
|
159
|
+
|
|
160
|
+
row = { code: component.code, label: component.label, amount: amount }
|
|
161
|
+
if component.earning?
|
|
162
|
+
earnings << row.merge(excluded_from_esi: !component.counts_for_esi)
|
|
163
|
+
else
|
|
164
|
+
deductions << row
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
[ earnings, deductions ]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The month's loan instalment, as one deduction line. Computed here and
|
|
172
|
+
# BOOKED only when the run is finalized — a draft is recomputed freely,
|
|
173
|
+
# and booking at compute would take a repayment on every pass.
|
|
174
|
+
def loan_instalment
|
|
175
|
+
total = Loan.active.where(user_id: @user.id).sum(BigDecimal(0)) do |loan|
|
|
176
|
+
loan.instalment_for(@run.period_month)
|
|
177
|
+
end
|
|
178
|
+
return nil unless total.positive?
|
|
179
|
+
|
|
180
|
+
{ code: "loan_repayment", label: "Loan repayment", amount: total }
|
|
181
|
+
end
|
|
182
|
+
|
|
110
183
|
# Months left in the Indian FY (Apr–Mar) including the run month itself:
|
|
111
184
|
# April = 12, December = 4, January = 3, March = 1. Capped at the exit
|
|
112
185
|
# month for a leaver, so a final settlement is not spread over months
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Waiting on you" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Waiting on you</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @covering_for.any? %>
|
|
7
|
+
<section class="hrl-card">
|
|
8
|
+
<p class="hrl-small hrl-muted">
|
|
9
|
+
You are covering for
|
|
10
|
+
<%= @covering_for.map { |d| hr_display_name(d.from_user) }.to_sentence %>.
|
|
11
|
+
Their approvals appear below alongside your own.
|
|
12
|
+
</p>
|
|
13
|
+
</section>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<section class="hrl-card">
|
|
17
|
+
<% if @approvals.empty? %>
|
|
18
|
+
<p class="hrl-muted">Nothing is waiting on you.</p>
|
|
19
|
+
<% else %>
|
|
20
|
+
<div class="hrl-table-wrap">
|
|
21
|
+
<table class="hrl-table hrl-table--stack">
|
|
22
|
+
<thead><tr><th>Request</th><th>From</th><th>Waiting</th><th></th></tr></thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% @approvals.each do |approval| %>
|
|
25
|
+
<% subject = approval.subject %>
|
|
26
|
+
<tr>
|
|
27
|
+
<td data-label="Request">
|
|
28
|
+
<strong><%= approval.label %></strong>
|
|
29
|
+
<% if approval.overdue? %>
|
|
30
|
+
<span class="hrl-badge hrl-badge--bad">Overdue</span>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% if approval.approver_id != hr_current_user.id %>
|
|
33
|
+
<span class="hrl-badge hrl-badge--muted">
|
|
34
|
+
for <%= hr_display_name(approval.approver) %>
|
|
35
|
+
</span>
|
|
36
|
+
<% end %>
|
|
37
|
+
</td>
|
|
38
|
+
<td data-label="From"><%= hr_display_name(subject&.user) %></td>
|
|
39
|
+
<td data-label="Waiting"><%= time_ago_in_words(approval.created_at) %></td>
|
|
40
|
+
<td data-label="">
|
|
41
|
+
<% if (path = hrl_approval_path(approval)) %>
|
|
42
|
+
<a class="hrl-small" href="<%= path %>">Open</a>
|
|
43
|
+
<% end %>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
<% end %>
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
</div>
|
|
50
|
+
<%= hrl_pagination %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
<% if @delegations.any? %>
|
|
55
|
+
<section class="hrl-card">
|
|
56
|
+
<h2 class="hrl-card__title">While you are away</h2>
|
|
57
|
+
<ul>
|
|
58
|
+
<% @delegations.each do |delegation| %>
|
|
59
|
+
<li class="hrl-small">
|
|
60
|
+
<%= hr_display_name(delegation.to_user) %> decides for you,
|
|
61
|
+
<%= delegation.starts_on.strftime("%d %b") %> – <%= delegation.ends_on.strftime("%d %b %Y") %>
|
|
62
|
+
</li>
|
|
63
|
+
<% end %>
|
|
64
|
+
</ul>
|
|
65
|
+
</section>
|
|
66
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -19,6 +19,7 @@ HrLite::Engine.routes.draw do
|
|
|
19
19
|
resources :regularization_requests, only: %i[index new create] do
|
|
20
20
|
member { post :cancel }
|
|
21
21
|
end
|
|
22
|
+
resources :approvals, only: :index
|
|
22
23
|
get "team", to: "team#show"
|
|
23
24
|
get "org", to: "org#show"
|
|
24
25
|
resources :holidays, only: :index
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
class CreateHrLiteApprovals < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Leave, comp-off, regularization and resignation each grew their own
|
|
3
|
+
# approve/reject/cancel, with their own transition guard and their own
|
|
4
|
+
# notification. Four copies of one idea, and every module still to come
|
|
5
|
+
# (expenses, loans, salary revisions, document verification) would have
|
|
6
|
+
# been a fifth.
|
|
7
|
+
#
|
|
8
|
+
# A FLOW is the definition — "leave needs the manager, then HR". A STEP is
|
|
9
|
+
# one rung of it, naming an approver by RULE rather than by person, so the
|
|
10
|
+
# flow survives somebody leaving. An APPROVAL is one live decision on one
|
|
11
|
+
# record.
|
|
12
|
+
def change
|
|
13
|
+
create_table :hr_lite_approval_flows do |t|
|
|
14
|
+
# The model this flow decides on, e.g. "HrLite::LeaveRequest".
|
|
15
|
+
t.string :subject_type, null: false
|
|
16
|
+
t.string :name, null: false
|
|
17
|
+
t.boolean :active, null: false, default: true
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
add_index :hr_lite_approval_flows, :subject_type,
|
|
21
|
+
unique: true, where: "active", name: "index_hr_lite_approval_flows_one_active_per_type"
|
|
22
|
+
|
|
23
|
+
create_table :hr_lite_approval_steps do |t|
|
|
24
|
+
t.references :flow, null: false, index: false,
|
|
25
|
+
foreign_key: { to_table: :hr_lite_approval_flows, on_delete: :cascade }
|
|
26
|
+
t.integer :position, null: false
|
|
27
|
+
# manager | manager_of_manager | permission | user
|
|
28
|
+
t.string :approver_rule, null: false
|
|
29
|
+
# permission key for `permission`, user id for `user`, unused otherwise
|
|
30
|
+
t.string :approver_key
|
|
31
|
+
# Everybody on this rung must decide, rather than the first to answer.
|
|
32
|
+
t.boolean :unanimous, null: false, default: false
|
|
33
|
+
# Hours before the step is escalated. Null = never.
|
|
34
|
+
t.integer :sla_hours
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
add_index :hr_lite_approval_steps, %i[flow_id position], unique: true
|
|
38
|
+
add_check_constraint :hr_lite_approval_steps,
|
|
39
|
+
"approver_rule IN ('manager', 'manager_of_manager', 'permission', 'user')",
|
|
40
|
+
name: "hr_lite_approval_steps_rule_check"
|
|
41
|
+
|
|
42
|
+
create_table :hr_lite_approvals do |t|
|
|
43
|
+
t.string :subject_type, null: false
|
|
44
|
+
t.bigint :subject_id, null: false
|
|
45
|
+
t.references :step, null: false, index: false,
|
|
46
|
+
foreign_key: { to_table: :hr_lite_approval_steps, on_delete: :cascade }
|
|
47
|
+
t.integer :position, null: false
|
|
48
|
+
t.bigint :approver_id, null: false
|
|
49
|
+
# pending | approved | rejected | returned | skipped | cancelled
|
|
50
|
+
t.string :status, null: false, default: "pending"
|
|
51
|
+
t.text :note
|
|
52
|
+
t.datetime :decided_at
|
|
53
|
+
# Who decided in the approver's place, when they had delegated.
|
|
54
|
+
t.bigint :decided_by_id
|
|
55
|
+
t.datetime :escalated_at
|
|
56
|
+
t.timestamps
|
|
57
|
+
end
|
|
58
|
+
add_index :hr_lite_approvals, %i[subject_type subject_id position]
|
|
59
|
+
# One live row per approver per rung: two would let the same person
|
|
60
|
+
# decide twice, or a retry double-count a unanimous step.
|
|
61
|
+
add_index :hr_lite_approvals, %i[subject_type subject_id position approver_id],
|
|
62
|
+
unique: true, name: "index_hr_lite_approvals_one_per_approver_per_step"
|
|
63
|
+
add_index :hr_lite_approvals, :approver_id
|
|
64
|
+
add_check_constraint :hr_lite_approvals,
|
|
65
|
+
"status IN ('pending', 'approved', 'rejected', 'returned', 'skipped', 'cancelled')",
|
|
66
|
+
name: "hr_lite_approvals_status_check"
|
|
67
|
+
|
|
68
|
+
create_table :hr_lite_approval_delegations do |t|
|
|
69
|
+
t.bigint :from_user_id, null: false
|
|
70
|
+
t.bigint :to_user_id, null: false
|
|
71
|
+
t.date :starts_on, null: false
|
|
72
|
+
t.date :ends_on, null: false
|
|
73
|
+
t.string :reason
|
|
74
|
+
t.timestamps
|
|
75
|
+
end
|
|
76
|
+
add_index :hr_lite_approval_delegations, %i[from_user_id starts_on ends_on],
|
|
77
|
+
name: "index_hr_lite_delegations_on_from_and_dates"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
class CreateHrLitePayrollComponentsAndLoans < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Payroll knew exactly four earning heads — basic, HRA, special allowance,
|
|
3
|
+
# "other" — because they were columns on the salary structure. A bonus, an
|
|
4
|
+
# incentive, an LTA or a reimbursement had nowhere to go but "other", which
|
|
5
|
+
# is one number on a payslip that should have said what it was for.
|
|
6
|
+
#
|
|
7
|
+
# Heads are DATA now. Amounts stay encrypted for the same reason every
|
|
8
|
+
# amount in this engine is: a payslip line is somebody's pay.
|
|
9
|
+
def change
|
|
10
|
+
create_table :hr_lite_salary_components do |t|
|
|
11
|
+
t.string :code, null: false
|
|
12
|
+
t.string :label, null: false
|
|
13
|
+
# earning | deduction
|
|
14
|
+
t.string :kind, null: false, default: "earning"
|
|
15
|
+
# Prorated by attendance like basic, or paid whole (a bonus is not
|
|
16
|
+
# halved because somebody joined mid-month).
|
|
17
|
+
t.boolean :prorated, null: false, default: true
|
|
18
|
+
t.boolean :taxable, null: false, default: true
|
|
19
|
+
# Counts toward the gross ESI is assessed on.
|
|
20
|
+
t.boolean :counts_for_esi, null: false, default: true
|
|
21
|
+
t.integer :position, null: false, default: 0
|
|
22
|
+
t.boolean :active, null: false, default: true
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
add_index :hr_lite_salary_components, :code, unique: true
|
|
26
|
+
add_check_constraint :hr_lite_salary_components, "kind IN ('earning', 'deduction')",
|
|
27
|
+
name: "hr_lite_salary_components_kind_check"
|
|
28
|
+
|
|
29
|
+
# A one-off on a single month's payslip: a bonus, an incentive, an
|
|
30
|
+
# arrears line after a backdated revision, a reimbursement. Dated to the
|
|
31
|
+
# month rather than to the run, so it survives a run being deleted and
|
|
32
|
+
# recomputed.
|
|
33
|
+
create_table :hr_lite_payroll_line_items do |t|
|
|
34
|
+
t.bigint :user_id, null: false
|
|
35
|
+
t.date :period_month, null: false
|
|
36
|
+
t.references :component, null: false, index: true,
|
|
37
|
+
foreign_key: { to_table: :hr_lite_salary_components }
|
|
38
|
+
t.text :amount, null: false # encrypted BigDecimal
|
|
39
|
+
t.string :note
|
|
40
|
+
t.bigint :created_by_id
|
|
41
|
+
t.timestamps
|
|
42
|
+
end
|
|
43
|
+
add_index :hr_lite_payroll_line_items, %i[user_id period_month]
|
|
44
|
+
|
|
45
|
+
create_table :hr_lite_loans do |t|
|
|
46
|
+
t.bigint :user_id, null: false
|
|
47
|
+
t.text :principal, null: false # encrypted
|
|
48
|
+
t.text :monthly_instalment, null: false # encrypted
|
|
49
|
+
t.date :starts_on, null: false
|
|
50
|
+
t.string :reason
|
|
51
|
+
# active | closed | cancelled
|
|
52
|
+
t.string :status, null: false, default: "active"
|
|
53
|
+
t.bigint :approved_by_id
|
|
54
|
+
t.timestamps
|
|
55
|
+
end
|
|
56
|
+
add_index :hr_lite_loans, %i[user_id status]
|
|
57
|
+
add_check_constraint :hr_lite_loans, "status IN ('active', 'closed', 'cancelled')",
|
|
58
|
+
name: "hr_lite_loans_status_check"
|
|
59
|
+
|
|
60
|
+
# One row per month actually deducted. The outstanding balance is derived
|
|
61
|
+
# from these rather than stored, so a recompute cannot double-count and a
|
|
62
|
+
# deleted run cannot leave the balance wrong.
|
|
63
|
+
create_table :hr_lite_loan_repayments do |t|
|
|
64
|
+
t.references :loan, null: false, index: true,
|
|
65
|
+
foreign_key: { to_table: :hr_lite_loans, on_delete: :cascade }
|
|
66
|
+
t.date :period_month, null: false
|
|
67
|
+
t.text :amount, null: false # encrypted
|
|
68
|
+
t.timestamps
|
|
69
|
+
end
|
|
70
|
+
add_index :hr_lite_loan_repayments, %i[loan_id period_month], unique: true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
class CreateHrLiteDocumentsAndDeclarations < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Two gaps that were table stakes and simply absent: nowhere to keep an
|
|
3
|
+
# employee's documents, and a tax declaration that was ONE opaque number.
|
|
4
|
+
#
|
|
5
|
+
# `declared_annual_deductions` on the profile is a single figure an admin
|
|
6
|
+
# types in. Nobody can see what it is made of, the employee cannot submit
|
|
7
|
+
# their own, and there is nowhere to put the proof. That is the number the
|
|
8
|
+
# whole year's TDS is projected from.
|
|
9
|
+
def change
|
|
10
|
+
create_table :hr_lite_documents do |t|
|
|
11
|
+
t.bigint :user_id, null: false
|
|
12
|
+
# aadhaar | pan | passport | bank | offer_letter | ... — data, so an
|
|
13
|
+
# install can add its own without a release.
|
|
14
|
+
t.string :category, null: false
|
|
15
|
+
t.string :title, null: false
|
|
16
|
+
t.string :reference_number
|
|
17
|
+
t.date :issued_on
|
|
18
|
+
t.date :expires_on
|
|
19
|
+
# self | hr | money — who may open the file. A payslip and a passport
|
|
20
|
+
# are not the same kind of secret.
|
|
21
|
+
#
|
|
22
|
+
# No column default on purpose: the model derives one from the category
|
|
23
|
+
# when none was chosen, and a column default would make "unset" and
|
|
24
|
+
# "deliberately hr" indistinguishable.
|
|
25
|
+
t.string :visibility, null: false
|
|
26
|
+
# pending | verified | rejected
|
|
27
|
+
t.string :verification, null: false, default: "pending"
|
|
28
|
+
t.bigint :verified_by_id
|
|
29
|
+
t.datetime :verified_at
|
|
30
|
+
t.string :verification_note
|
|
31
|
+
t.bigint :uploaded_by_id
|
|
32
|
+
t.timestamps
|
|
33
|
+
end
|
|
34
|
+
add_index :hr_lite_documents, %i[user_id category]
|
|
35
|
+
add_index :hr_lite_documents, :expires_on
|
|
36
|
+
add_check_constraint :hr_lite_documents, "visibility IN ('self', 'hr', 'money')",
|
|
37
|
+
name: "hr_lite_documents_visibility_check"
|
|
38
|
+
add_check_constraint :hr_lite_documents,
|
|
39
|
+
"verification IN ('pending', 'verified', 'rejected')",
|
|
40
|
+
name: "hr_lite_documents_verification_check"
|
|
41
|
+
|
|
42
|
+
create_table :hr_lite_tax_declarations do |t|
|
|
43
|
+
t.bigint :user_id, null: false
|
|
44
|
+
# 1 April of the financial year it covers.
|
|
45
|
+
t.date :financial_year, null: false
|
|
46
|
+
t.string :regime, null: false, default: "new"
|
|
47
|
+
# draft | submitted | verified | rejected
|
|
48
|
+
t.string :status, null: false, default: "draft"
|
|
49
|
+
t.datetime :submitted_at
|
|
50
|
+
t.bigint :verified_by_id
|
|
51
|
+
t.datetime :verified_at
|
|
52
|
+
t.string :note
|
|
53
|
+
t.timestamps
|
|
54
|
+
end
|
|
55
|
+
add_index :hr_lite_tax_declarations, %i[user_id financial_year], unique: true
|
|
56
|
+
add_check_constraint :hr_lite_tax_declarations,
|
|
57
|
+
"status IN ('draft', 'submitted', 'verified', 'rejected')",
|
|
58
|
+
name: "hr_lite_tax_declarations_status_check"
|
|
59
|
+
|
|
60
|
+
create_table :hr_lite_tax_declaration_items do |t|
|
|
61
|
+
t.references :declaration, null: false, index: true,
|
|
62
|
+
foreign_key: { to_table: :hr_lite_tax_declarations, on_delete: :cascade }
|
|
63
|
+
# 80c | 80d | hra | 80ccd_1b | 24b | other
|
|
64
|
+
t.string :section, null: false
|
|
65
|
+
t.string :label
|
|
66
|
+
t.text :declared_amount, null: false # encrypted
|
|
67
|
+
# What proof actually supported, once HR has looked. Null until then.
|
|
68
|
+
t.text :verified_amount
|
|
69
|
+
t.timestamps
|
|
70
|
+
end
|
|
71
|
+
add_index :hr_lite_tax_declaration_items, %i[declaration_id section]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -24,6 +24,16 @@ module HrLite
|
|
|
24
24
|
"appraisal.shared" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
25
25
|
"promotion.recorded" => { bell: true, email: true, leadership_email: true, leadership_bell: true },
|
|
26
26
|
"policy.changed" => { bell: false, email: false, leadership_email: true, leadership_bell: true },
|
|
27
|
+
# An approval past its deadline. Leadership hears too — a decision
|
|
28
|
+
# nobody is making is a process problem, not just one person's inbox.
|
|
29
|
+
"approval.escalated" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
30
|
+
# Tax declarations. The submission bells whoever verifies them; the
|
|
31
|
+
# decision goes back to the employee, whose year's TDS depends on it.
|
|
32
|
+
"tax.declaration_submitted" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
|
|
33
|
+
"tax.declaration_decided" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
34
|
+
# A document about to lapse — a passport or a visa is somebody's right
|
|
35
|
+
# to work, and finding out late is the whole problem.
|
|
36
|
+
"document.expiring" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
27
37
|
"digest.daily" => { bell: false, email: false, leadership_email: true, leadership_bell: false },
|
|
28
38
|
"resignation.submitted" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
|
|
29
39
|
"resignation.accepted" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
data/lib/hr_lite/permissions.rb
CHANGED
|
@@ -43,6 +43,10 @@ module HrLite
|
|
|
43
43
|
"appraisal.manage" => [ "Growth", "Write, share and act on appraisals and promotions" ],
|
|
44
44
|
"resignation.view" => [ "Lifecycle", "See resignations" ],
|
|
45
45
|
"resignation.manage" => [ "Lifecycle", "Accept resignations and set the last working day" ],
|
|
46
|
+
"document.view" => [ "Documents", "Open other people's documents" ],
|
|
47
|
+
"document.manage" => [ "Documents", "Upload, verify and open identity and bank documents" ],
|
|
48
|
+
"tax.view" => [ "Payroll", "See tax declarations" ],
|
|
49
|
+
"tax.manage" => [ "Payroll", "Verify tax declarations and the proof behind them" ],
|
|
46
50
|
"settings.manage" => [ "Administration", "Change company settings, offices and policy" ],
|
|
47
51
|
"audit.view" => [ "Administration", "Read the audit trail" ],
|
|
48
52
|
"audit.view_money" => [ "Administration", "Read audit rows about pay, appraisals and promotions" ],
|
data/lib/hr_lite/role_seeds.rb
CHANGED
|
@@ -17,7 +17,7 @@ module HrLite
|
|
|
17
17
|
"leave.request" => "self", "leave.view" => "self",
|
|
18
18
|
"attendance.view" => "self", "payroll.view" => "self",
|
|
19
19
|
"profile.view" => "self", "appraisal.view" => "self",
|
|
20
|
-
"resignation.view" => "self"
|
|
20
|
+
"resignation.view" => "self", "document.view" => "self", "tax.view" => "self"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
Role::MANAGER => {
|
|
@@ -35,7 +35,8 @@ module HrLite
|
|
|
35
35
|
"leave.request" => "self", "leave.view" => "all", "leave.approve" => "all",
|
|
36
36
|
"leave.manage" => "all", "attendance.view" => "all", "attendance.manage" => "all",
|
|
37
37
|
"profile.view" => "all", "payroll.view" => "self",
|
|
38
|
-
"appraisal.view" => "self", "resignation.view" => "all"
|
|
38
|
+
"appraisal.view" => "self", "resignation.view" => "all",
|
|
39
|
+
"document.view" => "all", "tax.view" => "self"
|
|
39
40
|
}
|
|
40
41
|
},
|
|
41
42
|
Role::FINANCE => {
|
|
@@ -45,6 +46,7 @@ module HrLite
|
|
|
45
46
|
"attendance.view" => "all", "profile.view" => "all",
|
|
46
47
|
"payroll.view" => "all", "payroll.manage" => "all", "payroll.export" => "all",
|
|
47
48
|
"salary.view" => "all", "salary.manage" => "all",
|
|
49
|
+
"tax.view" => "all", "tax.manage" => "all",
|
|
48
50
|
"audit.view" => "all", "audit.view_money" => "all"
|
|
49
51
|
}
|
|
50
52
|
},
|
|
@@ -55,7 +57,8 @@ module HrLite
|
|
|
55
57
|
"leave.manage" => "all", "attendance.view" => "all", "attendance.manage" => "all",
|
|
56
58
|
"profile.view" => "all", "profile.manage" => "all",
|
|
57
59
|
"resignation.view" => "all", "resignation.manage" => "all",
|
|
58
|
-
"settings.manage" => "all", "audit.view" => "all", "payroll.view" => "self"
|
|
60
|
+
"settings.manage" => "all", "audit.view" => "all", "payroll.view" => "self",
|
|
61
|
+
"document.view" => "all"
|
|
59
62
|
}
|
|
60
63
|
},
|
|
61
64
|
Role::SUPER_ADMIN => {
|
data/lib/hr_lite/version.rb
CHANGED
|
@@ -4,7 +4,8 @@ namespace :hr_lite do
|
|
|
4
4
|
require "hr_lite/seeds"
|
|
5
5
|
require "hr_lite/role_seeds"
|
|
6
6
|
require "hr_lite/statutory_seeds"
|
|
7
|
-
created = HrLite::Seeds.run! + HrLite::RoleSeeds.call + HrLite::StatutorySeeds.call
|
|
7
|
+
created = HrLite::Seeds.run! + HrLite::RoleSeeds.call + HrLite::StatutorySeeds.call +
|
|
8
|
+
HrLite::SalaryComponent.seed_defaults!
|
|
8
9
|
puts created.any? ? "hr_lite:seed created: #{created.join(', ')}" : "hr_lite:seed — nothing to do"
|
|
9
10
|
end
|
|
10
11
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hr_lite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -81,6 +81,7 @@ files:
|
|
|
81
81
|
- app/controllers/hr_lite/admin/superadmin_controller.rb
|
|
82
82
|
- app/controllers/hr_lite/application_controller.rb
|
|
83
83
|
- app/controllers/hr_lite/appraisals_controller.rb
|
|
84
|
+
- app/controllers/hr_lite/approvals_controller.rb
|
|
84
85
|
- app/controllers/hr_lite/attendance_controller.rb
|
|
85
86
|
- app/controllers/hr_lite/calendar_controller.rb
|
|
86
87
|
- app/controllers/hr_lite/career_controller.rb
|
|
@@ -99,19 +100,27 @@ files:
|
|
|
99
100
|
- app/controllers/hr_lite/users_controller.rb
|
|
100
101
|
- app/helpers/hr_lite/application_helper.rb
|
|
101
102
|
- app/jobs/hr_lite/application_job.rb
|
|
103
|
+
- app/jobs/hr_lite/approval_escalation_job.rb
|
|
102
104
|
- app/jobs/hr_lite/daily_digest_job.rb
|
|
105
|
+
- app/jobs/hr_lite/document_expiry_job.rb
|
|
103
106
|
- app/jobs/hr_lite/leave_year_rollover_job.rb
|
|
104
107
|
- app/jobs/hr_lite/payroll_auto_draft_job.rb
|
|
105
108
|
- app/mailers/hr_lite/application_mailer.rb
|
|
106
109
|
- app/mailers/hr_lite/event_mailer.rb
|
|
110
|
+
- app/models/concerns/hr_lite/approvable.rb
|
|
107
111
|
- app/models/concerns/hr_lite/audited.rb
|
|
108
112
|
- app/models/concerns/hr_lite/encrypted_money.rb
|
|
109
113
|
- app/models/hr_lite/application_record.rb
|
|
110
114
|
- app/models/hr_lite/appraisal.rb
|
|
115
|
+
- app/models/hr_lite/approval.rb
|
|
116
|
+
- app/models/hr_lite/approval_delegation.rb
|
|
117
|
+
- app/models/hr_lite/approval_flow.rb
|
|
118
|
+
- app/models/hr_lite/approval_step.rb
|
|
111
119
|
- app/models/hr_lite/attendance_record.rb
|
|
112
120
|
- app/models/hr_lite/audit_log.rb
|
|
113
121
|
- app/models/hr_lite/comp_off_request.rb
|
|
114
122
|
- app/models/hr_lite/designation_change.rb
|
|
123
|
+
- app/models/hr_lite/document.rb
|
|
115
124
|
- app/models/hr_lite/employee_profile.rb
|
|
116
125
|
- app/models/hr_lite/holiday.rb
|
|
117
126
|
- app/models/hr_lite/kudo.rb
|
|
@@ -119,7 +128,10 @@ files:
|
|
|
119
128
|
- app/models/hr_lite/leave_balance.rb
|
|
120
129
|
- app/models/hr_lite/leave_request.rb
|
|
121
130
|
- app/models/hr_lite/leave_type.rb
|
|
131
|
+
- app/models/hr_lite/loan.rb
|
|
132
|
+
- app/models/hr_lite/loan_repayment.rb
|
|
122
133
|
- app/models/hr_lite/office_location.rb
|
|
134
|
+
- app/models/hr_lite/payroll_line_item.rb
|
|
123
135
|
- app/models/hr_lite/payroll_run.rb
|
|
124
136
|
- app/models/hr_lite/professional_tax_slab.rb
|
|
125
137
|
- app/models/hr_lite/regularization_request.rb
|
|
@@ -127,11 +139,15 @@ files:
|
|
|
127
139
|
- app/models/hr_lite/role.rb
|
|
128
140
|
- app/models/hr_lite/role_assignment.rb
|
|
129
141
|
- app/models/hr_lite/role_grant.rb
|
|
142
|
+
- app/models/hr_lite/salary_component.rb
|
|
130
143
|
- app/models/hr_lite/salary_slip.rb
|
|
131
144
|
- app/models/hr_lite/salary_structure.rb
|
|
132
145
|
- app/models/hr_lite/setting.rb
|
|
133
146
|
- app/models/hr_lite/statutory_rate_card_record.rb
|
|
147
|
+
- app/models/hr_lite/tax_declaration.rb
|
|
148
|
+
- app/models/hr_lite/tax_declaration_item.rb
|
|
134
149
|
- app/services/hr_lite/access.rb
|
|
150
|
+
- app/services/hr_lite/approval_route.rb
|
|
135
151
|
- app/services/hr_lite/attendance_puncher.rb
|
|
136
152
|
- app/services/hr_lite/attendance_summary.rb
|
|
137
153
|
- app/services/hr_lite/calculators/esi.rb
|
|
@@ -195,6 +211,7 @@ files:
|
|
|
195
211
|
- app/views/hr_lite/admin/statutory_rate_cards/new.html.erb
|
|
196
212
|
- app/views/hr_lite/appraisals/index.html.erb
|
|
197
213
|
- app/views/hr_lite/appraisals/show.html.erb
|
|
214
|
+
- app/views/hr_lite/approvals/index.html.erb
|
|
198
215
|
- app/views/hr_lite/attendance/_month_grid.html.erb
|
|
199
216
|
- app/views/hr_lite/attendance/_punch_card.html.erb
|
|
200
217
|
- app/views/hr_lite/attendance/show.html.erb
|
|
@@ -263,6 +280,9 @@ files:
|
|
|
263
280
|
- db/migrate/20260817183347_create_hr_lite_roles_and_grants.rb
|
|
264
281
|
- db/migrate/20260817183629_seed_hr_lite_roles_from_email_tiers.rb
|
|
265
282
|
- db/migrate/20260817190159_create_hr_lite_statutory_rate_cards.rb
|
|
283
|
+
- db/migrate/20260818001151_create_hr_lite_approvals.rb
|
|
284
|
+
- db/migrate/20260818002627_create_hr_lite_payroll_components_and_loans.rb
|
|
285
|
+
- db/migrate/20260818003616_create_hr_lite_documents_and_declarations.rb
|
|
266
286
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
267
287
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
268
288
|
- lib/generators/hr_lite/install/templates/initializer.rb
|