hr_lite 0.5.3 → 0.7.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 +107 -1
- data/README.md +67 -31
- data/app/controllers/hr_lite/admin/attendances_controller.rb +18 -2
- data/app/controllers/hr_lite/admin/base_controller.rb +16 -6
- data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +16 -4
- data/app/controllers/hr_lite/admin/leadership_controller.rb +11 -8
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +21 -5
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +16 -4
- data/app/controllers/hr_lite/admin/role_assignments_controller.rb +63 -0
- data/app/controllers/hr_lite/admin/roles_controller.rb +73 -0
- data/app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb +97 -0
- data/app/controllers/hr_lite/admin/superadmin_controller.rb +15 -11
- data/app/controllers/hr_lite/application_controller.rb +39 -1
- data/app/helpers/hr_lite/application_helper.rb +6 -2
- data/app/models/hr_lite/professional_tax_slab.rb +41 -0
- data/app/models/hr_lite/role.rb +66 -0
- data/app/models/hr_lite/role_assignment.rb +18 -0
- data/app/models/hr_lite/role_grant.rb +15 -0
- data/app/models/hr_lite/statutory_rate_card_record.rb +95 -0
- data/app/services/hr_lite/access.rb +111 -0
- data/app/services/hr_lite/calculators/professional_tax.rb +34 -5
- data/app/services/hr_lite/payroll_run_processor.rb +17 -0
- data/app/views/hr_lite/admin/roles/_form.html.erb +47 -0
- data/app/views/hr_lite/admin/roles/edit.html.erb +53 -0
- data/app/views/hr_lite/admin/roles/index.html.erb +49 -0
- data/app/views/hr_lite/admin/roles/new.html.erb +3 -0
- data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +109 -0
- data/app/views/hr_lite/admin/statutory_rate_cards/edit.html.erb +4 -0
- data/app/views/hr_lite/admin/statutory_rate_cards/index.html.erb +59 -0
- data/app/views/hr_lite/admin/statutory_rate_cards/new.html.erb +4 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20260817183347_create_hr_lite_roles_and_grants.rb +49 -0
- data/db/migrate/20260817183629_seed_hr_lite_roles_from_email_tiers.rb +82 -0
- data/db/migrate/20260817190159_create_hr_lite_statutory_rate_cards.rb +44 -0
- data/lib/generators/hr_lite/install/templates/initializer.rb +13 -7
- data/lib/hr_lite/configuration.rb +8 -1
- data/lib/hr_lite/current.rb +4 -0
- data/lib/hr_lite/permissions.rb +79 -0
- data/lib/hr_lite/role_seeds.rb +81 -0
- data/lib/hr_lite/statutory_rate_card.rb +55 -10
- data/lib/hr_lite/statutory_seeds.rb +71 -0
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +64 -10
- data/lib/tasks/hr_lite_tasks.rake +18 -2
- metadata +24 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# The whole vocabulary of the engine's authorization, in one place.
|
|
3
|
+
#
|
|
4
|
+
# A permission is a KEY plus a SCOPE. The key says what the action is
|
|
5
|
+
# ("approve leave"); the scope says whose rows it reaches:
|
|
6
|
+
#
|
|
7
|
+
# self — only this person's own records
|
|
8
|
+
# team — the people who report to them (EmployeeProfile#manager_id)
|
|
9
|
+
# all — everyone in the company
|
|
10
|
+
#
|
|
11
|
+
# That is why there is no `leave.approve_own` / `leave.approve_team` /
|
|
12
|
+
# `leave.approve_all` triple: one key with three possible scopes says the
|
|
13
|
+
# same thing without three times the surface to get wrong.
|
|
14
|
+
#
|
|
15
|
+
# Keys are DATA — a role grants them from the database. They are declared
|
|
16
|
+
# here so that a typo in a controller is a boot-time failure rather than a
|
|
17
|
+
# permission that silently never matches.
|
|
18
|
+
module Permissions
|
|
19
|
+
SCOPES = %i[self team all].freeze
|
|
20
|
+
|
|
21
|
+
# Ordered weakest to strongest. `all` satisfies a `team` requirement,
|
|
22
|
+
# `team` satisfies `self`; never the other way round.
|
|
23
|
+
SCOPE_RANK = { self: 0, team: 1, all: 2 }.freeze
|
|
24
|
+
|
|
25
|
+
# key => [ group, description ]. The group is what the roles screen
|
|
26
|
+
# renders as a heading; the description is the sentence beside the
|
|
27
|
+
# checkbox, so it is written for the person granting it.
|
|
28
|
+
REGISTRY = {
|
|
29
|
+
"profile.view" => [ "People", "See employee profiles" ],
|
|
30
|
+
"profile.manage" => [ "People", "Create and edit employee profiles, onboard and offboard" ],
|
|
31
|
+
"attendance.view" => [ "Attendance", "See attendance records" ],
|
|
32
|
+
"attendance.manage" => [ "Attendance", "Correct punches and decide regularization tickets" ],
|
|
33
|
+
"leave.request" => [ "Leave", "Apply for leave and comp-off" ],
|
|
34
|
+
"leave.view" => [ "Leave", "See leave requests and balances" ],
|
|
35
|
+
"leave.approve" => [ "Leave", "Approve, reject and cancel leave and comp-off" ],
|
|
36
|
+
"leave.manage" => [ "Leave", "Adjust balances, configure leave types and holidays" ],
|
|
37
|
+
"payroll.view" => [ "Payroll", "See payroll runs and salary slips" ],
|
|
38
|
+
"payroll.manage" => [ "Payroll", "Create, compute, finalize, unlock and publish payroll" ],
|
|
39
|
+
"payroll.export" => [ "Payroll", "Download the payout register, including bank details" ],
|
|
40
|
+
"salary.view" => [ "Payroll", "See salary structures" ],
|
|
41
|
+
"salary.manage" => [ "Payroll", "Set and revise salary structures" ],
|
|
42
|
+
"appraisal.view" => [ "Growth", "See appraisals" ],
|
|
43
|
+
"appraisal.manage" => [ "Growth", "Write, share and act on appraisals and promotions" ],
|
|
44
|
+
"resignation.view" => [ "Lifecycle", "See resignations" ],
|
|
45
|
+
"resignation.manage" => [ "Lifecycle", "Accept resignations and set the last working day" ],
|
|
46
|
+
"settings.manage" => [ "Administration", "Change company settings, offices and policy" ],
|
|
47
|
+
"audit.view" => [ "Administration", "Read the audit trail" ],
|
|
48
|
+
"audit.view_money" => [ "Administration", "Read audit rows about pay, appraisals and promotions" ],
|
|
49
|
+
"role.manage" => [ "Administration", "Create roles and grant permissions" ]
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
KEYS = REGISTRY.keys.freeze
|
|
53
|
+
|
|
54
|
+
def self.valid?(key) = REGISTRY.key?(key.to_s)
|
|
55
|
+
|
|
56
|
+
# Raises rather than returning false: an unknown key in a controller is a
|
|
57
|
+
# typo that would otherwise read as "nobody may do this", which fails in
|
|
58
|
+
# the safe direction but silently, and is then very hard to find.
|
|
59
|
+
def self.validate!(key)
|
|
60
|
+
return key.to_s if valid?(key)
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "Unknown HrLite permission #{key.inspect}. " \
|
|
63
|
+
"Declare it in HrLite::Permissions::REGISTRY first."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.group(key) = REGISTRY.fetch(key.to_s).first
|
|
67
|
+
def self.description(key) = REGISTRY.fetch(key.to_s).last
|
|
68
|
+
|
|
69
|
+
def self.grouped
|
|
70
|
+
REGISTRY.group_by { |_key, (group, _)| group }
|
|
71
|
+
.transform_values { |pairs| pairs.map(&:first) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Does `held` satisfy a requirement for `needed`?
|
|
75
|
+
def self.scope_covers?(held, needed)
|
|
76
|
+
SCOPE_RANK.fetch(held.to_sym) >= SCOPE_RANK.fetch(needed.to_sym)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# The six roles an install starts with. They are a STARTING POINT, not a
|
|
3
|
+
# ladder in code: an install is expected to edit the grants, and `roles:seed`
|
|
4
|
+
# never overwrites a role that already exists.
|
|
5
|
+
#
|
|
6
|
+
# Read the scopes as the interesting part. Manager and HR hold the same
|
|
7
|
+
# leave.approve key — the difference between them is `team` and `all`, which
|
|
8
|
+
# is the whole reason scope lives on the grant.
|
|
9
|
+
module RoleSeeds
|
|
10
|
+
# A method, not a constant: the keys are Role constants, and this file is
|
|
11
|
+
# required while the gem loads, long before Active Record models exist.
|
|
12
|
+
def self.definitions
|
|
13
|
+
{
|
|
14
|
+
Role::EMPLOYEE => {
|
|
15
|
+
description: "Self-service only: own attendance, leave, documents and payslips.",
|
|
16
|
+
grants: {
|
|
17
|
+
"leave.request" => "self", "leave.view" => "self",
|
|
18
|
+
"attendance.view" => "self", "payroll.view" => "self",
|
|
19
|
+
"profile.view" => "self", "appraisal.view" => "self",
|
|
20
|
+
"resignation.view" => "self"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
Role::MANAGER => {
|
|
24
|
+
description: "Everything an employee has, plus their reports' attendance and leave.",
|
|
25
|
+
grants: {
|
|
26
|
+
"leave.request" => "self", "leave.view" => "team", "leave.approve" => "team",
|
|
27
|
+
"attendance.view" => "team", "attendance.manage" => "team",
|
|
28
|
+
"payroll.view" => "self", "profile.view" => "team",
|
|
29
|
+
"appraisal.view" => "self", "resignation.view" => "self"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
Role::HR => {
|
|
33
|
+
description: "Day-to-day operations for everyone: attendance, leave, holidays, tickets.",
|
|
34
|
+
grants: {
|
|
35
|
+
"leave.request" => "self", "leave.view" => "all", "leave.approve" => "all",
|
|
36
|
+
"leave.manage" => "all", "attendance.view" => "all", "attendance.manage" => "all",
|
|
37
|
+
"profile.view" => "all", "payroll.view" => "self",
|
|
38
|
+
"appraisal.view" => "self", "resignation.view" => "all"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
Role::FINANCE => {
|
|
42
|
+
description: "Payroll and pay data. No authority over people or policy.",
|
|
43
|
+
grants: {
|
|
44
|
+
"leave.request" => "self", "leave.view" => "self",
|
|
45
|
+
"attendance.view" => "all", "profile.view" => "all",
|
|
46
|
+
"payroll.view" => "all", "payroll.manage" => "all", "payroll.export" => "all",
|
|
47
|
+
"salary.view" => "all", "salary.manage" => "all",
|
|
48
|
+
"audit.view" => "all", "audit.view_money" => "all"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
Role::LEADERSHIP => {
|
|
52
|
+
description: "People and policy for everyone — deliberately NOT pay.",
|
|
53
|
+
grants: {
|
|
54
|
+
"leave.request" => "self", "leave.view" => "all", "leave.approve" => "all",
|
|
55
|
+
"leave.manage" => "all", "attendance.view" => "all", "attendance.manage" => "all",
|
|
56
|
+
"profile.view" => "all", "profile.manage" => "all",
|
|
57
|
+
"resignation.view" => "all", "resignation.manage" => "all",
|
|
58
|
+
"settings.manage" => "all", "audit.view" => "all", "payroll.view" => "self"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
Role::SUPER_ADMIN => {
|
|
62
|
+
description: "Everything, including pay, appraisals and who holds which role.",
|
|
63
|
+
grants: Permissions::KEYS.to_h { |key| [ key, "all" ] }
|
|
64
|
+
}
|
|
65
|
+
}.freeze
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Creates any role that does not exist yet and leaves every existing one
|
|
69
|
+
# exactly as the install has tuned it — the same contract as the leave-type
|
|
70
|
+
# seed. Returns the names it created.
|
|
71
|
+
def self.call
|
|
72
|
+
definitions.filter_map do |name, definition|
|
|
73
|
+
next if Role.exists?(name: name)
|
|
74
|
+
|
|
75
|
+
role = Role.create!(name: name, description: definition[:description], system: true)
|
|
76
|
+
role.replace_grants!(definition[:grants])
|
|
77
|
+
name
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
module HrLite
|
|
2
|
-
#
|
|
3
|
-
# is a one-hash edit that gets code review and a spec diff, never an
|
|
4
|
-
# inline-constant hunt.
|
|
2
|
+
# Where payroll asks "what were the statutory figures in this month".
|
|
5
3
|
#
|
|
6
|
-
#
|
|
7
|
-
# a year is
|
|
4
|
+
# Cards live in the DATABASE (`hr_lite_statutory_rate_cards`), effective-
|
|
5
|
+
# dated, so adding a financial year is a screen an accountant fills in
|
|
6
|
+
# rather than a gem release. Every April used to be a deadline the gem
|
|
7
|
+
# controlled and the company did not.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# CARDS below is the SEED — the figures the gem ships with, copied into the
|
|
10
|
+
# table by `hr_lite:seed` and then owned by the install. It is also the
|
|
11
|
+
# fallback for a host that has not migrated or seeded yet, so the lookup
|
|
12
|
+
# never returns nil half-way through computing somebody's salary.
|
|
13
|
+
#
|
|
14
|
+
# When no card covers a run's financial year the lookup still answers —
|
|
15
|
+
# payroll cannot stop dead every 1 April — but `warning_for` says so, and
|
|
16
|
+
# `PayrollRunProcessor` puts that sentence above every other warning.
|
|
13
17
|
module StatutoryRateCard
|
|
14
18
|
def self.r(value) = BigDecimal(value.to_s)
|
|
15
19
|
private_class_method :r
|
|
@@ -62,15 +66,44 @@ module HrLite
|
|
|
62
66
|
}.freeze
|
|
63
67
|
|
|
64
68
|
def self.for(period_month)
|
|
69
|
+
record = record_for(period_month)
|
|
70
|
+
return record.to_card if record
|
|
71
|
+
|
|
65
72
|
CARDS[effective_date_for(period_month)]
|
|
66
73
|
end
|
|
67
74
|
|
|
75
|
+
# The stored card a run resolves to, or nil when the table is empty (a
|
|
76
|
+
# host that has not run `hr_lite:seed` yet) and the shipped hash answers
|
|
77
|
+
# instead.
|
|
78
|
+
def self.record_for(period_month)
|
|
79
|
+
return nil unless stored?
|
|
80
|
+
|
|
81
|
+
StatutoryRateCardRecord.effective_for(period_month)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Deliberately rescued: the lookup is called from payroll, and a host
|
|
85
|
+
# mid-migration — gem upgraded, `db:migrate` not yet run — must fall back
|
|
86
|
+
# to the shipped figures rather than 500.
|
|
87
|
+
def self.stored?
|
|
88
|
+
StatutoryRateCardRecord.table_exists? && StatutoryRateCardRecord.exists?
|
|
89
|
+
rescue ActiveRecord::ActiveRecordError
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
68
93
|
# Which card a run will actually use. A month older than every card
|
|
69
94
|
# borrows the earliest one — see `predates_cards?`, which says so.
|
|
70
95
|
def self.effective_date_for(period_month)
|
|
96
|
+
if stored?
|
|
97
|
+
return StatutoryRateCardRecord.effective_for(period_month).effective_from
|
|
98
|
+
end
|
|
99
|
+
|
|
71
100
|
CARDS.keys.sort.reverse.find { |date| date <= period_month } || CARDS.keys.min
|
|
72
101
|
end
|
|
73
102
|
|
|
103
|
+
def self.earliest_date
|
|
104
|
+
stored? ? StatutoryRateCardRecord.chronological.first.effective_from : CARDS.keys.min
|
|
105
|
+
end
|
|
106
|
+
|
|
74
107
|
# The card in force is from an EARLIER financial year than the run. The
|
|
75
108
|
# lookup still returns figures — it has to, or payroll would stop dead
|
|
76
109
|
# every April — so the run carries the warning instead.
|
|
@@ -81,7 +114,19 @@ module HrLite
|
|
|
81
114
|
# The run is older than every card we ship, so it is being computed on
|
|
82
115
|
# rates that had not been announced yet.
|
|
83
116
|
def self.predates_cards?(period_month)
|
|
84
|
-
period_month <
|
|
117
|
+
period_month < earliest_date
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A card exists for the run's year but nobody has signed it off. Ranked
|
|
121
|
+
# below staleness because wrong-year figures are the worse problem, but
|
|
122
|
+
# still worth saying: these numbers decide what lands in a bank account.
|
|
123
|
+
def self.unverified_for(period_month)
|
|
124
|
+
record = record_for(period_month)
|
|
125
|
+
return nil if record.nil? || record.verified? || stale_for?(period_month)
|
|
126
|
+
|
|
127
|
+
"The FY #{record.financial_year} statutory card has not been marked " \
|
|
128
|
+
"verified. Check PF, ESI and the tax slabs with your accountant, then " \
|
|
129
|
+
"record who confirmed them on the rate-card screen."
|
|
85
130
|
end
|
|
86
131
|
|
|
87
132
|
# One sentence naming both the run's FY and the card's, or nil when they
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Copies the figures the gem ships with into the tables that own them from
|
|
3
|
+
# then on. Idempotent, and it NEVER touches a card the install already has:
|
|
4
|
+
# once an accountant has corrected a number, a deploy must not put the
|
|
5
|
+
# gem's version back.
|
|
6
|
+
#
|
|
7
|
+
# Nothing here is invented. Every figure is lifted from
|
|
8
|
+
# StatutoryRateCard::CARDS, which is the same hash that has always shipped.
|
|
9
|
+
module StatutorySeeds
|
|
10
|
+
def self.call
|
|
11
|
+
seed_cards! + seed_pt_slabs!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.seed_cards!
|
|
15
|
+
StatutoryRateCard::CARDS.filter_map do |effective_from, card|
|
|
16
|
+
next if StatutoryRateCardRecord.exists?(effective_from: effective_from)
|
|
17
|
+
|
|
18
|
+
StatutoryRateCardRecord.create!(
|
|
19
|
+
effective_from: effective_from,
|
|
20
|
+
pf: stringify(card[:pf]),
|
|
21
|
+
esi: stringify(card[:esi]),
|
|
22
|
+
income_tax: card[:income_tax].transform_values { |regime| stringify_regime(regime) },
|
|
23
|
+
notes: "Shipped with hr_lite #{HrLite::VERSION}. Confirm with your " \
|
|
24
|
+
"accountant, then record who verified it."
|
|
25
|
+
)
|
|
26
|
+
"rate card FY #{FinancialYear.label(effective_from)}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.seed_pt_slabs!
|
|
31
|
+
created = []
|
|
32
|
+
StatutoryRateCard::CARDS.each do |effective_from, card|
|
|
33
|
+
card[:pt].each do |state, slabs|
|
|
34
|
+
# An empty slab list in the shipped hash meant "we know of no bands
|
|
35
|
+
# for this state" — which is exactly the silence the slab table is
|
|
36
|
+
# here to distinguish from a real zero. Nothing to copy.
|
|
37
|
+
next if slabs.empty?
|
|
38
|
+
next if ProfessionalTaxSlab.exists?(state: state, effective_from: effective_from)
|
|
39
|
+
|
|
40
|
+
slabs.each do |slab|
|
|
41
|
+
ProfessionalTaxSlab.create!(
|
|
42
|
+
state: state, effective_from: effective_from,
|
|
43
|
+
from_amount: slab[:from] || slab[:above],
|
|
44
|
+
monthly: slab[:monthly], feb_extra: slab[:feb_extra]
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
created << "#{state} PT slabs"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
created
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# JSON columns take strings so a BigDecimal survives the round trip
|
|
54
|
+
# intact — a float would not, and these figures decide deductions.
|
|
55
|
+
def self.stringify(hash)
|
|
56
|
+
hash.to_h { |key, value| [ key.to_s, value.to_s("F") ] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.stringify_regime(regime)
|
|
60
|
+
{
|
|
61
|
+
"standard_deduction" => regime[:standard_deduction].to_s("F"),
|
|
62
|
+
"rebate_cap" => regime[:rebate_cap].to_s("F"),
|
|
63
|
+
"cess_rate" => regime[:cess_rate].to_s("F"),
|
|
64
|
+
"marginal_relief" => !!regime[:marginal_relief],
|
|
65
|
+
"slabs" => regime[:slabs].map { |from, to, rate|
|
|
66
|
+
[ from.to_s("F"), to&.to_s("F"), rate.to_s("F") ]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/hr_lite/version.rb
CHANGED
data/lib/hr_lite.rb
CHANGED
|
@@ -4,6 +4,9 @@ require "hr_lite/configuration"
|
|
|
4
4
|
require "hr_lite/current"
|
|
5
5
|
require "hr_lite/leave_year"
|
|
6
6
|
require "hr_lite/financial_year"
|
|
7
|
+
require "hr_lite/permissions"
|
|
8
|
+
require "hr_lite/role_seeds"
|
|
9
|
+
require "hr_lite/statutory_seeds"
|
|
7
10
|
require "hr_lite/mention_parser"
|
|
8
11
|
require "hr_lite/notifications"
|
|
9
12
|
require "hr_lite/seeds"
|
|
@@ -31,16 +34,49 @@ module HrLite
|
|
|
31
34
|
config.user_class.constantize
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
# --- authorization -----------------------------------------------------
|
|
38
|
+
|
|
39
|
+
# The question every gate now asks. `scope:` is what the CALLER needs;
|
|
40
|
+
# a holder with a wider scope satisfies it.
|
|
41
|
+
def can?(user, key, scope: :self)
|
|
42
|
+
Access.for(user).can?(key, scope: scope)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whether `user` may exercise `key` over `subject`'s rows. This is the
|
|
46
|
+
# question that did not exist before roles, and its absence is why any
|
|
47
|
+
# admin could approve anybody's leave.
|
|
48
|
+
def reaches?(user, key, subject)
|
|
49
|
+
Access.for(user).reaches?(key, subject)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def access_for(user) = Access.for(user)
|
|
53
|
+
|
|
54
|
+
# --- legacy tier predicates --------------------------------------------
|
|
55
|
+
#
|
|
56
|
+
# Kept because views, hosts and the notification fan-out all call them.
|
|
57
|
+
# They are now READ OFF ROLES rather than off a mutable email column; the
|
|
58
|
+
# configured lambdas survive only as an explicit opt-in for a host that
|
|
59
|
+
# has not migrated yet (see Configuration#legacy_tier_checks).
|
|
60
|
+
|
|
34
61
|
def admin?(user)
|
|
35
|
-
user.
|
|
62
|
+
return false if user.blank?
|
|
63
|
+
return !!config.admin_check.call(user) if config.legacy_tier_checks
|
|
64
|
+
|
|
65
|
+
can?(user, "leave.approve", scope: :all) || can?(user, "attendance.manage", scope: :all)
|
|
36
66
|
end
|
|
37
67
|
|
|
38
68
|
def leadership?(user)
|
|
39
|
-
user.
|
|
69
|
+
return false if user.blank?
|
|
70
|
+
return !!config.leadership_check.call(user) if config.legacy_tier_checks
|
|
71
|
+
|
|
72
|
+
can?(user, "profile.manage", scope: :all)
|
|
40
73
|
end
|
|
41
74
|
|
|
42
75
|
def superadmin?(user)
|
|
43
|
-
user.
|
|
76
|
+
return false if user.blank?
|
|
77
|
+
return !!config.superadmin_check.call(user) if config.legacy_tier_checks
|
|
78
|
+
|
|
79
|
+
can?(user, "payroll.manage", scope: :all)
|
|
44
80
|
end
|
|
45
81
|
|
|
46
82
|
# An access list, cleaned. "a@x.com,,b@x.com".split(",") — one stray
|
|
@@ -60,21 +96,39 @@ module HrLite
|
|
|
60
96
|
end
|
|
61
97
|
|
|
62
98
|
# Leadership members resolvable to actual user records (for bell
|
|
63
|
-
# notifications).
|
|
64
|
-
# still reachable by email — see Notifications.
|
|
99
|
+
# notifications). On a legacy host, emails configured but absent from the
|
|
100
|
+
# user table are still reachable by email — see Notifications.
|
|
65
101
|
def leadership_users
|
|
102
|
+
unless config.legacy_tier_checks
|
|
103
|
+
return users_holding("profile.manage", scope: :all)
|
|
104
|
+
end
|
|
105
|
+
|
|
66
106
|
emails = normalize_email_list(config.leadership_emails)
|
|
67
107
|
return user_klass.none if emails.empty?
|
|
68
108
|
|
|
69
109
|
user_klass.where("LOWER(#{user_klass.table_name}.email) IN (?)", emails)
|
|
70
110
|
end
|
|
71
111
|
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
# Everyone whose roles grant `key` at `scope` or wider. One query over
|
|
113
|
+
# the grant tables rather than instantiating every user and asking.
|
|
114
|
+
def users_holding(key, scope: :all)
|
|
115
|
+
wide = Permissions::SCOPE_RANK.select { |_, rank| rank >= Permissions::SCOPE_RANK.fetch(scope) }
|
|
116
|
+
ids = RoleAssignment.joins(role: :role_grants)
|
|
117
|
+
.where(hr_lite_role_grants: {
|
|
118
|
+
permission_key: Permissions.validate!(key), scope: wide.keys.map(&:to_s)
|
|
119
|
+
})
|
|
120
|
+
.distinct.pluck(:user_id)
|
|
121
|
+
user_klass.where(id: ids)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Every domain event that bells the admins calls this. On roles it is one
|
|
125
|
+
# query over the grant tables; on a legacy host it still has to run the
|
|
126
|
+
# host's lambda per row, which is why it starts from employees_scope
|
|
127
|
+
# (narrowed to real staff) rather than every row that exists.
|
|
76
128
|
def admin_users
|
|
77
|
-
employees.select { |u| admin?(u) }
|
|
129
|
+
return employees.select { |u| admin?(u) } if config.legacy_tier_checks
|
|
130
|
+
|
|
131
|
+
users_holding("leave.approve", scope: :all).sort_by { |u| display_name(u).downcase }
|
|
78
132
|
end
|
|
79
133
|
|
|
80
134
|
# Everyone HR tracks (host-overridable to exclude bots/test accounts),
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
namespace :hr_lite do
|
|
2
|
-
desc "Idempotently seed HR reference data (leave types, national holidays)"
|
|
2
|
+
desc "Idempotently seed HR reference data (leave types, national holidays, roles)"
|
|
3
3
|
task seed: :environment do
|
|
4
4
|
require "hr_lite/seeds"
|
|
5
|
-
|
|
5
|
+
require "hr_lite/role_seeds"
|
|
6
|
+
require "hr_lite/statutory_seeds"
|
|
7
|
+
created = HrLite::Seeds.run! + HrLite::RoleSeeds.call + HrLite::StatutorySeeds.call
|
|
6
8
|
puts created.any? ? "hr_lite:seed created: #{created.join(', ')}" : "hr_lite:seed — nothing to do"
|
|
7
9
|
end
|
|
10
|
+
|
|
11
|
+
desc "Copy the shipped statutory figures into the rate-card tables (never overwrites)"
|
|
12
|
+
task statutory: :environment do
|
|
13
|
+
require "hr_lite/statutory_seeds"
|
|
14
|
+
created = HrLite::StatutorySeeds.call
|
|
15
|
+
puts created.any? ? "hr_lite:statutory created: #{created.join(', ')}" : "hr_lite:statutory — nothing to do"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Idempotently seed the built-in roles only (never overwrites a tuned role)"
|
|
19
|
+
task roles: :environment do
|
|
20
|
+
require "hr_lite/role_seeds"
|
|
21
|
+
created = HrLite::RoleSeeds.call
|
|
22
|
+
puts created.any? ? "hr_lite:roles created: #{created.join(', ')}" : "hr_lite:roles — nothing to do"
|
|
23
|
+
end
|
|
8
24
|
end
|
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.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -72,9 +72,12 @@ files:
|
|
|
72
72
|
- app/controllers/hr_lite/admin/payroll_runs_controller.rb
|
|
73
73
|
- app/controllers/hr_lite/admin/regularization_requests_controller.rb
|
|
74
74
|
- app/controllers/hr_lite/admin/resignations_controller.rb
|
|
75
|
+
- app/controllers/hr_lite/admin/role_assignments_controller.rb
|
|
76
|
+
- app/controllers/hr_lite/admin/roles_controller.rb
|
|
75
77
|
- app/controllers/hr_lite/admin/salary_slips_controller.rb
|
|
76
78
|
- app/controllers/hr_lite/admin/salary_structures_controller.rb
|
|
77
79
|
- app/controllers/hr_lite/admin/settings_controller.rb
|
|
80
|
+
- app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb
|
|
78
81
|
- app/controllers/hr_lite/admin/superadmin_controller.rb
|
|
79
82
|
- app/controllers/hr_lite/application_controller.rb
|
|
80
83
|
- app/controllers/hr_lite/appraisals_controller.rb
|
|
@@ -118,11 +121,17 @@ files:
|
|
|
118
121
|
- app/models/hr_lite/leave_type.rb
|
|
119
122
|
- app/models/hr_lite/office_location.rb
|
|
120
123
|
- app/models/hr_lite/payroll_run.rb
|
|
124
|
+
- app/models/hr_lite/professional_tax_slab.rb
|
|
121
125
|
- app/models/hr_lite/regularization_request.rb
|
|
122
126
|
- app/models/hr_lite/resignation.rb
|
|
127
|
+
- app/models/hr_lite/role.rb
|
|
128
|
+
- app/models/hr_lite/role_assignment.rb
|
|
129
|
+
- app/models/hr_lite/role_grant.rb
|
|
123
130
|
- app/models/hr_lite/salary_slip.rb
|
|
124
131
|
- app/models/hr_lite/salary_structure.rb
|
|
125
132
|
- app/models/hr_lite/setting.rb
|
|
133
|
+
- app/models/hr_lite/statutory_rate_card_record.rb
|
|
134
|
+
- app/services/hr_lite/access.rb
|
|
126
135
|
- app/services/hr_lite/attendance_puncher.rb
|
|
127
136
|
- app/services/hr_lite/attendance_summary.rb
|
|
128
137
|
- app/services/hr_lite/calculators/esi.rb
|
|
@@ -170,12 +179,20 @@ files:
|
|
|
170
179
|
- app/views/hr_lite/admin/payroll_runs/show.html.erb
|
|
171
180
|
- app/views/hr_lite/admin/regularization_requests/index.html.erb
|
|
172
181
|
- app/views/hr_lite/admin/regularization_requests/show.html.erb
|
|
182
|
+
- app/views/hr_lite/admin/roles/_form.html.erb
|
|
183
|
+
- app/views/hr_lite/admin/roles/edit.html.erb
|
|
184
|
+
- app/views/hr_lite/admin/roles/index.html.erb
|
|
185
|
+
- app/views/hr_lite/admin/roles/new.html.erb
|
|
173
186
|
- app/views/hr_lite/admin/salary_slips/show.html.erb
|
|
174
187
|
- app/views/hr_lite/admin/salary_structures/_form.html.erb
|
|
175
188
|
- app/views/hr_lite/admin/salary_structures/edit.html.erb
|
|
176
189
|
- app/views/hr_lite/admin/salary_structures/new.html.erb
|
|
177
190
|
- app/views/hr_lite/admin/settings/edit.html.erb
|
|
178
191
|
- app/views/hr_lite/admin/shared/_approvals_tabs.html.erb
|
|
192
|
+
- app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb
|
|
193
|
+
- app/views/hr_lite/admin/statutory_rate_cards/edit.html.erb
|
|
194
|
+
- app/views/hr_lite/admin/statutory_rate_cards/index.html.erb
|
|
195
|
+
- app/views/hr_lite/admin/statutory_rate_cards/new.html.erb
|
|
179
196
|
- app/views/hr_lite/appraisals/index.html.erb
|
|
180
197
|
- app/views/hr_lite/appraisals/show.html.erb
|
|
181
198
|
- app/views/hr_lite/attendance/_month_grid.html.erb
|
|
@@ -243,6 +260,9 @@ files:
|
|
|
243
260
|
- db/migrate/20260807184426_add_fy_opening_to_hr_lite_employee_profiles.rb
|
|
244
261
|
- db/migrate/20260807184939_add_out_of_window_days_to_hr_lite_salary_slips.rb
|
|
245
262
|
- db/migrate/20260817182124_add_status_constraints_and_appraisal_fk_to_hr_lite.rb
|
|
263
|
+
- db/migrate/20260817183347_create_hr_lite_roles_and_grants.rb
|
|
264
|
+
- db/migrate/20260817183629_seed_hr_lite_roles_from_email_tiers.rb
|
|
265
|
+
- db/migrate/20260817190159_create_hr_lite_statutory_rate_cards.rb
|
|
246
266
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
247
267
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
248
268
|
- lib/generators/hr_lite/install/templates/initializer.rb
|
|
@@ -257,8 +277,11 @@ files:
|
|
|
257
277
|
- lib/hr_lite/mention_parser.rb
|
|
258
278
|
- lib/hr_lite/money.rb
|
|
259
279
|
- lib/hr_lite/notifications.rb
|
|
280
|
+
- lib/hr_lite/permissions.rb
|
|
281
|
+
- lib/hr_lite/role_seeds.rb
|
|
260
282
|
- lib/hr_lite/seeds.rb
|
|
261
283
|
- lib/hr_lite/statutory_rate_card.rb
|
|
284
|
+
- lib/hr_lite/statutory_seeds.rb
|
|
262
285
|
- lib/hr_lite/version.rb
|
|
263
286
|
- lib/tasks/hr_lite_tasks.rake
|
|
264
287
|
homepage: https://github.com/kshtzkr/hr_lite
|