hr_lite 0.20.0 → 0.20.1
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 +33 -0
- data/app/controllers/hr_lite/admin/attendances_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/documents_controller.rb +2 -0
- data/app/controllers/hr_lite/admin/expenses_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/leave_balances_controller.rb +1 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +3 -2
- data/app/controllers/hr_lite/admin/overview_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +5 -1
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/reports_controller.rb +3 -1
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +1 -0
- data/app/models/hr_lite/audit_log.rb +1 -1
- data/app/models/hr_lite/document.rb +3 -0
- data/app/models/hr_lite/expense.rb +8 -0
- data/app/services/hr_lite/overview_query.rb +13 -7
- data/app/services/hr_lite/slip_builder.rb +2 -1
- data/lib/hr_lite/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d52aa888b9029219bfde72b9d7eabcae22691d9bb5ec73ec6a924191cdf8ac2
|
|
4
|
+
data.tar.gz: b1e9c215421a30a9c521059c90349db86276f0e74f3ac4c7eb4a34a05ca2cd2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 349fcb87fb86b6ee48633227123a83e712bdd5486cda707de5f3c70d8f2b4df941747610fbf5a817336ece750f4c3d28727559faff4997c840a1fd598be83499
|
|
7
|
+
data.tar.gz: a1c509e0a1189eeac70b67ab848ab04b3507ce85ce456d17396dca9419911187fd26021bbb997d779f07d6d51835e58c105dd4b52f0a7d3ee788899a3781ca9d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.20.1] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
Security fixes. No migration.
|
|
13
|
+
|
|
14
|
+
### Upgrading
|
|
15
|
+
|
|
16
|
+
Document numbers are now encrypted with Active Record Encryption. Rows
|
|
17
|
+
saved before this release are plain text, so the host needs
|
|
18
|
+
`config.active_record.encryption.support_unencrypted_data = true` to keep
|
|
19
|
+
reading them, and should re-save each `HrLite::Document` once to encrypt
|
|
20
|
+
the old numbers.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- **Nobody decides their own request.** A manager could approve their own
|
|
25
|
+
leave, comp-off and attendance fixes, Finance its own expense claims, and
|
|
26
|
+
anyone recording leave for someone else could record their own as
|
|
27
|
+
approved. HR could verify or reject its own documents. All refused now.
|
|
28
|
+
- **Nobody edits their own records from the admin screens.** HR rewriting
|
|
29
|
+
its own attendance, adjusting its own leave balance, or Finance verifying
|
|
30
|
+
its own tax declaration is refused.
|
|
31
|
+
- **Document numbers are encrypted.** Aadhaar, PAN and passport numbers
|
|
32
|
+
were stored in plain text and copied into audit diffs and the leadership
|
|
33
|
+
audit email. Loans join the money tier for audit mail.
|
|
34
|
+
- **A rejected tax declaration no longer lowers TDS.**
|
|
35
|
+
- **Payroll CSVs defuse formulas.** Register and report cells that open
|
|
36
|
+
with `=`, `+`, `-` or `@` are escaped, since names are staff-typed.
|
|
37
|
+
- **The payout register needs `payroll.export`**, not just
|
|
38
|
+
`payroll.manage`.
|
|
39
|
+
- **The admin overview respects reach.** A team-scoped manager sees only
|
|
40
|
+
the people they reach, not the whole company.
|
|
41
|
+
- **Expense receipts** take the same type and size rule as documents.
|
|
42
|
+
|
|
10
43
|
## [0.20.0] - 2026-10-01
|
|
11
44
|
|
|
12
45
|
Salary as CTC, and who is out today. No migration.
|
|
@@ -82,7 +82,8 @@ module HrLite
|
|
|
82
82
|
# authorities, so they resolve through different permissions — a role
|
|
83
83
|
# can be given the board without being given the pencil.
|
|
84
84
|
def find_visible_employee = employee_within("attendance.view")
|
|
85
|
-
|
|
85
|
+
# Nobody rewrites their own punches: team and company scope include the holder.
|
|
86
|
+
def find_manageable_employee = employee_within("attendance.manage").tap { |e| raise ActiveRecord::RecordNotFound if e.id == hr_current_user.id }
|
|
86
87
|
|
|
87
88
|
def employee_within(permission)
|
|
88
89
|
employee = HrLite.user_klass.find(params[:user_id])
|
|
@@ -40,8 +40,9 @@ module HrLite
|
|
|
40
40
|
# A manager decides for their own reports; HR decides for everyone.
|
|
41
41
|
# Scoped through the relation, so reaching for somebody else's report
|
|
42
42
|
# is a 404 rather than a 403 that confirms the person exists.
|
|
43
|
+
# Nobody decides their own request: team scope includes the approver.
|
|
43
44
|
def decidable
|
|
44
|
-
hr_scope(CompOffRequest.all, "leave.approve")
|
|
45
|
+
hr_scope(CompOffRequest.all, "leave.approve").where.not(user_id: hr_current_user.id)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def find_decidable = decidable.find(params[:id])
|
|
@@ -54,6 +54,8 @@ module HrLite
|
|
|
54
54
|
# whoever happens to run HR.
|
|
55
55
|
def find_manageable
|
|
56
56
|
document = Document.find(params[:id])
|
|
57
|
+
# readable_by? always admits the owner, and nobody verifies their own paperwork.
|
|
58
|
+
raise ActiveRecord::RecordNotFound if document.user_id == hr_current_user.id
|
|
57
59
|
raise ActiveRecord::RecordNotFound unless document.readable_by?(hr_current_user)
|
|
58
60
|
|
|
59
61
|
document
|
|
@@ -39,7 +39,8 @@ module HrLite
|
|
|
39
39
|
|
|
40
40
|
def require_deciding! = hr_require_permission!("expense.approve", scope: :team)
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
# Nobody approves their own claim: team scope includes the approver.
|
|
43
|
+
def decidable = hr_scope(Expense.all, "expense.approve").where.not(user_id: hr_current_user.id)
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
end
|
|
@@ -13,6 +13,7 @@ module HrLite
|
|
|
13
13
|
# A manager reaches this screen through leave.approve; changing
|
|
14
14
|
# somebody's quota is leave.manage, for THAT person.
|
|
15
15
|
hr_require_reach!("leave.manage", user)
|
|
16
|
+
raise ActiveRecord::RecordNotFound if user.id == hr_current_user.id # never one's own quota
|
|
16
17
|
type = LeaveType.find(params[:leave_type_id])
|
|
17
18
|
# Unsanitised, a missing param wrote the adjustment into leave year 0,
|
|
18
19
|
# where no screen can ever show it.
|
|
@@ -84,13 +84,14 @@ module HrLite
|
|
|
84
84
|
# somebody else's report gets a 404 through the scoped relation rather
|
|
85
85
|
# than a 403 — the same shape every employee-tier screen already uses,
|
|
86
86
|
# and it does not confirm that the other person exists.
|
|
87
|
+
# Nobody decides their own request: team scope includes the approver.
|
|
87
88
|
def decidable
|
|
88
|
-
hr_scope(LeaveRequest.all, "leave.approve")
|
|
89
|
+
hr_scope(LeaveRequest.all, "leave.approve").where.not(user_id: hr_current_user.id)
|
|
89
90
|
end
|
|
90
91
|
|
|
91
92
|
def find_decidable = decidable.find(params[:id])
|
|
92
93
|
|
|
93
|
-
def recordable = hr_scope(EmployeeProfile.includes(:user), "leave.approve")
|
|
94
|
+
def recordable = hr_scope(EmployeeProfile.includes(:user), "leave.approve").where.not(user_id: hr_current_user.id)
|
|
94
95
|
end
|
|
95
96
|
end
|
|
96
97
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module HrLite
|
|
2
2
|
module Admin
|
|
3
3
|
class PayrollRunsController < SuperadminController
|
|
4
|
+
# The register carries every bank account: its own key, not just payroll.manage.
|
|
5
|
+
before_action -> { hr_require_permission!("payroll.export", scope: :all) }, only: :register
|
|
4
6
|
def index
|
|
5
7
|
@runs = paginate(PayrollRun.recent_first)
|
|
6
8
|
end
|
|
@@ -82,6 +84,8 @@ module HrLite
|
|
|
82
84
|
|
|
83
85
|
def register_csv(run)
|
|
84
86
|
require "csv"
|
|
87
|
+
# A cell opening with = + - @ runs as a formula; names are staff-typed.
|
|
88
|
+
safe = ->(v) { v.is_a?(String) && v.match?(/\A[=+\-@\t\r]/) ? "'#{v}" : v }
|
|
85
89
|
CSV.generate do |csv|
|
|
86
90
|
csv << [ "Code", "Name", "Days", "LOP", "Gross", "PF", "ESI", "PT", "TDS",
|
|
87
91
|
"Net pay", "Bank", "Account", "IFSC" ]
|
|
@@ -95,7 +99,7 @@ module HrLite
|
|
|
95
99
|
deductions.dig("esi_employee", "amount") || 0, deductions.dig("pt", "amount") || 0,
|
|
96
100
|
deductions.dig("tds", "amount") || 0, slip.net_pay,
|
|
97
101
|
profile&.bank_name, profile&.bank_account_number, profile&.bank_ifsc
|
|
98
|
-
]
|
|
102
|
+
].map(&safe)
|
|
99
103
|
end
|
|
100
104
|
end
|
|
101
105
|
end
|
|
@@ -40,8 +40,9 @@ module HrLite
|
|
|
40
40
|
# A manager decides for their own reports; HR decides for everyone.
|
|
41
41
|
# Scoped through the relation, so reaching for somebody else's report
|
|
42
42
|
# is a 404 rather than a 403 that confirms the person exists.
|
|
43
|
+
# Nobody decides their own request: team scope includes the approver.
|
|
43
44
|
def decidable
|
|
44
|
-
hr_scope(RegularizationRequest.all, "attendance.manage")
|
|
45
|
+
hr_scope(RegularizationRequest.all, "attendance.manage").where.not(user_id: hr_current_user.id)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def find_decidable = decidable.find(params[:id])
|
|
@@ -141,9 +141,11 @@ module HrLite
|
|
|
141
141
|
require "csv"
|
|
142
142
|
return "" if rows.empty?
|
|
143
143
|
|
|
144
|
+
# A cell opening with = + - @ runs as a formula; names are staff-typed.
|
|
145
|
+
safe = ->(v) { v.is_a?(String) && v.match?(/\A[=+\-@\t\r]/) ? "'#{v}" : v }
|
|
144
146
|
CSV.generate do |csv|
|
|
145
147
|
csv << rows.first.keys
|
|
146
|
-
rows.each { |row| csv << row.values }
|
|
148
|
+
rows.each { |row| csv << row.values.map(&safe) }
|
|
147
149
|
end
|
|
148
150
|
end
|
|
149
151
|
end
|
|
@@ -62,6 +62,7 @@ module HrLite
|
|
|
62
62
|
def find_manageable
|
|
63
63
|
declaration = TaxDeclaration.find(params[:id])
|
|
64
64
|
hr_require_reach!("tax.manage", declaration.user)
|
|
65
|
+
raise ActiveRecord::RecordNotFound if declaration.user_id == hr_current_user.id # never one's own
|
|
65
66
|
declaration
|
|
66
67
|
end
|
|
67
68
|
|
|
@@ -14,7 +14,7 @@ module HrLite
|
|
|
14
14
|
# leadership audit screen or the policy.changed email.
|
|
15
15
|
MONEY_TIER_TYPES = %w[
|
|
16
16
|
HrLite::Appraisal HrLite::DesignationChange HrLite::SalaryStructure
|
|
17
|
-
HrLite::PayrollRun HrLite::SalarySlip HrLite::PayrollLineItem
|
|
17
|
+
HrLite::PayrollRun HrLite::SalarySlip HrLite::PayrollLineItem HrLite::Loan
|
|
18
18
|
].freeze
|
|
19
19
|
|
|
20
20
|
scope :recent, -> { order(created_at: :desc) }
|
|
@@ -5,6 +5,9 @@ module HrLite
|
|
|
5
5
|
class Document < ApplicationRecord
|
|
6
6
|
include Audited
|
|
7
7
|
|
|
8
|
+
# An Aadhaar, PAN or passport number: encrypted, which also redacts it from audit diffs and their emails.
|
|
9
|
+
encrypts :reference_number
|
|
10
|
+
|
|
8
11
|
VISIBILITIES = %w[self hr money].freeze
|
|
9
12
|
VERIFICATIONS = %w[pending verified rejected].freeze
|
|
10
13
|
|
|
@@ -23,6 +23,7 @@ module HrLite
|
|
|
23
23
|
validate :spent_on_is_not_in_the_future
|
|
24
24
|
validate :within_the_category_cap, on: :create
|
|
25
25
|
validate :receipt_is_present_when_the_category_demands_one
|
|
26
|
+
validate :receipt_is_a_small_pdf_or_image
|
|
26
27
|
|
|
27
28
|
scope :awaiting_reimbursement, -> { where(status: "approved") }
|
|
28
29
|
scope :recent_first, -> { order(spent_on: :desc, id: :desc) }
|
|
@@ -138,5 +139,12 @@ module HrLite
|
|
|
138
139
|
|
|
139
140
|
errors.add(:receipt, "is required for #{category.name}")
|
|
140
141
|
end
|
|
142
|
+
|
|
143
|
+
def receipt_is_a_small_pdf_or_image
|
|
144
|
+
return unless receipt.attached?
|
|
145
|
+
return errors.add(:receipt, "must be a PDF or an image") unless Document::ALLOWED_TYPES.include?(receipt.blob.content_type)
|
|
146
|
+
|
|
147
|
+
errors.add(:receipt, "must be under #{Document::MAX_BYTES / 1.megabyte} MB") if receipt.blob.byte_size > Document::MAX_BYTES
|
|
148
|
+
end
|
|
141
149
|
end
|
|
142
150
|
end
|
|
@@ -3,36 +3,38 @@ module HrLite
|
|
|
3
3
|
# one source, so they always agree. Sections return Relations (callers
|
|
4
4
|
# paginate or cap as they see fit).
|
|
5
5
|
class OverviewQuery
|
|
6
|
-
|
|
6
|
+
# user_ids nil = everyone; a team-scoped viewer sees only the people they reach.
|
|
7
|
+
def initialize(date: Date.current, user_ids: nil)
|
|
7
8
|
@date = date
|
|
9
|
+
@user_ids = user_ids
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def pending_requests
|
|
11
|
-
LeaveRequest.pending.includes(:leave_type, :user).order(:created_at)
|
|
13
|
+
reach(LeaveRequest.pending.includes(:leave_type, :user).order(:created_at))
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def on_leave_today
|
|
15
|
-
LeaveRequest.active_on(@date).includes(:leave_type, :user).order(:start_date)
|
|
17
|
+
reach(LeaveRequest.active_on(@date).includes(:leave_type, :user).order(:start_date))
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
# Comp-off and regularization are approvals too. Counting leave alone made
|
|
19
21
|
# the board announce "All present and accounted for" while both queues
|
|
20
22
|
# still had work in them.
|
|
21
23
|
def pending_comp_offs
|
|
22
|
-
CompOffRequest.pending.includes(:user).recent_first
|
|
24
|
+
reach(CompOffRequest.pending.includes(:user).recent_first)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def pending_regularizations
|
|
26
|
-
RegularizationRequest.pending.includes(:user).recent_first
|
|
28
|
+
reach(RegularizationRequest.pending.includes(:user).recent_first)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def flagged_today
|
|
30
|
-
AttendanceRecord.for_date(@date).flagged.includes(:user)
|
|
32
|
+
reach(AttendanceRecord.for_date(@date).flagged.includes(:user))
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# AttendanceCloseJob fills check_out_at, so the days it closed count too.
|
|
34
36
|
def missing_checkout_yesterday
|
|
35
|
-
yesterday = AttendanceRecord.for_date(@date - 1)
|
|
37
|
+
yesterday = reach(AttendanceRecord.for_date(@date - 1))
|
|
36
38
|
yesterday.missing_checkout.or(yesterday.where(regularization_note: AttendanceRecord::AUTO_CHECKOUT_NOTE))
|
|
37
39
|
.includes(:user)
|
|
38
40
|
end
|
|
@@ -49,5 +51,9 @@ module HrLite
|
|
|
49
51
|
def empty?
|
|
50
52
|
kpis.values.all?(&:zero?)
|
|
51
53
|
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def reach(relation) = @user_ids ? relation.where(user_id: @user_ids) : relation
|
|
52
58
|
end
|
|
53
59
|
end
|
|
@@ -135,7 +135,8 @@ module HrLite
|
|
|
135
135
|
def annual_deductions
|
|
136
136
|
declaration = TaxDeclaration.for(@user, @run.period_month)
|
|
137
137
|
return @profile.declared_annual_deductions if declaration.nil?
|
|
138
|
-
|
|
138
|
+
# A rejected declaration lowers nothing: its self-declared total stays editable and unchecked.
|
|
139
|
+
return @profile.declared_annual_deductions if declaration.draft? || declaration.rejected?
|
|
139
140
|
|
|
140
141
|
declaration.allowable_total
|
|
141
142
|
end
|
data/lib/hr_lite/version.rb
CHANGED