hr_lite 0.11.0 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 655b4d27557dace6262528eb82af6912b4b7dfcdc3ff6b8274068876b8bb970c
4
- data.tar.gz: ba15eee422c61704681db9d8d1194851f8c761459524844ade6e4cd054776f81
3
+ metadata.gz: a035599be75b81c0d2f3d1fc1b8f95328df845445f6c93d997a9906f9b43846e
4
+ data.tar.gz: 8a840cc6c1af2ef49af660859fc6edf0f2ccec4b59377017c9cc0467793e8396
5
5
  SHA512:
6
- metadata.gz: 4b4224f36f2cd4b3c256d440e6f439f59ce86e61436545f4f5a1afab97d827c64db86b4ee3b21f72003ea8864010b0369d6a719cd833a7194855adc1af10602c
7
- data.tar.gz: 3de774d902610ab761c5794fe4f9eec3c404dac26ccf0fbea553bfe6bafe09e45c576e8264b07c70b6eef07af3f93f54c88aa43d3fd149921d4726456823a890
6
+ metadata.gz: 9bef92ea728cc960bc548c37fe3a8a34090ad531bec3ab248dc572ce9931f44fb5bf2591b1578df0c22f68ffd5ab7c02e6e7a8ed43445b1087524fc59584889b
7
+ data.tar.gz: 2598075e1f67e8e2c9e7fa4142db5e7561f42f17e32a9be6207ca560116a8e9d3ac7b7876c02e4bc77c250df6ef0ec0b05592ad9c073b22aaa8f01158129276f
data/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-08-18
11
+
12
+ The four things an employee had to leave the system to do. **Adds one
13
+ migration.**
14
+
15
+ ### Added
16
+
17
+ - **Expenses**, with categories carrying a monthly cap and a receipt rule.
18
+ The cap is checked when the claim is MADE, not at approval — telling
19
+ somebody they are over it after a week of waiting is the wrong moment to
20
+ find out. A claim awaiting a decision counts against the cap; a rejected
21
+ one gives the room back.
22
+ - Expenses route through the approval engine from 0.9.0 rather than growing a
23
+ fifth approve/reject. Configure a flow and a claim gets manager-then-finance
24
+ for free. `reimbursed` is a separate state from `approved`, because agreeing
25
+ to pay somebody and paying them are different days and the person waiting
26
+ cares about the second one.
27
+ - **Benefits and enrolments.** Dependants are a COUNT, not a list — names and
28
+ dates of birth of somebody's family are data this engine has no reason to
29
+ hold, and holding them would mean protecting them.
30
+ - **An HR help desk**: raise, assign, resolve, with the answer going back to
31
+ the person who asked instead of dying in somebody's inbox.
32
+ - **Policies, versioned.** Re-issuing asks everybody again, because an
33
+ acknowledgement of v1 says nothing about v2 — that is the whole point of
34
+ versioning it. Acknowledgements are `readonly?` once written: evidence that
35
+ can be edited afterwards is not evidence. Clicking twice is not an error.
36
+ - Permissions for all four, granted to the roles that should hold them, and
37
+ nine notification events.
38
+
39
+ ### Changed
40
+
41
+ - `HrLite::Expense` is the second module on the approval engine, which is the
42
+ first real test of the claim made in 0.9.0 that a new module gets
43
+ multi-level approval without touching the engine. It did.
44
+
10
45
  ## [0.11.0] - 2026-08-18
11
46
 
12
47
  Documents, and a tax declaration that is more than one number. **Adds one
@@ -694,7 +729,8 @@ Initial release.
694
729
  bus with per-event channel matrix (bell, email, leadership email/bell),
695
730
  daily leadership digest and an append-only audit trail.
696
731
 
697
- [Unreleased]: https://github.com/kshtzkr/hr_lite/compare/v0.11.0...HEAD
732
+ [Unreleased]: https://github.com/kshtzkr/hr_lite/compare/v0.12.0...HEAD
733
+ [0.12.0]: https://github.com/kshtzkr/hr_lite/compare/v0.11.0...v0.12.0
698
734
  [0.11.0]: https://github.com/kshtzkr/hr_lite/compare/v0.10.0...v0.11.0
699
735
  [0.10.0]: https://github.com/kshtzkr/hr_lite/compare/v0.9.0...v0.10.0
700
736
  [0.9.0]: https://github.com/kshtzkr/hr_lite/compare/v0.8.0...v0.9.0
@@ -22,7 +22,7 @@ module HrLite
22
22
  def self.approvable_types
23
23
  %w[
24
24
  HrLite::LeaveRequest HrLite::CompOffRequest
25
- HrLite::RegularizationRequest HrLite::Resignation
25
+ HrLite::RegularizationRequest HrLite::Resignation HrLite::Expense
26
26
  ].freeze
27
27
  end
28
28
 
@@ -0,0 +1,37 @@
1
+ module HrLite
2
+ # An insurance policy or other benefit the company provides. Employees can
3
+ # finally see what they are covered for without emailing somebody.
4
+ class Benefit < ApplicationRecord
5
+ include EncryptedMoney
6
+ include Audited
7
+
8
+ KINDS = %w[health life accident other].freeze
9
+
10
+ encrypted_money :coverage, :employer_premium, :employee_premium
11
+
12
+ has_many :benefit_enrolments, class_name: "HrLite::BenefitEnrolment", dependent: :destroy
13
+ has_many :enrolled_users, through: :benefit_enrolments, source: :user
14
+
15
+ validates :name, presence: true
16
+ validates :kind, inclusion: { in: KINDS }
17
+ validate :expiry_is_after_the_start
18
+
19
+ scope :active, -> { where(active: true) }
20
+ scope :live_on, ->(date) {
21
+ active.where("effective_from IS NULL OR effective_from <= ?", date)
22
+ .where("expires_on IS NULL OR expires_on >= ?", date)
23
+ }
24
+
25
+ def expiring?(within: 30, on: Date.current)
26
+ expires_on.present? && expires_on <= on + within && expires_on >= on
27
+ end
28
+
29
+ private
30
+
31
+ def expiry_is_after_the_start
32
+ return if effective_from.nil? || expires_on.nil? || expires_on >= effective_from
33
+
34
+ errors.add(:expires_on, "must be on or after the start date")
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ module HrLite
2
+ # Who is covered, from when. Dependants are a COUNT, not a list — names and
3
+ # dates of birth of somebody's family are data this engine has no reason to
4
+ # hold, and holding them would mean protecting them.
5
+ class BenefitEnrolment < ApplicationRecord
6
+ include Audited
7
+
8
+ belongs_to :benefit, class_name: "HrLite::Benefit"
9
+ belongs_to :user, class_name: HrLite.config.user_class
10
+
11
+ validates :enrolled_on, presence: true
12
+ validates :user_id, uniqueness: { scope: :benefit_id }
13
+ validates :dependants, numericality: { greater_than_or_equal_to: 0 }
14
+ validate :ended_after_it_started
15
+
16
+ scope :live_on, ->(date) {
17
+ where(enrolled_on: ..date).where("ended_on IS NULL OR ended_on >= ?", date)
18
+ }
19
+
20
+ def live?(on = Date.current) = enrolled_on <= on && (ended_on.nil? || ended_on >= on)
21
+
22
+ private
23
+
24
+ def ended_after_it_started
25
+ return if ended_on.nil? || enrolled_on.nil? || ended_on >= enrolled_on
26
+
27
+ errors.add(:ended_on, "must be on or after the enrolment date")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,142 @@
1
+ module HrLite
2
+ # A claim. Routed through the approval engine rather than growing a fifth
3
+ # approve/reject of its own — configure a flow for it and it gets manager
4
+ # then finance for free.
5
+ class Expense < ApplicationRecord
6
+ include EncryptedMoney
7
+ include Audited
8
+ include Approvable
9
+
10
+ STATUSES = %w[draft submitted approved rejected reimbursed cancelled].freeze
11
+
12
+ encrypted_money :amount
13
+
14
+ belongs_to :category, class_name: "HrLite::ExpenseCategory"
15
+ belongs_to :user, class_name: HrLite.config.user_class
16
+
17
+ has_one_attached :receipt
18
+
19
+ validates :status, inclusion: { in: STATUSES }
20
+ validates :description, presence: true
21
+ validates :spent_on, presence: true
22
+ validate :amount_is_positive
23
+ validate :spent_on_is_not_in_the_future
24
+ validate :within_the_category_cap, on: :create
25
+ validate :receipt_is_present_when_the_category_demands_one
26
+
27
+ scope :awaiting_reimbursement, -> { where(status: "approved") }
28
+ scope :recent_first, -> { order(spent_on: :desc, id: :desc) }
29
+
30
+ STATUSES.each { |s| define_method("#{s}?") { status == s } }
31
+
32
+ def submit!(actor:)
33
+ raise ActiveRecord::RecordInvalid.new(self), "not a draft" unless draft? || rejected?
34
+
35
+ update!(status: "submitted", submitted_at: Time.current)
36
+ notify_deciders
37
+ true
38
+ end
39
+
40
+ def approve!(actor:, note: nil)
41
+ return record_routed!(actor, note, :approved) if awaiting?(actor)
42
+
43
+ settle!("approved", actor, note)
44
+ end
45
+
46
+ def reject!(actor:, note:)
47
+ raise ArgumentError, "a rejection needs a reason" if note.blank?
48
+ return record_routed!(actor, note, :rejected) if awaiting?(actor)
49
+
50
+ settle!("rejected", actor, note)
51
+ end
52
+
53
+ # Paid out with a payroll month. Kept separate from `approved` because
54
+ # agreeing to pay somebody and actually paying them are different days,
55
+ # and the person waiting cares about the second one.
56
+ def reimburse!(actor:, period_month:)
57
+ raise ActiveRecord::RecordInvalid.new(self), "not approved" unless approved?
58
+
59
+ transaction do
60
+ update!(status: "reimbursed", reimbursed_in: period_month.beginning_of_month)
61
+ AuditLog.record!(action: "expense.reimbursed", subject: self, actor: actor,
62
+ changes: { "employee" => HrLite.display_name(user),
63
+ "period" => period_month.strftime("%B %Y") })
64
+ end
65
+ Notifications.publish(
66
+ "expense.reimbursed",
67
+ title: "Your #{category.name} claim was reimbursed with #{period_month.strftime('%B %Y')} payroll",
68
+ path: "/expenses", bell_to: [ user ], email_to: [ user ]
69
+ )
70
+ true
71
+ end
72
+
73
+ private
74
+
75
+ def record_routed!(actor, note, intent)
76
+ approval = approval_for(actor)
77
+ result = approval_route.decide!(approval, status: intent.to_s, actor: actor, note: note)
78
+
79
+ case result.outcome
80
+ when :approved then settle!("approved", actor, note)
81
+ # `reject!` already refuses a blank note, so there is nothing to
82
+ # default to here.
83
+ when :rejected then settle!("rejected", actor, note)
84
+ else
85
+ notify_deciders
86
+ true
87
+ end
88
+ end
89
+
90
+ def settle!(new_status, actor, note)
91
+ update!(status: new_status, decision_note: note.presence)
92
+ Notifications.publish(
93
+ new_status == "approved" ? "expense.approved" : "expense.rejected",
94
+ title: "Your #{category.name} claim was #{new_status}",
95
+ body: note.presence, path: "/expenses", bell_to: [ user ], email_to: [ user ]
96
+ )
97
+ true
98
+ end
99
+
100
+ def notify_deciders
101
+ deciders = pending_approvals.map(&:approver).compact.uniq
102
+ deciders = HrLite.users_holding("expense.approve").to_a if deciders.empty?
103
+ return if deciders.empty?
104
+
105
+ Notifications.publish(
106
+ "expense.submitted",
107
+ title: "#{HrLite.display_name(user)} claimed #{Money.format(amount)} — #{category.name}",
108
+ body: description, path: "/admin/expenses", bell_to: deciders
109
+ )
110
+ end
111
+
112
+ def amount_is_positive
113
+ errors.add(:amount, "must be more than zero") unless amount&.positive?
114
+ end
115
+
116
+ def spent_on_is_not_in_the_future
117
+ return if spent_on.nil? || spent_on <= Date.current
118
+
119
+ errors.add(:spent_on, "cannot be in the future")
120
+ end
121
+
122
+ # Checked at claim time rather than at approval: telling somebody they
123
+ # are over the cap after they have waited a week for an answer is the
124
+ # wrong moment to find out.
125
+ def within_the_category_cap
126
+ return if category.nil? || amount.nil? || spent_on.nil?
127
+
128
+ remaining = category.remaining_for(user, spent_on.beginning_of_month)
129
+ return if remaining.nil? || amount <= remaining
130
+
131
+ errors.add(:amount, "is over the #{category.name} cap for #{spent_on.strftime('%B')} " \
132
+ "— #{Money.format(remaining)} left")
133
+ end
134
+
135
+ def receipt_is_present_when_the_category_demands_one
136
+ return unless category&.receipt_required
137
+ return if receipt.attached?
138
+
139
+ errors.add(:receipt, "is required for #{category.name}")
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,33 @@
1
+ module HrLite
2
+ # "Travel", "Client entertainment", "Home office" — with the cap and the
3
+ # receipt rule that go with each.
4
+ class ExpenseCategory < ApplicationRecord
5
+ include EncryptedMoney
6
+ include Audited
7
+
8
+ encrypted_money :monthly_cap
9
+
10
+ has_many :expenses, class_name: "HrLite::Expense", foreign_key: :category_id,
11
+ dependent: :restrict_with_error
12
+
13
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
14
+
15
+ scope :active, -> { where(active: true) }
16
+ scope :alphabetical, -> { order(:name) }
17
+
18
+ def uncapped? = monthly_cap.nil?
19
+
20
+ # What is left of this month's cap for one person, or nil when uncapped.
21
+ # Counts everything not refused: a claim awaiting a decision has still
22
+ # been spent against the cap.
23
+ def remaining_for(user, month = Date.current.beginning_of_month)
24
+ return nil if uncapped?
25
+
26
+ spent = expenses.where(user_id: user.id)
27
+ .where(spent_on: month..month.end_of_month)
28
+ .where.not(status: %w[rejected cancelled])
29
+ .sum(BigDecimal(0)) { |e| e.amount || BigDecimal(0) }
30
+ [ monthly_cap - spent, BigDecimal(0) ].max
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,75 @@
1
+ module HrLite
2
+ # "Can I have a salary certificate?" — the questions that were going to
3
+ # somebody's inbox and getting lost there.
4
+ class HrRequest < ApplicationRecord
5
+ include Audited
6
+
7
+ STATUSES = %w[open in_progress resolved closed cancelled].freeze
8
+
9
+ CATEGORIES = %w[
10
+ salary_certificate employment_certificate address_change bank_change
11
+ document_request payroll_query tax_query insurance_query policy_query other
12
+ ].freeze
13
+
14
+ belongs_to :user, class_name: HrLite.config.user_class
15
+ belongs_to :assigned_to, class_name: HrLite.config.user_class, optional: true
16
+
17
+ validates :subject, presence: true
18
+ validates :category, inclusion: { in: CATEGORIES }
19
+ validates :status, inclusion: { in: STATUSES }
20
+
21
+ scope :open_requests, -> { where(status: %w[open in_progress]) }
22
+ scope :recent_first, -> { order(created_at: :desc) }
23
+
24
+ STATUSES.each { |s| define_method("#{s}?") { status == s } }
25
+
26
+ after_create_commit :notify_desk
27
+
28
+ def category_label = category.humanize
29
+
30
+ def assign!(actor:, assignee:)
31
+ update!(assigned_to_id: assignee.id, status: "in_progress")
32
+ Notifications.publish(
33
+ "hr_request.assigned",
34
+ title: "#{category_label}: #{subject}",
35
+ body: "Assigned to you by #{HrLite.display_name(actor)}.",
36
+ path: "/admin/hr_requests/#{id}", bell_to: [ assignee ]
37
+ )
38
+ true
39
+ end
40
+
41
+ def resolve!(actor:, resolution:)
42
+ raise ArgumentError, "a resolution needs an answer" if resolution.blank?
43
+
44
+ update!(status: "resolved", resolution: resolution, resolved_at: Time.current,
45
+ assigned_to_id: assigned_to_id || actor.id)
46
+ Notifications.publish(
47
+ "hr_request.resolved",
48
+ title: "Your request was answered — #{subject}",
49
+ body: resolution, path: "/hr_requests/#{id}",
50
+ bell_to: [ user ], email_to: [ user ]
51
+ )
52
+ true
53
+ end
54
+
55
+ def cancel!(actor:)
56
+ raise ActiveRecord::RecordInvalid.new(self), "already settled" unless open? || in_progress?
57
+
58
+ update!(status: "cancelled")
59
+ true
60
+ end
61
+
62
+ private
63
+
64
+ def notify_desk
65
+ desk = HrLite.users_holding("hr_request.manage").to_a
66
+ return if desk.empty?
67
+
68
+ Notifications.publish(
69
+ "hr_request.raised",
70
+ title: "#{HrLite.display_name(user)} asked: #{subject}",
71
+ body: body.presence, path: "/admin/hr_requests/#{id}", bell_to: desk
72
+ )
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,59 @@
1
+ module HrLite
2
+ # A written policy people are held to. Versioned, because "you agreed to
3
+ # this" is only true of the version somebody actually read.
4
+ class Policy < ApplicationRecord
5
+ include Audited
6
+
7
+ has_many :policy_acknowledgements, class_name: "HrLite::PolicyAcknowledgement",
8
+ dependent: :destroy
9
+
10
+ validates :title, presence: true, uniqueness: { scope: :version }
11
+ validates :body, presence: true
12
+ validates :effective_from, presence: true
13
+ validates :version, numericality: { greater_than: 0 }
14
+
15
+ scope :published, -> { where(published: true) }
16
+ scope :live_on, ->(date) { published.where(effective_from: ..date) }
17
+ scope :newest_first, -> { order(effective_from: :desc, version: :desc) }
18
+
19
+ # The version in force today for each title — an older one stays for the
20
+ # acknowledgements attached to it, but nobody should be reading it.
21
+ def self.current
22
+ live_on(Date.current).group_by(&:title).values.map do |versions|
23
+ versions.max_by(&:version)
24
+ end
25
+ end
26
+
27
+ # Re-issuing asks everybody again, which is the whole point of versioning
28
+ # it: an acknowledgement of v1 says nothing about v2.
29
+ def supersede!(body:, effective_from: Date.current, acknowledgement_required: nil)
30
+ self.class.create!(
31
+ title: title, body: body, version: version + 1, effective_from: effective_from,
32
+ acknowledgement_required: acknowledgement_required.nil? ? self.acknowledgement_required : acknowledgement_required,
33
+ published: true
34
+ )
35
+ end
36
+
37
+ def acknowledged_by?(user)
38
+ policy_acknowledgements.exists?(user_id: user.id)
39
+ end
40
+
41
+ def outstanding_for
42
+ return HrLite.user_klass.none unless acknowledgement_required && published
43
+
44
+ HrLite.user_klass.where(id: HrLite.active_employees.map(&:id))
45
+ .where.not(id: policy_acknowledgements.select(:user_id))
46
+ end
47
+
48
+ # Clicking twice is not an error and must not read as one. The
49
+ # find_or_create handles the second click; the rescue handles two
50
+ # requests arriving at once, which the unique index refuses.
51
+ def acknowledge!(user)
52
+ policy_acknowledgements.find_or_create_by!(user_id: user.id) do |ack|
53
+ ack.acknowledged_at = Time.current
54
+ end
55
+ rescue ActiveRecord::RecordNotUnique
56
+ policy_acknowledgements.find_by!(user_id: user.id)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ module HrLite
2
+ # "I have read this." Kept per VERSION, and never edited — it is the
3
+ # evidence, and evidence that can be changed afterwards is not evidence.
4
+ class PolicyAcknowledgement < ApplicationRecord
5
+ belongs_to :policy, class_name: "HrLite::Policy"
6
+ belongs_to :user, class_name: HrLite.config.user_class
7
+
8
+ validates :acknowledged_at, presence: true
9
+ validates :user_id, uniqueness: { scope: :policy_id }
10
+
11
+ def readonly? = persisted?
12
+ end
13
+ end
@@ -0,0 +1,108 @@
1
+ class CreateHrLiteEmployeeServices < ActiveRecord::Migration[8.1]
2
+ # Four things an employee has to leave the system to do today: claim an
3
+ # expense, see what insurance they have, ask HR a question, and read the
4
+ # policy they are being held to.
5
+ #
6
+ # Expenses and requests route through the approval engine from 0.9.0
7
+ # rather than growing a fifth and sixth approve/reject of their own.
8
+ def change
9
+ create_table :hr_lite_expense_categories do |t|
10
+ t.string :name, null: false
11
+ t.text :monthly_cap # encrypted; null = uncapped
12
+ t.boolean :receipt_required, null: false, default: true
13
+ t.boolean :active, null: false, default: true
14
+ t.timestamps
15
+ end
16
+ add_index :hr_lite_expense_categories, :name, unique: true
17
+
18
+ create_table :hr_lite_expenses do |t|
19
+ t.bigint :user_id, null: false
20
+ t.references :category, null: false, index: true,
21
+ foreign_key: { to_table: :hr_lite_expense_categories }
22
+ t.text :amount, null: false # encrypted
23
+ t.date :spent_on, null: false
24
+ t.string :description, null: false
25
+ # draft | submitted | approved | rejected | reimbursed | cancelled
26
+ t.string :status, null: false, default: "draft"
27
+ t.datetime :submitted_at
28
+ t.string :decision_note
29
+ # Which payroll month paid it out, once it was reimbursed.
30
+ t.date :reimbursed_in
31
+ t.timestamps
32
+ end
33
+ add_index :hr_lite_expenses, %i[user_id status]
34
+ add_check_constraint :hr_lite_expenses,
35
+ "status IN ('draft', 'submitted', 'approved', 'rejected', 'reimbursed', 'cancelled')",
36
+ name: "hr_lite_expenses_status_check"
37
+
38
+ create_table :hr_lite_benefits do |t|
39
+ t.string :name, null: false
40
+ # health | life | accident | other
41
+ t.string :kind, null: false, default: "health"
42
+ t.string :provider
43
+ t.string :policy_number
44
+ t.text :coverage # encrypted — sum assured
45
+ t.text :employer_premium # encrypted
46
+ t.text :employee_premium # encrypted
47
+ t.date :effective_from
48
+ t.date :expires_on
49
+ t.text :notes
50
+ t.boolean :active, null: false, default: true
51
+ t.timestamps
52
+ end
53
+
54
+ create_table :hr_lite_benefit_enrolments do |t|
55
+ t.references :benefit, null: false, index: true,
56
+ foreign_key: { to_table: :hr_lite_benefits, on_delete: :cascade }
57
+ t.bigint :user_id, null: false
58
+ t.date :enrolled_on, null: false
59
+ t.date :ended_on
60
+ # Dependants covered, as a count — names and dates of birth are the
61
+ # kind of family data this engine has no reason to hold.
62
+ t.integer :dependants, null: false, default: 0
63
+ t.timestamps
64
+ end
65
+ add_index :hr_lite_benefit_enrolments, %i[benefit_id user_id], unique: true
66
+
67
+ create_table :hr_lite_hr_requests do |t|
68
+ t.bigint :user_id, null: false
69
+ # salary_certificate | address_change | bank_change | payroll_query | ...
70
+ t.string :category, null: false
71
+ t.string :subject, null: false
72
+ t.text :body
73
+ # open | in_progress | resolved | closed | cancelled
74
+ t.string :status, null: false, default: "open"
75
+ t.bigint :assigned_to_id
76
+ t.datetime :resolved_at
77
+ t.text :resolution
78
+ t.timestamps
79
+ end
80
+ add_index :hr_lite_hr_requests, %i[user_id status]
81
+ add_index :hr_lite_hr_requests, :assigned_to_id
82
+ add_check_constraint :hr_lite_hr_requests,
83
+ "status IN ('open', 'in_progress', 'resolved', 'closed', 'cancelled')",
84
+ name: "hr_lite_hr_requests_status_check"
85
+
86
+ create_table :hr_lite_policies do |t|
87
+ t.string :title, null: false
88
+ t.text :body, null: false
89
+ t.integer :version, null: false, default: 1
90
+ t.date :effective_from, null: false
91
+ t.boolean :acknowledgement_required, null: false, default: false
92
+ t.boolean :published, null: false, default: false
93
+ t.timestamps
94
+ end
95
+ add_index :hr_lite_policies, %i[title version], unique: true
96
+
97
+ create_table :hr_lite_policy_acknowledgements do |t|
98
+ t.references :policy, null: false, index: true,
99
+ foreign_key: { to_table: :hr_lite_policies, on_delete: :cascade }
100
+ t.bigint :user_id, null: false
101
+ t.datetime :acknowledged_at, null: false
102
+ t.timestamps
103
+ end
104
+ # One acknowledgement per person per VERSION: re-issuing a policy has to
105
+ # ask everybody again, which is the entire point of versioning it.
106
+ add_index :hr_lite_policy_acknowledgements, %i[policy_id user_id], unique: true
107
+ end
108
+ end
@@ -34,6 +34,14 @@ module HrLite
34
34
  # A document about to lapse — a passport or a visa is somebody's right
35
35
  # to work, and finding out late is the whole problem.
36
36
  "document.expiring" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
37
+ "expense.submitted" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
38
+ "expense.approved" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
39
+ "expense.rejected" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
40
+ "expense.reimbursed" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
41
+ "hr_request.raised" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
42
+ "hr_request.assigned" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
43
+ "hr_request.resolved" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
44
+ "policy.published" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
37
45
  "digest.daily" => { bell: false, email: false, leadership_email: true, leadership_bell: false },
38
46
  "resignation.submitted" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
39
47
  "resignation.accepted" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
@@ -47,6 +47,15 @@ module HrLite
47
47
  "document.manage" => [ "Documents", "Upload, verify and open identity and bank documents" ],
48
48
  "tax.view" => [ "Payroll", "See tax declarations" ],
49
49
  "tax.manage" => [ "Payroll", "Verify tax declarations and the proof behind them" ],
50
+ "expense.claim" => [ "Expenses", "Claim expenses" ],
51
+ "expense.approve" => [ "Expenses", "Approve and reject expense claims" ],
52
+ "expense.reimburse" => [ "Expenses", "Mark claims reimbursed with a payroll month" ],
53
+ "benefit.view" => [ "Benefits", "See benefits and who is enrolled" ],
54
+ "benefit.manage" => [ "Benefits", "Add benefits and enrol people" ],
55
+ "hr_request.raise" => [ "Help desk", "Raise a request with HR" ],
56
+ "hr_request.manage" => [ "Help desk", "Answer and assign HR requests" ],
57
+ "policy.view" => [ "Policies", "Read published policies" ],
58
+ "policy.manage" => [ "Policies", "Write, publish and re-issue policies" ],
50
59
  "settings.manage" => [ "Administration", "Change company settings, offices and policy" ],
51
60
  "audit.view" => [ "Administration", "Read the audit trail" ],
52
61
  "audit.view_money" => [ "Administration", "Read audit rows about pay, appraisals and promotions" ],
@@ -17,7 +17,9 @@ 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", "document.view" => "self", "tax.view" => "self"
20
+ "resignation.view" => "self", "document.view" => "self", "tax.view" => "self",
21
+ "expense.claim" => "self", "benefit.view" => "self",
22
+ "hr_request.raise" => "self", "policy.view" => "all"
21
23
  }
22
24
  },
23
25
  Role::MANAGER => {
@@ -36,7 +38,9 @@ module HrLite
36
38
  "leave.manage" => "all", "attendance.view" => "all", "attendance.manage" => "all",
37
39
  "profile.view" => "all", "payroll.view" => "self",
38
40
  "appraisal.view" => "self", "resignation.view" => "all",
39
- "document.view" => "all", "tax.view" => "self"
41
+ "document.view" => "all", "tax.view" => "self",
42
+ "expense.claim" => "self", "benefit.view" => "all", "benefit.manage" => "all",
43
+ "hr_request.raise" => "self", "hr_request.manage" => "all", "policy.view" => "all"
40
44
  }
41
45
  },
42
46
  Role::FINANCE => {
@@ -47,6 +51,8 @@ module HrLite
47
51
  "payroll.view" => "all", "payroll.manage" => "all", "payroll.export" => "all",
48
52
  "salary.view" => "all", "salary.manage" => "all",
49
53
  "tax.view" => "all", "tax.manage" => "all",
54
+ "expense.claim" => "self", "expense.approve" => "all", "expense.reimburse" => "all",
55
+ "policy.view" => "all",
50
56
  "audit.view" => "all", "audit.view_money" => "all"
51
57
  }
52
58
  },
@@ -58,7 +64,8 @@ module HrLite
58
64
  "profile.view" => "all", "profile.manage" => "all",
59
65
  "resignation.view" => "all", "resignation.manage" => "all",
60
66
  "settings.manage" => "all", "audit.view" => "all", "payroll.view" => "self",
61
- "document.view" => "all"
67
+ "document.view" => "all", "expense.approve" => "all", "benefit.manage" => "all",
68
+ "hr_request.manage" => "all", "policy.view" => "all", "policy.manage" => "all"
62
69
  }
63
70
  },
64
71
  Role::SUPER_ADMIN => {
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.11.0"
2
+ VERSION = "0.12.0"
3
3
  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.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -118,11 +118,16 @@ files:
118
118
  - app/models/hr_lite/approval_step.rb
119
119
  - app/models/hr_lite/attendance_record.rb
120
120
  - app/models/hr_lite/audit_log.rb
121
+ - app/models/hr_lite/benefit.rb
122
+ - app/models/hr_lite/benefit_enrolment.rb
121
123
  - app/models/hr_lite/comp_off_request.rb
122
124
  - app/models/hr_lite/designation_change.rb
123
125
  - app/models/hr_lite/document.rb
124
126
  - app/models/hr_lite/employee_profile.rb
127
+ - app/models/hr_lite/expense.rb
128
+ - app/models/hr_lite/expense_category.rb
125
129
  - app/models/hr_lite/holiday.rb
130
+ - app/models/hr_lite/hr_request.rb
126
131
  - app/models/hr_lite/kudo.rb
127
132
  - app/models/hr_lite/kudo_mention.rb
128
133
  - app/models/hr_lite/leave_balance.rb
@@ -133,6 +138,8 @@ files:
133
138
  - app/models/hr_lite/office_location.rb
134
139
  - app/models/hr_lite/payroll_line_item.rb
135
140
  - app/models/hr_lite/payroll_run.rb
141
+ - app/models/hr_lite/policy.rb
142
+ - app/models/hr_lite/policy_acknowledgement.rb
136
143
  - app/models/hr_lite/professional_tax_slab.rb
137
144
  - app/models/hr_lite/regularization_request.rb
138
145
  - app/models/hr_lite/resignation.rb
@@ -283,6 +290,7 @@ files:
283
290
  - db/migrate/20260818001151_create_hr_lite_approvals.rb
284
291
  - db/migrate/20260818002627_create_hr_lite_payroll_components_and_loans.rb
285
292
  - db/migrate/20260818003616_create_hr_lite_documents_and_declarations.rb
293
+ - db/migrate/20260818004707_create_hr_lite_employee_services.rb
286
294
  - lib/generators/hr_lite/install/install_generator.rb
287
295
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
288
296
  - lib/generators/hr_lite/install/templates/initializer.rb