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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -1
  3. data/README.md +67 -31
  4. data/app/controllers/hr_lite/admin/attendances_controller.rb +18 -2
  5. data/app/controllers/hr_lite/admin/base_controller.rb +16 -6
  6. data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +16 -4
  7. data/app/controllers/hr_lite/admin/leadership_controller.rb +11 -8
  8. data/app/controllers/hr_lite/admin/leave_requests_controller.rb +21 -5
  9. data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +16 -4
  10. data/app/controllers/hr_lite/admin/role_assignments_controller.rb +63 -0
  11. data/app/controllers/hr_lite/admin/roles_controller.rb +73 -0
  12. data/app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb +97 -0
  13. data/app/controllers/hr_lite/admin/superadmin_controller.rb +15 -11
  14. data/app/controllers/hr_lite/application_controller.rb +39 -1
  15. data/app/helpers/hr_lite/application_helper.rb +6 -2
  16. data/app/models/hr_lite/professional_tax_slab.rb +41 -0
  17. data/app/models/hr_lite/role.rb +66 -0
  18. data/app/models/hr_lite/role_assignment.rb +18 -0
  19. data/app/models/hr_lite/role_grant.rb +15 -0
  20. data/app/models/hr_lite/statutory_rate_card_record.rb +95 -0
  21. data/app/services/hr_lite/access.rb +111 -0
  22. data/app/services/hr_lite/calculators/professional_tax.rb +34 -5
  23. data/app/services/hr_lite/payroll_run_processor.rb +17 -0
  24. data/app/views/hr_lite/admin/roles/_form.html.erb +47 -0
  25. data/app/views/hr_lite/admin/roles/edit.html.erb +53 -0
  26. data/app/views/hr_lite/admin/roles/index.html.erb +49 -0
  27. data/app/views/hr_lite/admin/roles/new.html.erb +3 -0
  28. data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +109 -0
  29. data/app/views/hr_lite/admin/statutory_rate_cards/edit.html.erb +4 -0
  30. data/app/views/hr_lite/admin/statutory_rate_cards/index.html.erb +59 -0
  31. data/app/views/hr_lite/admin/statutory_rate_cards/new.html.erb +4 -0
  32. data/config/routes.rb +4 -0
  33. data/db/migrate/20260817183347_create_hr_lite_roles_and_grants.rb +49 -0
  34. data/db/migrate/20260817183629_seed_hr_lite_roles_from_email_tiers.rb +82 -0
  35. data/db/migrate/20260817190159_create_hr_lite_statutory_rate_cards.rb +44 -0
  36. data/lib/generators/hr_lite/install/templates/initializer.rb +13 -7
  37. data/lib/hr_lite/configuration.rb +8 -1
  38. data/lib/hr_lite/current.rb +4 -0
  39. data/lib/hr_lite/permissions.rb +79 -0
  40. data/lib/hr_lite/role_seeds.rb +81 -0
  41. data/lib/hr_lite/statutory_rate_card.rb +55 -10
  42. data/lib/hr_lite/statutory_seeds.rb +71 -0
  43. data/lib/hr_lite/version.rb +1 -1
  44. data/lib/hr_lite.rb +64 -10
  45. data/lib/tasks/hr_lite_tasks.rake +18 -2
  46. 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
- # Config-driven statutory rates, keyed by effective date a budget change
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
- # VERIFY WITH A CA before the first run of any new financial year. Adding
7
- # a year is one new dated entry in CARDS; nothing else changes.
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
- # When no card exists for a run's financial year the lookup falls back to
10
- # the newest one it has payroll cannot simply stop every April — but
11
- # `warning_for` says so on the run, and `PayrollRunProcessor` puts that
12
- # sentence in front of every other warning until a card is added.
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 < CARDS.keys.min
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
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.5.3"
2
+ VERSION = "0.7.0"
3
3
  end
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.present? && !!config.admin_check.call(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.present? && !!config.leadership_check.call(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.present? && !!config.superadmin_check.call(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). Emails configured but absent from the user table are
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
- # Every domain event that bells the admins calls this. Loading and
73
- # instantiating the entire users table to run admin_check per row is fine
74
- # at ten staff and silly at a thousand — so it starts from employees_scope
75
- # (which a host narrows to real staff) rather than every row that exists.
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
- created = HrLite::Seeds.run!
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.5.3
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