hr_lite 0.25.0 → 0.27.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 +14 -0
- data/README.md +2 -0
- data/app/controllers/hr_lite/admin/ca_reviews_controller.rb +76 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +19 -0
- data/app/controllers/hr_lite/admin/salary_slips_controller.rb +4 -0
- data/app/helpers/hr_lite/application_helper.rb +2 -0
- data/app/jobs/hr_lite/ca_review_overdue_job.rb +19 -0
- data/app/jobs/hr_lite/ca_review_reminder_job.rb +21 -0
- data/app/models/hr_lite/audit_log.rb +1 -1
- data/app/models/hr_lite/ca_review.rb +66 -0
- data/app/models/hr_lite/leave_request.rb +18 -2
- data/app/models/hr_lite/role.rb +3 -0
- data/app/views/hr_lite/admin/ca_reviews/index.html.erb +94 -0
- data/app/views/hr_lite/admin/leave_requests/edit.html.erb +30 -0
- data/app/views/hr_lite/admin/leave_requests/show.html.erb +10 -0
- data/config/routes.rb +5 -1
- data/db/migrate/20261010110000_create_hr_lite_ca_reviews.rb +21 -0
- data/db/migrate/20261010110100_seed_hr_lite_ca_role.rb +14 -0
- data/lib/hr_lite/notifications.rb +3 -0
- data/lib/hr_lite/permissions.rb +1 -0
- data/lib/hr_lite/role_seeds.rb +6 -0
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +9 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d6e999438a679338ca82e170c4e71ac3519476913ce34b86d5783c3a0f97671
|
|
4
|
+
data.tar.gz: 9a3ef8a6d6a6451f0563ff0f99979460d958e3d47f594d0e6de3b916cc4e8b41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 953c47cc46554a8e953f0d125c5878d1d5b03344673f0e706d4c11718a410157dce3652cc3e0cbf414bd88989c13ae3c55f5d09b6fbc885ceb6447672a703583
|
|
7
|
+
data.tar.gz: ccaae3351ea83e10bb2b25c46966713a9cfad1a98909b0409ec286a9e64644293794675fb4c0ae5a9adac891a79546ac6929f996eb4a5f61c57a0f0b4d2c9cf7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.27.0] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
- HR and admins can fix leave they recorded (type, dates, half day) at any time, past dates included, without cancelling it. A reason is required; it is kept as the decision note and in the audit log (`leave.corrected`), and the balance and loss-of-pay split are worked out again.
|
|
13
|
+
|
|
14
|
+
## [0.26.0] - 2026-10-10
|
|
15
|
+
|
|
16
|
+
Two migrations: `hr_lite_ca_reviews`, and a data migration creating the CA role and giving Super Admin the new `payroll.verify` permission. Schedule `CaReviewReminderJob` weekly and `CaReviewOverdueJob` on 31 March.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- CA year-end review: a new **CA** role for an outside chartered accountant (reads every slip, salary structure and tax declaration with its proofs; verifies; nothing else). CA logins without an HR profile are not staff — `HrLite.employees` leaves them out, so attendance, payroll, team screens and broadcasts never count them.
|
|
20
|
+
- "CA review" (`/admin/ca_reviews`): progress for the financial year (slips and declarations verified, flags open), tabs for salary slips and tax declarations, filters (to review, flagged, verified, resolved), Verify, Flag with a required note, and "Verify all shown". A flag tells approvers and payroll (`ca.flagged`); they fix it the normal way and Mark resolved with a note. A published slip is never edited. `HrLite::CaReview`.
|
|
21
|
+
- `CaReviewReminderJob` (weekly, acts only in March) and `CaReviewOverdueJob` (31 March): what is left, to the CA reviewers and then to payroll and HR.
|
|
22
|
+
- A CA (`payroll.view`, all) can open any slip read-only.
|
|
23
|
+
|
|
10
24
|
## [0.25.0] - 2026-10-10
|
|
11
25
|
|
|
12
26
|
Three migrations: `senior` and `landlord_pan` (encrypted) on declaration lines; a data migration adding the FY 2026-27 and October 2026 rate cards to installs that already have cards; a data migration giving HR `tax.view` and `tax.manage` (all).
|
data/README.md
CHANGED
|
@@ -382,6 +382,8 @@ whatever you run). Each is idempotent and sends nothing on a quiet day.
|
|
|
382
382
|
|---|---|---|
|
|
383
383
|
| `HrLite::DailyDigestJob` | each morning | Leadership digest: who is out today, pending approvals, flagged punches, missing checkouts |
|
|
384
384
|
| `HrLite::PayrollAutoDraftJob` | monthly, on the 1st | Draft and compute the previous month's payroll from attendance, then notify the approvers (`payroll.approve`) and payroll operators |
|
|
385
|
+
| `HrLite::CaReviewReminderJob` | weekly (it acts only in March) | Tell CA reviewers how many slips and declarations are still unverified before 31 March |
|
|
386
|
+
| `HrLite::CaReviewOverdueJob` | 31 March, evening | Tell payroll and HR what the CA has not verified, and any flags left open |
|
|
385
387
|
| `HrLite::PayrollAutoApproveJob` | monthly, on the 3rd | Approve and publish last month's run if nobody finished approving it — unless it has blocking warnings (no salary structure, no PT slabs, a stale or unverified statutory card), when the approvers are told it is overdue instead |
|
|
386
388
|
| `HrLite::LeaveYearRolloverJob` | leave year's first day (Jan 1, or Jul 1 for a July–June year) | Materialize carry-forward into the new year's balances |
|
|
387
389
|
| `HrLite::AttendanceCloseJob` | each night, `55 23 * * * Asia/Kolkata` | Close the day: an open punch is checked out at 23:59 and marked half day; a run before noon (a retry, or a morning schedule that lets night shifts check out first) closes the previous day instead; nobody checked in stays absent. Each is emailed how to fix it. Skips weekends, holidays and approved leave. Run it once a day — a re-run tells the no-shows again. |
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# The CA's year-end pass over every slip and declaration. Reviewers
|
|
4
|
+
# (payroll.verify) verify or flag; approvers and payroll resolve flags.
|
|
5
|
+
class CaReviewsController < BaseController
|
|
6
|
+
skip_before_action :require_operations_access!
|
|
7
|
+
before_action -> { require_any!("payroll.verify", "payroll.approve", "payroll.manage") }, only: :index
|
|
8
|
+
before_action -> { require_any!("payroll.verify") }, only: %i[verify flag verify_all]
|
|
9
|
+
before_action -> { require_any!("payroll.approve", "payroll.manage") }, only: :resolve
|
|
10
|
+
|
|
11
|
+
def index
|
|
12
|
+
@fy = FinancialYear.start_for(params[:fy].present? ? parse_date_param(params[:fy]) : Date.current)
|
|
13
|
+
@tab = params[:tab] == "declarations" ? "declarations" : "slips"
|
|
14
|
+
@status = params[:status].presence_in(%w[unreviewed verified flagged resolved])
|
|
15
|
+
scope = @tab == "slips" ? CaReview.slips_for(@fy).includes(:user, :payroll_run).order("hr_lite_payroll_runs.period_month", :user_id) : CaReview.declarations_for(@fy).includes(:user).order(:user_id)
|
|
16
|
+
type = scope.klass.name
|
|
17
|
+
reviewed = CaReview.where(subject_type: type)
|
|
18
|
+
scope = if @status == "unreviewed"
|
|
19
|
+
scope.where.not(id: reviewed.select(:subject_id))
|
|
20
|
+
elsif @status
|
|
21
|
+
scope.where(id: reviewed.where(status: @status).select(:subject_id))
|
|
22
|
+
else
|
|
23
|
+
scope
|
|
24
|
+
end
|
|
25
|
+
@rows = paginate(scope)
|
|
26
|
+
@reviews = reviewed.where(subject_id: @rows.map(&:id)).index_by(&:subject_id)
|
|
27
|
+
@left = CaReview.outstanding(@fy)
|
|
28
|
+
@totals = { "slips" => CaReview.slips_for(@fy).count, "declarations" => CaReview.declarations_for(@fy).count }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def verify
|
|
32
|
+
CaReview.verify!(subject, actor: hr_current_user)
|
|
33
|
+
redirect_back_to_list notice: "Verified."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def flag
|
|
37
|
+
return redirect_back_to_list(alert: "Say what is wrong — HR has to know what to fix.") if params[:note].blank?
|
|
38
|
+
|
|
39
|
+
CaReview.flag!(subject, actor: hr_current_user, note: params[:note])
|
|
40
|
+
redirect_back_to_list notice: "Flagged — HR and payroll have been told."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# "Verify all shown": the unreviewed rows on the page the CA was looking at.
|
|
44
|
+
def verify_all
|
|
45
|
+
model = params[:subject_type] == "HrLite::TaxDeclaration" ? TaxDeclaration : SalarySlip
|
|
46
|
+
reviewed = CaReview.where(subject_type: model.name, subject_id: params[:ids]).pluck(:subject_id)
|
|
47
|
+
model.where(id: Array(params[:ids]) - reviewed.map(&:to_s)).find_each { |s| CaReview.verify!(s, actor: hr_current_user) }
|
|
48
|
+
redirect_back_to_list notice: "Verified everything shown."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve
|
|
52
|
+
CaReview.find(params[:id]).resolve!(actor: hr_current_user, note: params[:note])
|
|
53
|
+
redirect_back_to_list notice: "Marked resolved."
|
|
54
|
+
rescue ActiveRecord::RecordInvalid
|
|
55
|
+
redirect_back_to_list alert: "Only a flagged item can be resolved."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def require_any!(*keys)
|
|
61
|
+
return if keys.any? { |key| hr_can?(key, scope: :all) }
|
|
62
|
+
|
|
63
|
+
hr_require_permission!(keys.first, scope: :all)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def subject
|
|
67
|
+
model = params[:subject_type] == "HrLite::TaxDeclaration" ? TaxDeclaration : SalarySlip
|
|
68
|
+
model.find(params[:subject_id])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def redirect_back_to_list(**flash)
|
|
72
|
+
redirect_to admin_ca_reviews_path(params.permit(:fy, :tab, :status, :page).to_h), **flash
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -37,6 +37,23 @@ module HrLite
|
|
|
37
37
|
render :new, status: :unprocessable_entity
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Leave HR recorded is fixed in place, any time, with a reason.
|
|
41
|
+
def edit
|
|
42
|
+
@request = find_recorded
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update
|
|
46
|
+
@request = find_recorded
|
|
47
|
+
reason = params[:correction_reason].to_s.strip
|
|
48
|
+
@request.errors.add(:base, "A reason is required to fix this leave") if reason.blank?
|
|
49
|
+
attrs = params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day_part)
|
|
50
|
+
if reason.present? && @request.correct!(actor: hr_current_user, attrs: attrs, reason: reason)
|
|
51
|
+
return redirect_to(admin_leave_request_path(@request), notice: "Leave fixed.")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
render :edit, status: :unprocessable_entity
|
|
55
|
+
end
|
|
56
|
+
|
|
40
57
|
def approve
|
|
41
58
|
request = find_decidable
|
|
42
59
|
if request.approve!(actor: hr_current_user, note: params[:decision_note].presence)
|
|
@@ -91,6 +108,8 @@ module HrLite
|
|
|
91
108
|
|
|
92
109
|
def find_decidable = decidable.find(params[:id])
|
|
93
110
|
|
|
111
|
+
def find_recorded = decidable.approved.where.not(created_by_id: nil).where("created_by_id <> user_id").find(params[:id])
|
|
112
|
+
|
|
94
113
|
def recordable = hr_scope(EmployeeProfile.includes(:user), "leave.approve").where.not(user_id: hr_current_user.id)
|
|
95
114
|
end
|
|
96
115
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module HrLite
|
|
2
2
|
module Admin
|
|
3
3
|
class SalarySlipsController < SuperadminController
|
|
4
|
+
# A CA reads a slip to verify it (payroll.view, all) without running payroll.
|
|
5
|
+
skip_before_action :require_money_access!, only: :show
|
|
6
|
+
before_action -> { hr_require_permission!("payroll.view", scope: :all) }, only: :show
|
|
7
|
+
|
|
4
8
|
def show
|
|
5
9
|
@slip = SalarySlip.includes(:payroll_run).find(params[:id])
|
|
6
10
|
@profile = @slip.user_profile
|
|
@@ -18,6 +18,8 @@ module HrLite
|
|
|
18
18
|
{ label: "ID card", path: :id_card_path, match: [ "/id_card" ], group: "Me" },
|
|
19
19
|
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals" ], group: "Me" },
|
|
20
20
|
{ label: "Timeline", path: :timeline_path, match: [ "/timeline" ], group: "Me" },
|
|
21
|
+
{ label: "CA review", path: :admin_ca_reviews_path, match: [ "/admin/ca_reviews" ], group: "Company",
|
|
22
|
+
if: -> { hr_can?("payroll.verify", scope: :all) || hr_can?("payroll.approve", scope: :all) } },
|
|
21
23
|
{ label: "Policies", path: :policies_path, match: [ "/policies" ], group: "Company" },
|
|
22
24
|
{ label: "Ask HR", path: :hr_requests_path, match: [ "/hr_requests" ], group: "Company" },
|
|
23
25
|
{ label: "Org", path: :org_path, match: [ "/org" ], group: "Company" },
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Schedule on 31 March evening: anything the CA has not verified, or a flag
|
|
3
|
+
# nobody resolved, goes to payroll and HR as overdue.
|
|
4
|
+
class CaReviewOverdueJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform(on: Date.current)
|
|
8
|
+
left = CaReview.outstanding(FinancialYear.start_for(on))
|
|
9
|
+
return if left.values.sum.zero?
|
|
10
|
+
|
|
11
|
+
people = HrLite.users_holding("payroll.approve", scope: :all).to_a | HrLite.users_holding("payroll.manage", scope: :all).to_a
|
|
12
|
+
Notifications.publish(
|
|
13
|
+
"ca.overdue",
|
|
14
|
+
title: "CA review overdue: #{left['slips']} slips, #{left['declarations']} declarations unverified, #{left['flagged']} flags open",
|
|
15
|
+
path: "/admin/ca_reviews?status=unreviewed", bell_to: people, email_to: people
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Schedule weekly in March: tells the CA reviewers what is left before 31 March.
|
|
3
|
+
class CaReviewReminderJob < ApplicationJob
|
|
4
|
+
queue_as :default
|
|
5
|
+
|
|
6
|
+
def perform(on: Date.current)
|
|
7
|
+
return unless on.month == 3
|
|
8
|
+
|
|
9
|
+
left = CaReview.outstanding(FinancialYear.start_for(on))
|
|
10
|
+
return if left.values.sum.zero?
|
|
11
|
+
|
|
12
|
+
reviewers = HrLite.users_holding("payroll.verify", scope: :all).to_a
|
|
13
|
+
Notifications.publish(
|
|
14
|
+
"ca.reminder",
|
|
15
|
+
title: "CA review: #{left['slips']} slips and #{left['declarations']} declarations left before 31 March" \
|
|
16
|
+
"#{", #{left['flagged']} flagged" if left['flagged'].positive?}",
|
|
17
|
+
path: "/admin/ca_reviews", bell_to: reviewers, email_to: reviewers
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -15,7 +15,7 @@ module HrLite
|
|
|
15
15
|
MONEY_TIER_TYPES = %w[
|
|
16
16
|
HrLite::Appraisal HrLite::DesignationChange HrLite::SalaryStructure
|
|
17
17
|
HrLite::PayrollRun HrLite::SalarySlip HrLite::PayrollLineItem HrLite::Loan
|
|
18
|
-
HrLite::PerformancePlan
|
|
18
|
+
HrLite::PerformancePlan HrLite::CaReview
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
21
|
scope :recent, -> { order(created_at: :desc) }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# A chartered accountant's year-end mark on one salary slip or tax
|
|
3
|
+
# declaration. Verifying is final; a flag says what is wrong and stays open
|
|
4
|
+
# until HR or payroll fixes it the normal way (a correcting line on a later
|
|
5
|
+
# slip, or reopening the declaration) and resolves it. The slip itself is
|
|
6
|
+
# never edited.
|
|
7
|
+
class CaReview < ApplicationRecord
|
|
8
|
+
include Audited
|
|
9
|
+
|
|
10
|
+
SUBJECTS = %w[HrLite::SalarySlip HrLite::TaxDeclaration].freeze
|
|
11
|
+
STATUSES = %w[verified flagged resolved].freeze
|
|
12
|
+
|
|
13
|
+
belongs_to :subject, polymorphic: true
|
|
14
|
+
belongs_to :reviewed_by, class_name: HrLite.config.user_class
|
|
15
|
+
belongs_to :resolved_by, class_name: HrLite.config.user_class, optional: true
|
|
16
|
+
|
|
17
|
+
validates :subject_type, inclusion: { in: SUBJECTS }
|
|
18
|
+
validates :status, inclusion: { in: STATUSES }
|
|
19
|
+
validates :note, presence: true, if: -> { status == "flagged" }
|
|
20
|
+
|
|
21
|
+
# What the CA reviews for a financial year: every published slip, and
|
|
22
|
+
# every declaration somebody actually filed.
|
|
23
|
+
def self.slips_for(fy)
|
|
24
|
+
SalarySlip.joins(:payroll_run).where(hr_lite_payroll_runs: { status: "published", period_month: fy...fy.next_year })
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.declarations_for(fy) = TaxDeclaration.where(financial_year: fy).where.not(status: "draft")
|
|
28
|
+
|
|
29
|
+
# Still to do before 31 March: never reviewed, or flagged and not resolved.
|
|
30
|
+
def self.outstanding(fy)
|
|
31
|
+
{ "slips" => slips_for(fy), "declarations" => declarations_for(fy) }.transform_values do |scope|
|
|
32
|
+
done = where(subject: scope, status: %w[verified resolved]).count
|
|
33
|
+
scope.count - done
|
|
34
|
+
end.merge("flagged" => where(subject_type: SUBJECTS, status: "flagged").count)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.verify!(subject, actor:)
|
|
38
|
+
review = find_or_initialize_by(subject: subject)
|
|
39
|
+
review.update!(status: "verified", note: nil, reviewed_by_id: actor.id, reviewed_at: Time.current)
|
|
40
|
+
review
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.flag!(subject, actor:, note:)
|
|
44
|
+
review = find_or_initialize_by(subject: subject)
|
|
45
|
+
review.update!(status: "flagged", note: note.to_s.strip, reviewed_by_id: actor.id, reviewed_at: Time.current,
|
|
46
|
+
resolved_by_id: nil, resolved_at: nil, resolution_note: nil)
|
|
47
|
+
people = (HrLite.users_holding("payroll.approve", scope: :all).to_a | HrLite.users_holding("payroll.manage", scope: :all).to_a)
|
|
48
|
+
Notifications.publish("ca.flagged", title: "CA flagged #{review.subject_label}", body: review.note,
|
|
49
|
+
path: "/admin/ca_reviews?status=flagged", bell_to: people, email_to: people)
|
|
50
|
+
review
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resolve!(actor:, note:)
|
|
54
|
+
raise ActiveRecord::RecordInvalid.new(self), "not flagged" unless status == "flagged"
|
|
55
|
+
|
|
56
|
+
update!(status: "resolved", resolved_by_id: actor.id, resolved_at: Time.current, resolution_note: note.to_s.strip.presence)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def subject_label
|
|
60
|
+
case subject
|
|
61
|
+
when SalarySlip then "#{HrLite.display_name(subject.user)}'s #{subject.period_month.strftime('%B %Y')} slip"
|
|
62
|
+
else "#{HrLite.display_name(subject.user)}'s #{subject.label} tax declaration"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -123,6 +123,24 @@ module HrLite
|
|
|
123
123
|
true
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# HR fixes leave it recorded — any time, even past dates — without a
|
|
127
|
+
# cancel. Re-run as a fresh recording so the create checks and the
|
|
128
|
+
# balance / loss-of-pay split are worked out exactly as the first time.
|
|
129
|
+
def correct!(actor:, attrs:, reason:)
|
|
130
|
+
fixed = false
|
|
131
|
+
transaction do
|
|
132
|
+
assign_attributes(attrs.merge(status: "pending", paid_days: nil, half_day: false))
|
|
133
|
+
raise ActiveRecord::Rollback unless valid?(:create) && save(validate: false)
|
|
134
|
+
|
|
135
|
+
audit!("corrected", actor, reason)
|
|
136
|
+
fixed = approve_outright!(actor: actor, note: "Corrected: #{reason}") || raise(ActiveRecord::Rollback)
|
|
137
|
+
end
|
|
138
|
+
errors.add(:base, "Not enough #{leave_type.name} balance") unless fixed || errors.any?
|
|
139
|
+
fixed
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def recorded_by_hr? = created_by_id.present? && created_by_id != user_id
|
|
143
|
+
|
|
126
144
|
def date_range_label
|
|
127
145
|
if start_date == end_date
|
|
128
146
|
"#{start_date.strftime('%d %b')}#{half_day ? " (#{half_day_part.present? ? "#{half_day_part} half" : 'half day'})" : ''}"
|
|
@@ -230,8 +248,6 @@ module HrLite
|
|
|
230
248
|
)
|
|
231
249
|
end
|
|
232
250
|
|
|
233
|
-
def recorded_by_hr? = created_by_id.present? && created_by_id != user_id
|
|
234
|
-
|
|
235
251
|
# The balance covers whole half-days only, never less than nothing.
|
|
236
252
|
def covered_by(available) = [ (BigDecimal(available.to_s) * 2).floor / BigDecimal(2), 0 ].max
|
|
237
253
|
|
data/app/models/hr_lite/role.rb
CHANGED
|
@@ -12,6 +12,9 @@ module HrLite
|
|
|
12
12
|
FINANCE = "Finance".freeze
|
|
13
13
|
LEADERSHIP = "Leadership".freeze
|
|
14
14
|
SUPER_ADMIN = "Super Admin".freeze
|
|
15
|
+
# An outside chartered accountant: reads pay and tax to verify them at
|
|
16
|
+
# year end. Not an employee — see HrLite.employees.
|
|
17
|
+
CA = "CA".freeze
|
|
15
18
|
|
|
16
19
|
has_many :role_grants, dependent: :destroy
|
|
17
20
|
has_many :role_assignments, dependent: :destroy
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<% content_for(:page_title) { "CA review" } %>
|
|
2
|
+
<% fy_label = HrLite::FinancialYear.label(@fy) %>
|
|
3
|
+
<% reviewer = hr_can?("payroll.verify", scope: :all) %>
|
|
4
|
+
<% resolver = hr_can?("payroll.approve", scope: :all) || hr_can?("payroll.manage", scope: :all) %>
|
|
5
|
+
<% keep = ->(extra = {}) { { fy: @fy, tab: @tab, status: @status }.merge(extra).compact } %>
|
|
6
|
+
<div class="hrl-page__head">
|
|
7
|
+
<h1 class="hrl-page__title">CA review — FY <%= fy_label %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
<p class="hrl-small hrl-muted">Every published salary slip and every filed tax declaration, verified by the CA before 31 March <%= @fy.year + 1 %>. A flag goes to HR and payroll; the fix is a correcting line on a later slip or reopening the declaration — a published slip is never edited.</p>
|
|
10
|
+
|
|
11
|
+
<div class="hrl-kpis">
|
|
12
|
+
<% %w[slips declarations].each do |kind| %>
|
|
13
|
+
<% total = @totals[kind] %>
|
|
14
|
+
<% done = total - @left[kind] %>
|
|
15
|
+
<div class="hrl-kpi">
|
|
16
|
+
<div class="hrl-kpi__value"><%= done %> / <%= total %></div>
|
|
17
|
+
<div class="hrl-kpi__label"><%= kind == "slips" ? "Salary slips" : "Tax declarations" %> verified</div>
|
|
18
|
+
<progress class="hrl-tax-cap" max="<%= [ total, 1 ].max %>" value="<%= done %>" aria-hidden="true"></progress>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @left["flagged"] %></div><div class="hrl-kpi__label">Flags open</div></div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="hrl-row hrl-row--mb" role="tablist">
|
|
25
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @tab == "slips" %>" href="<%= hr_lite.admin_ca_reviews_path(keep.(tab: "slips", status: @status)) %>" aria-current="<%= @tab == "slips" ? "page" : nil %>">Salary slips</a>
|
|
26
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @tab == "declarations" %>" href="<%= hr_lite.admin_ca_reviews_path(keep.(tab: "declarations", status: @status)) %>" aria-current="<%= @tab == "declarations" ? "page" : nil %>">Tax declarations</a>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="hrl-row hrl-row--mb">
|
|
29
|
+
<% [ [ nil, "All" ], [ "unreviewed", "To review" ], [ "flagged", "Flagged" ], [ "verified", "Verified" ], [ "resolved", "Resolved" ] ].each do |value, label| %>
|
|
30
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @status == value %>" href="<%= hr_lite.admin_ca_reviews_path(keep.(status: value)) %>"><%= label %></a>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<section class="hrl-card">
|
|
35
|
+
<% if @rows.empty? %>
|
|
36
|
+
<p class="hrl-card--empty">Nothing here.</p>
|
|
37
|
+
<% else %>
|
|
38
|
+
<% subject_type = @rows.first.class.name %>
|
|
39
|
+
<% if reviewer && @rows.any? { |row| @reviews[row.id].nil? } %>
|
|
40
|
+
<%= button_to "Verify all shown", hr_lite.verify_all_admin_ca_reviews_path, method: :post, class: "hrl-btn",
|
|
41
|
+
params: keep.(subject_type: subject_type, ids: @rows.map(&:id)),
|
|
42
|
+
form: { data: { turbo_confirm: "Verify every unreviewed item on this page?" } } %>
|
|
43
|
+
<% end %>
|
|
44
|
+
<ul class="hrl-feed">
|
|
45
|
+
<% @rows.each do |row| %>
|
|
46
|
+
<% review = @reviews[row.id] %>
|
|
47
|
+
<li class="hrl-feed__item">
|
|
48
|
+
<div class="hrl-feed__body">
|
|
49
|
+
<div class="hrl-row">
|
|
50
|
+
<% if row.is_a?(HrLite::SalarySlip) %>
|
|
51
|
+
<a class="hrl-card__link" href="<%= hr_lite.admin_salary_slip_path(row) %>"><%= hr_display_name(row.user) %> — <%= row.period_month.strftime("%B %Y") %></a>
|
|
52
|
+
<span class="hrl-small hrl-muted">net <%= hrl_money(row.net_pay) %></span>
|
|
53
|
+
<% else %>
|
|
54
|
+
<a class="hrl-card__link" href="<%= hr_lite.admin_tax_declaration_path(row) %>"><%= hr_display_name(row.user) %> — <%= row.label %></a>
|
|
55
|
+
<span class="hrl-small hrl-muted"><%= row.regime.humanize %> regime · <%= row.status.humanize %></span>
|
|
56
|
+
<% end %>
|
|
57
|
+
<span class="hrl-right">
|
|
58
|
+
<% badge = { "verified" => [ "hrl-badge--ok", "Verified" ], "flagged" => [ "hrl-badge--bad", "Flagged" ],
|
|
59
|
+
"resolved" => [ "", "Resolved" ] }.fetch(review&.status, [ "hrl-badge--muted", "To review" ]) %>
|
|
60
|
+
<span class="hrl-badge <%= badge.first %>"><%= badge.last %></span>
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
<% if review&.note.present? %><p class="hrl-small">CA: <%= review.note %></p><% end %>
|
|
64
|
+
<% if review&.resolution_note.present? %><p class="hrl-small hrl-muted">Resolved by <%= hr_display_name(review.resolved_by) %>: <%= review.resolution_note %></p><% end %>
|
|
65
|
+
<div class="hrl-row hrl-mt">
|
|
66
|
+
<% if reviewer && review&.status != "verified" %>
|
|
67
|
+
<%= button_to "Verify", hr_lite.verify_admin_ca_reviews_path, method: :post, class: "hrl-btn",
|
|
68
|
+
params: keep.(subject_type: subject_type, subject_id: row.id) %>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% if reviewer %>
|
|
71
|
+
<details>
|
|
72
|
+
<summary class="hrl-btn">Flag…</summary>
|
|
73
|
+
<%= form_with url: hr_lite.flag_admin_ca_reviews_path, method: :post, local: true do %>
|
|
74
|
+
<% keep.(subject_type: subject_type, subject_id: row.id).each do |key, value| %><%= hidden_field_tag key, value %><% end %>
|
|
75
|
+
<div class="hrl-field"><%= label_tag "note_#{row.id}", "What is wrong" %><%= text_area_tag :note, nil, id: "note_#{row.id}", required: true, rows: 2 %></div>
|
|
76
|
+
<%= submit_tag "Flag it", class: "hrl-btn hrl-btn--primary" %>
|
|
77
|
+
<% end %>
|
|
78
|
+
</details>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% if resolver && review&.status == "flagged" %>
|
|
81
|
+
<%= form_with url: hr_lite.resolve_admin_ca_review_path(review, keep.()), method: :post, local: true, class: "hrl-row" do %>
|
|
82
|
+
<%= label_tag "resolution_#{review.id}", "How it was fixed", class: "hrl-sr-only" %>
|
|
83
|
+
<%= text_field_tag :note, nil, id: "resolution_#{review.id}", placeholder: "How it was fixed" %>
|
|
84
|
+
<%= submit_tag "Mark resolved", class: "hrl-btn" %>
|
|
85
|
+
<% end %>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</li>
|
|
90
|
+
<% end %>
|
|
91
|
+
</ul>
|
|
92
|
+
<%= hrl_pagination %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</section>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Fix leave" } %>
|
|
2
|
+
<a class="hrl-backlink" href="<%= hr_lite.admin_leave_request_path(@request) %>"><span aria-hidden="true">‹</span> Leave</a>
|
|
3
|
+
<div class="hrl-page__head">
|
|
4
|
+
<h1 class="hrl-page__title">Fix leave — <%= hr_display_name(@request.user) %></h1>
|
|
5
|
+
<p class="hrl-page__sub">Change the type, dates or day. The balance and loss of pay are worked out again, and they get an email.</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<section class="hrl-card">
|
|
9
|
+
<%= form_with model: @request, url: hr_lite.admin_leave_request_path(@request), method: :patch, local: true do |f| %>
|
|
10
|
+
<%= render "hr_lite/shared/form_errors", record: @request %>
|
|
11
|
+
<div class="hrl-field">
|
|
12
|
+
<%= f.label :leave_type_id, "Type" %>
|
|
13
|
+
<%= f.collection_select :leave_type_id, HrLite::LeaveType.active, :id, :name %>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="hrl-field"><%= f.label :start_date, "From" %><%= f.date_field :start_date %></div>
|
|
16
|
+
<div class="hrl-field"><%= f.label :end_date, "To" %><%= f.date_field :end_date %></div>
|
|
17
|
+
<div class="hrl-field">
|
|
18
|
+
<%= f.label :half_day_part, "Day" %>
|
|
19
|
+
<%= f.select :half_day_part, HrLite::DAY_PARTS %>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="hrl-field">
|
|
22
|
+
<%= label_tag :correction_reason, "Reason for the fix (required)" %>
|
|
23
|
+
<%= text_area_tag :correction_reason, params[:correction_reason], required: true, class: "hrl-input" %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="hrl-form-actions">
|
|
26
|
+
<%= f.submit "Save fix", class: "hrl-btn hrl-btn--primary" %>
|
|
27
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_leave_request_path(@request) %>">Back</a>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</section>
|
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
</dl>
|
|
25
25
|
</section>
|
|
26
26
|
|
|
27
|
+
<% if @request.approved? && @request.recorded_by_hr? %>
|
|
28
|
+
<section class="hrl-card">
|
|
29
|
+
<h2 class="hrl-card__title">Fix</h2>
|
|
30
|
+
<p class="hrl-small hrl-muted">Recorded by HR. Change it any time — no need to cancel.</p>
|
|
31
|
+
<div class="hrl-form-actions">
|
|
32
|
+
<a class="hrl-btn" href="<%= hr_lite.edit_admin_leave_request_path(@request) %>">Fix this leave</a>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
27
37
|
<%# An approved leave that has not started yet can still be called off — the
|
|
28
38
|
balance is recomputed live, so cancelling gives the days straight back. %>
|
|
29
39
|
<% if @request.cancellable_by?(hr_current_user) && !@request.pending? %>
|
data/config/routes.rb
CHANGED
|
@@ -60,7 +60,7 @@ HrLite::Engine.routes.draw do
|
|
|
60
60
|
resources :attendances, only: %i[index show update], param: :user_id do
|
|
61
61
|
collection { get :week }
|
|
62
62
|
end
|
|
63
|
-
resources :leave_requests, only: %i[index show new create] do
|
|
63
|
+
resources :leave_requests, only: %i[index show new create edit update] do
|
|
64
64
|
member { post :approve; post :reject; post :cancel }
|
|
65
65
|
end
|
|
66
66
|
resources :leave_balances, only: :index do
|
|
@@ -122,6 +122,10 @@ HrLite::Engine.routes.draw do
|
|
|
122
122
|
end
|
|
123
123
|
resources :performance_plans, only: %i[index new create edit update]
|
|
124
124
|
resources :awards, only: %i[index new create edit update]
|
|
125
|
+
resources :ca_reviews, only: :index do
|
|
126
|
+
collection { post :verify; post :flag; post :verify_all }
|
|
127
|
+
member { post :resolve }
|
|
128
|
+
end
|
|
125
129
|
resources :payroll_runs, only: %i[index show new create destroy] do
|
|
126
130
|
member do
|
|
127
131
|
post :compute
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CreateHrLiteCaReviews < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The CA's year-end mark on one salary slip or tax declaration: verified,
|
|
3
|
+
# or flagged with what is wrong until HR/payroll resolves it.
|
|
4
|
+
def change
|
|
5
|
+
create_table :hr_lite_ca_reviews do |t|
|
|
6
|
+
t.string :subject_type, null: false
|
|
7
|
+
t.bigint :subject_id, null: false
|
|
8
|
+
# verified | flagged | resolved
|
|
9
|
+
t.string :status, null: false
|
|
10
|
+
t.text :note
|
|
11
|
+
t.bigint :reviewed_by_id, null: false
|
|
12
|
+
t.datetime :reviewed_at, null: false
|
|
13
|
+
t.bigint :resolved_by_id
|
|
14
|
+
t.datetime :resolved_at
|
|
15
|
+
t.text :resolution_note
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
add_index :hr_lite_ca_reviews, %i[subject_type subject_id], unique: true
|
|
19
|
+
add_check_constraint :hr_lite_ca_reviews, "status IN ('verified', 'flagged', 'resolved')", name: "hr_lite_ca_reviews_status_check"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class SeedHrLiteCaRole < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The CA role is new in 0.26.0 and payroll.verify is a new key. RoleSeeds
|
|
3
|
+
# creates the missing role; Super Admin, an existing role, gets the key here.
|
|
4
|
+
def up
|
|
5
|
+
return unless HrLite::Role.exists?
|
|
6
|
+
|
|
7
|
+
require "hr_lite/role_seeds"
|
|
8
|
+
HrLite::RoleSeeds.call
|
|
9
|
+
super_admin = HrLite::Role.find_by(name: "Super Admin") or return
|
|
10
|
+
HrLite::RoleGrant.find_or_initialize_by(role: super_admin, permission_key: "payroll.verify").update!(scope: "all")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def down; end
|
|
14
|
+
end
|
|
@@ -22,6 +22,9 @@ module HrLite
|
|
|
22
22
|
"attendance.missed_check_out" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
23
23
|
"attendance.short_day" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
24
24
|
"payroll.finalized" => { bell: false, email: false, leadership_email: true, leadership_bell: true },
|
|
25
|
+
"ca.flagged" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
26
|
+
"ca.reminder" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
27
|
+
"ca.overdue" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
25
28
|
"payroll.approval_needed" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
26
29
|
"payroll.overdue" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
27
30
|
"payroll.published" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
data/lib/hr_lite/permissions.rb
CHANGED
|
@@ -37,6 +37,7 @@ module HrLite
|
|
|
37
37
|
"payroll.view" => [ "Payroll", "See payroll runs and salary slips" ],
|
|
38
38
|
"payroll.manage" => [ "Payroll", "Create, compute, finalize, unlock and publish payroll" ],
|
|
39
39
|
"payroll.export" => [ "Payroll", "Download the payout register, including bank details" ],
|
|
40
|
+
"payroll.verify" => [ "Payroll", "Verify or flag each salary slip and tax declaration at year end (CA review)" ],
|
|
40
41
|
"payroll.approve" => [ "Payroll", "Approve a computed payroll run (two approvals finalize and publish it)" ],
|
|
41
42
|
"salary.view" => [ "Payroll", "See salary structures" ],
|
|
42
43
|
"salary.manage" => [ "Payroll", "Set and revise salary structures" ],
|
data/lib/hr_lite/role_seeds.rb
CHANGED
|
@@ -70,6 +70,12 @@ module HrLite
|
|
|
70
70
|
"asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
+
Role::CA => {
|
|
74
|
+
description: "Outside chartered accountant: reads every slip and tax declaration and verifies them before 31 March.",
|
|
75
|
+
grants: {
|
|
76
|
+
"payroll.view" => "all", "salary.view" => "all", "tax.view" => "all", "payroll.verify" => "all"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
73
79
|
Role::SUPER_ADMIN => {
|
|
74
80
|
description: "Everything, including pay, appraisals and who holds which role.",
|
|
75
81
|
grants: Permissions::KEYS.to_h { |key| [ key, "all" ] }
|
data/lib/hr_lite/version.rb
CHANGED
data/lib/hr_lite.rb
CHANGED
|
@@ -114,7 +114,15 @@ module HrLite
|
|
|
114
114
|
# Everyone HR tracks (host-overridable to exclude bots/test accounts),
|
|
115
115
|
# sorted by display name for team screens.
|
|
116
116
|
def employees
|
|
117
|
-
|
|
117
|
+
outside = outside_accountant_ids
|
|
118
|
+
config.employees_scope.call.reject { |u| outside.include?(u.id) }.sort_by { |u| display_name(u).downcase }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# CA logins with no HR profile: they review pay, they are not staff, so
|
|
122
|
+
# no attendance, leave, payroll or team list ever counts them.
|
|
123
|
+
def outside_accountant_ids
|
|
124
|
+
ids = HrLite::RoleAssignment.joins(:role).where(hr_lite_roles: { name: HrLite::Role::CA }).pluck(:user_id)
|
|
125
|
+
(ids - HrLite::EmployeeProfile.where(user_id: ids).pluck(:user_id)).to_set
|
|
118
126
|
end
|
|
119
127
|
|
|
120
128
|
# employees minus anyone whose profile says they have exited — user
|
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.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -80,6 +80,7 @@ files:
|
|
|
80
80
|
- app/controllers/hr_lite/admin/awards_controller.rb
|
|
81
81
|
- app/controllers/hr_lite/admin/base_controller.rb
|
|
82
82
|
- app/controllers/hr_lite/admin/benefits_controller.rb
|
|
83
|
+
- app/controllers/hr_lite/admin/ca_reviews_controller.rb
|
|
83
84
|
- app/controllers/hr_lite/admin/checklists_controller.rb
|
|
84
85
|
- app/controllers/hr_lite/admin/comp_off_requests_controller.rb
|
|
85
86
|
- app/controllers/hr_lite/admin/designation_changes_controller.rb
|
|
@@ -143,6 +144,8 @@ files:
|
|
|
143
144
|
- app/jobs/hr_lite/application_job.rb
|
|
144
145
|
- app/jobs/hr_lite/approval_escalation_job.rb
|
|
145
146
|
- app/jobs/hr_lite/attendance_close_job.rb
|
|
147
|
+
- app/jobs/hr_lite/ca_review_overdue_job.rb
|
|
148
|
+
- app/jobs/hr_lite/ca_review_reminder_job.rb
|
|
146
149
|
- app/jobs/hr_lite/daily_digest_job.rb
|
|
147
150
|
- app/jobs/hr_lite/document_expiry_job.rb
|
|
148
151
|
- app/jobs/hr_lite/leave_year_rollover_job.rb
|
|
@@ -167,6 +170,7 @@ files:
|
|
|
167
170
|
- app/models/hr_lite/award.rb
|
|
168
171
|
- app/models/hr_lite/benefit.rb
|
|
169
172
|
- app/models/hr_lite/benefit_enrolment.rb
|
|
173
|
+
- app/models/hr_lite/ca_review.rb
|
|
170
174
|
- app/models/hr_lite/checklist_item.rb
|
|
171
175
|
- app/models/hr_lite/checklist_template.rb
|
|
172
176
|
- app/models/hr_lite/comp_off_request.rb
|
|
@@ -237,6 +241,7 @@ files:
|
|
|
237
241
|
- app/views/hr_lite/admin/awards/new.html.erb
|
|
238
242
|
- app/views/hr_lite/admin/benefits/index.html.erb
|
|
239
243
|
- app/views/hr_lite/admin/benefits/new.html.erb
|
|
244
|
+
- app/views/hr_lite/admin/ca_reviews/index.html.erb
|
|
240
245
|
- app/views/hr_lite/admin/checklists/index.html.erb
|
|
241
246
|
- app/views/hr_lite/admin/comp_off_requests/index.html.erb
|
|
242
247
|
- app/views/hr_lite/admin/comp_off_requests/show.html.erb
|
|
@@ -257,6 +262,7 @@ files:
|
|
|
257
262
|
- app/views/hr_lite/admin/hr_requests/index.html.erb
|
|
258
263
|
- app/views/hr_lite/admin/hr_requests/show.html.erb
|
|
259
264
|
- app/views/hr_lite/admin/leave_balances/index.html.erb
|
|
265
|
+
- app/views/hr_lite/admin/leave_requests/edit.html.erb
|
|
260
266
|
- app/views/hr_lite/admin/leave_requests/index.html.erb
|
|
261
267
|
- app/views/hr_lite/admin/leave_requests/new.html.erb
|
|
262
268
|
- app/views/hr_lite/admin/leave_requests/show.html.erb
|
|
@@ -412,6 +418,8 @@ files:
|
|
|
412
418
|
- db/migrate/20261010100300_seed_hr_lite_fy2026_rate_cards.rb
|
|
413
419
|
- db/migrate/20261010100400_add_tax_fields_to_hr_lite_tax_declaration_items.rb
|
|
414
420
|
- db/migrate/20261010100500_grant_hr_lite_tax_to_hr.rb
|
|
421
|
+
- db/migrate/20261010110000_create_hr_lite_ca_reviews.rb
|
|
422
|
+
- db/migrate/20261010110100_seed_hr_lite_ca_role.rb
|
|
415
423
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
416
424
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
417
425
|
- lib/generators/hr_lite/install/templates/initializer.rb
|