hr_lite 0.55.0 → 0.57.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 +17 -0
- data/app/assets/javascripts/hr_lite/geo_punch.js +11 -4
- data/app/controllers/hr_lite/admin/attendances_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +2 -2
- data/app/controllers/hr_lite/admin/expenses_controller.rb +2 -2
- data/app/controllers/hr_lite/admin/salary_structures_controller.rb +2 -2
- data/app/controllers/hr_lite/attendance_controller.rb +14 -0
- data/app/helpers/hr_lite/application_helper.rb +6 -6
- data/app/models/hr_lite/benefit_enrolment.rb +3 -1
- data/app/models/hr_lite/expense.rb +10 -5
- data/app/models/hr_lite/payroll_run.rb +3 -0
- data/app/services/hr_lite/payroll_run_processor.rb +3 -0
- data/app/services/hr_lite/slip_builder.rb +17 -9
- data/app/views/hr_lite/admin/attendances/summary.html.erb +6 -0
- data/app/views/hr_lite/admin/documents/show.html.erb +1 -1
- data/app/views/hr_lite/admin/employees/show.html.erb +1 -1
- data/app/views/hr_lite/admin/expenses/index.html.erb +2 -2
- data/app/views/hr_lite/admin/loans/index.html.erb +1 -1
- data/db/migrate/20261017161441_make_hr_lite_benefit_enrolments_unique_while_open.rb +8 -0
- data/db/migrate/20261017161443_add_attendance_record_id_index_to_hr_lite_work_mode_events.rb +5 -0
- data/lib/hr_lite/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9104f47987b7a8a7e613fd6d07ba269de6b1f2af1ecdd39eed2684b7a62f891
|
|
4
|
+
data.tar.gz: 3a73e730997fefd65b794b47dc14d0c1d836ac60484ee32c5256f601d4cd8043
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89aae488789fee0bf918a04c4570ed9186860be2acadbca4647d314bd41a1d6008709d10bd341a1636639534086bac276e335e50b2753b1b02ff49f51e8cb241
|
|
7
|
+
data.tar.gz: b152d8230fcccf6c6eb7dbcfbfc5152258d125fa3083a3dad7c439b1188059d01166f0e8ada8d7a5c25900f2223dbb5f192a8d4a502fee126ff4e8b210ae6dd4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.57.0] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
No migration.
|
|
13
|
+
|
|
14
|
+
- **Why a day is Absent.** A refused check-in or check-out is now saved with its reason, and the person's attendance summary lists it under that day, for example "Check-in tried 10:09 — refused: You are not within the office location…". When the phone holds a tap because location is blocked or not found, the browser tells the server, and the day shows "Location not picked up on the phone (denied)". A past Absent day with no tries says "No check-in tried". Days before the first refusal was ever saved show neither line, because there is no record to go on. The rows are `attendance.punch_refused` entries in the audit trail.
|
|
15
|
+
|
|
16
|
+
## [0.56.0] - 2026-10-17
|
|
17
|
+
|
|
18
|
+
Owner decisions on the HRMS audit (#133–#138). **Two migrations**: the host runs `bin/rails db:migrate` and refreshes `db/schema.rb`.
|
|
19
|
+
|
|
20
|
+
- **Claims paid with payroll** (FB-5). "Pay with payroll" books an approved claim to a month. The claim is added to that month's slip as a reimbursement line. It is outside gross, so PF, ESI, PT and TDS do not change. Publishing the run marks the claim reimbursed. Booking or moving a claim after compute blocks the run until it is recomputed. A month whose run is finalized or published is refused. Claims marked reimbursed before this release are not picked up again.
|
|
21
|
+
- **Loan instalments never take more than the month's pay** (FB-4). The deduction is capped at what is left after every other deduction. Only the amount taken is booked, the rest stays owed, and the run shows a warning naming the person.
|
|
22
|
+
- **Re-enrol in a benefit after cover ends** (FB-17). The unique index on enrolments now covers only open rows (`ended_on IS NULL`), and the validation matches it. A new index on `work_mode_events.attendance_record_id` (PF-14).
|
|
23
|
+
- **Super Admin may change their own record** (SE-1). The edit is audited. HR and Leadership still cannot edit their own pay, profile or exit.
|
|
24
|
+
- **Menu** (NV-2): Tax checks, Team loans and Team benefits move from Pay to Admin.
|
|
25
|
+
- **Phone**: the Team loans and person-documents "Open" links are 44 px buttons.
|
|
26
|
+
|
|
10
27
|
## [0.55.0] - 2026-10-17
|
|
11
28
|
|
|
12
29
|
The second pass of the HRMS audit of 0.54.0 covers bugs, security, performance and the remaining screen fixes (#115–#131). No migrations.
|
|
@@ -88,10 +88,17 @@
|
|
|
88
88
|
form.submit();
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function hold(headline) {
|
|
91
|
+
function hold(headline, status) {
|
|
92
92
|
locating = false;
|
|
93
93
|
if (button) { button.disabled = false; button.removeAttribute("aria-busy"); }
|
|
94
94
|
showHelp(label, headline);
|
|
95
|
+
// Tell the server about the tap, so HR sees why the day has no check-in.
|
|
96
|
+
if (/\/check_(in|out)$/.test(form.action) && navigator.sendBeacon) {
|
|
97
|
+
var data = new FormData(form);
|
|
98
|
+
data.set("held", "1");
|
|
99
|
+
data.set("geo_status", status);
|
|
100
|
+
navigator.sendBeacon(form.action, data);
|
|
101
|
+
}
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
form.addEventListener("submit", function (event) {
|
|
@@ -102,7 +109,7 @@
|
|
|
102
109
|
if (label) { label.hidden = false; label.classList.replace("hrl-errors", "hrl-muted"); label.textContent = "Getting location…"; }
|
|
103
110
|
|
|
104
111
|
if (!navigator.geolocation) {
|
|
105
|
-
if (required) hold("This browser cannot share your location. Open this page in Chrome or Safari.");
|
|
112
|
+
if (required) hold("This browser cannot share your location. Open this page in Chrome or Safari.", "unavailable");
|
|
106
113
|
else send("unavailable");
|
|
107
114
|
return;
|
|
108
115
|
}
|
|
@@ -117,8 +124,8 @@
|
|
|
117
124
|
function (err) {
|
|
118
125
|
// Browsers ask only once; a past "Don't allow" denies silently forever.
|
|
119
126
|
// Hold the punch so it is never filed (and flagged) without GPS.
|
|
120
|
-
if (err && err.code === 1) { hold("Location is blocked. To allow it:"); return; }
|
|
121
|
-
if (required) { hold("Couldn't get your location. Check that location is on:"); return; }
|
|
127
|
+
if (err && err.code === 1) { hold("Location is blocked. To allow it:", "denied"); return; }
|
|
128
|
+
if (required) { hold("Couldn't get your location. Check that location is on:", err && err.code === 3 ? "timeout" : "unavailable"); return; }
|
|
122
129
|
send("timeout");
|
|
123
130
|
},
|
|
124
131
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
|
|
@@ -141,6 +141,10 @@ module HrLite
|
|
|
141
141
|
range = @period.from..@period.to
|
|
142
142
|
@days = DayStatus.new(user: @employee, range: range)
|
|
143
143
|
@punches = AttendancePunch.where(user_id: @employee.id, date: range).order(:at, :id).group_by(&:date)
|
|
144
|
+
refused = AuditLog.where(action: "attendance.punch_refused")
|
|
145
|
+
@refused = refused.where(subject_type: HrLite.user_klass.name, subject_id: @employee.id, created_at: range.first.beginning_of_day..range.last.end_of_day)
|
|
146
|
+
.order(:created_at).group_by { |log| log.created_at.to_date }
|
|
147
|
+
@tracked_from = refused.minimum(:created_at)&.to_date # before this, a day with no tries proves nothing
|
|
144
148
|
@penalties = AttendancePenalty.active.where(user_id: @employee.id, date: range).group(:date).sum(:minutes)
|
|
145
149
|
@balances = LeaveType.active.where(paid: true).map { |type| LeaveBalance.for(@employee, type, LeaveYear.current_key) }
|
|
146
150
|
@requests = summary_requests(@employee, range)
|
|
@@ -114,8 +114,8 @@ module HrLite
|
|
|
114
114
|
|
|
115
115
|
private
|
|
116
116
|
|
|
117
|
-
# Nobody edits or offboards their own record here (auto-approve would publish it)
|
|
118
|
-
def others_profile = EmployeeProfile.find(params[:id]).tap { |p| raise ActiveRecord::RecordNotFound if p.user_id == hr_current_user.id }
|
|
117
|
+
# Nobody but Super Admin edits or offboards their own record here (auto-approve would publish it).
|
|
118
|
+
def others_profile = EmployeeProfile.find(params[:id]).tap { |p| raise ActiveRecord::RecordNotFound if p.user_id == hr_current_user.id && !hr_superadmin? }
|
|
119
119
|
|
|
120
120
|
def profile_params
|
|
121
121
|
params.require(:employee_profile).permit(
|
|
@@ -34,9 +34,9 @@ module HrLite
|
|
|
34
34
|
expense = decidable.find(params[:id])
|
|
35
35
|
month = parse_month_param(params[:period_month])
|
|
36
36
|
expense.reimburse!(actor: hr_current_user, period_month: month)
|
|
37
|
-
redirect_to admin_expenses_path, notice: "
|
|
37
|
+
redirect_to admin_expenses_path, notice: "Will be paid with #{month.strftime('%B %Y')} payroll."
|
|
38
38
|
rescue ActiveRecord::RecordInvalid
|
|
39
|
-
redirect_to admin_expenses_path, alert: "Only an approved claim can
|
|
39
|
+
redirect_to admin_expenses_path, alert: "Only an approved claim can go on payroll, and only on a month not yet paid."
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# The receipt the employee had to upload, for the person deciding; same rules as a document download.
|
|
@@ -38,10 +38,10 @@ module HrLite
|
|
|
38
38
|
|
|
39
39
|
private
|
|
40
40
|
|
|
41
|
-
# Nobody sets their own pay.
|
|
41
|
+
# Nobody but Super Admin sets their own pay (the owner has nobody above them).
|
|
42
42
|
def set_profile
|
|
43
43
|
@profile = EmployeeProfile.find(params[:employee_id])
|
|
44
|
-
raise ActiveRecord::RecordNotFound if @profile.user_id == hr_current_user.id
|
|
44
|
+
raise ActiveRecord::RecordNotFound if @profile.user_id == hr_current_user.id && !hr_superadmin?
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def structure_params
|
|
@@ -51,6 +51,13 @@ module HrLite
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
53
|
def punch(kind)
|
|
54
|
+
# The browser held the tap (location blocked or not found): log it, file nothing.
|
|
55
|
+
if params[:held]
|
|
56
|
+
geo = AttendancePuncher::GEO_STATUSES.include?(params[:geo_status]) ? params[:geo_status] : "unavailable"
|
|
57
|
+
log_refused(kind, "Location not picked up on the phone (#{geo})")
|
|
58
|
+
return head(:no_content)
|
|
59
|
+
end
|
|
60
|
+
|
|
54
61
|
field = params.fetch(:field, {}).permit(:mode, :purpose, :place).to_h
|
|
55
62
|
result = AttendancePuncher.call(user: hr_current_user, kind: kind, field: field, travel: params[:place] == "travel",
|
|
56
63
|
save_home: params[:save_home] == "1" || params[:place] == "home", **punch_params.to_h.symbolize_keys)
|
|
@@ -58,11 +65,18 @@ module HrLite
|
|
|
58
65
|
time = Time.current.strftime("%H:%M")
|
|
59
66
|
redirect_to attendance_path, notice: "#{kind == :check_in ? 'Checked in' : 'Checked out'} at #{time}."
|
|
60
67
|
else
|
|
68
|
+
log_refused(kind, result.error)
|
|
61
69
|
flash[:punch_offer] = result.offer.to_s if result.offer
|
|
62
70
|
redirect_to attendance_path, alert: result.error
|
|
63
71
|
end
|
|
64
72
|
end
|
|
65
73
|
|
|
74
|
+
# The person's summary lists these per day, so HR can see why a day is Absent.
|
|
75
|
+
def log_refused(kind, error)
|
|
76
|
+
AuditLog.record!(action: "attendance.punch_refused", subject: hr_current_user, actor: hr_current_user,
|
|
77
|
+
changes: { "kind" => kind.to_s, "error" => error })
|
|
78
|
+
end
|
|
79
|
+
|
|
66
80
|
def punch_params
|
|
67
81
|
params.permit(:lat, :lng, :accuracy_m, :geo_status)
|
|
68
82
|
end
|
|
@@ -64,11 +64,7 @@ module HrLite
|
|
|
64
64
|
{ label: "Benefits", path: :benefits_path, match: [ "/benefits" ], if: -> { hr_can?("benefit.view") } },
|
|
65
65
|
# HR approves payroll without running it; Super Admin runs it. One page.
|
|
66
66
|
{ label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips", "/admin/payroll_line_items" ],
|
|
67
|
-
if: -> { hr_superadmin? || (hr_can?("payroll.approve", scope: :all) && instance_exec(&MANAGE_OR_CLAIMS)) } }
|
|
68
|
-
{ label: "Tax checks", path: :admin_tax_declarations_path, match: [ "/admin/tax_declarations" ],
|
|
69
|
-
if: -> { hr_superadmin? || (hr_can?("tax.view", scope: :all) && instance_exec(&MANAGE_OR_CLAIMS)) } },
|
|
70
|
-
{ label: "Team loans", path: :admin_loans_path, match: [ "/admin/loans" ], if: SUPER },
|
|
71
|
-
{ label: "Team benefits", path: :admin_benefits_path, match: [ "/admin/benefits" ], if: MANAGE }
|
|
67
|
+
if: -> { hr_superadmin? || (hr_can?("payroll.approve", scope: :all) && instance_exec(&MANAGE_OR_CLAIMS)) } }
|
|
72
68
|
] },
|
|
73
69
|
{ label: "People", match: [ "/people" ], tabs: [
|
|
74
70
|
{ label: "Team board", path: :team_path, match: [ "/team" ] },
|
|
@@ -99,7 +95,11 @@ module HrLite
|
|
|
99
95
|
{ label: "Statutory rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ], if: SUPER },
|
|
100
96
|
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ], if: -> { hr_can?("expense.approve", scope: :all) || %w[leave.view attendance.view profile.view].any? { |key| hr_can?(key, scope: :team) } } },
|
|
101
97
|
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ], if: -> { hr_can?("asset.manage", scope: :all) } },
|
|
102
|
-
{ label: "Audit trail", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ], if: LEAD }
|
|
98
|
+
{ label: "Audit trail", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ], if: LEAD },
|
|
99
|
+
{ label: "Tax checks", path: :admin_tax_declarations_path, match: [ "/admin/tax_declarations" ],
|
|
100
|
+
if: -> { hr_superadmin? || (hr_can?("tax.view", scope: :all) && instance_exec(&MANAGE_OR_CLAIMS)) } },
|
|
101
|
+
{ label: "Team loans", path: :admin_loans_path, match: [ "/admin/loans" ], if: SUPER },
|
|
102
|
+
{ label: "Team benefits", path: :admin_benefits_path, match: [ "/admin/benefits" ], if: MANAGE }
|
|
103
103
|
] }
|
|
104
104
|
].freeze
|
|
105
105
|
|
|
@@ -9,7 +9,9 @@ module HrLite
|
|
|
9
9
|
belongs_to :user, class_name: HrLite.config.user_class
|
|
10
10
|
|
|
11
11
|
validates :enrolled_on, presence: true
|
|
12
|
-
|
|
12
|
+
# Mirrors the partial unique index: ended cover is history, not a clash.
|
|
13
|
+
validates :user_id, uniqueness: { scope: :benefit_id, conditions: -> { where(ended_on: nil) },
|
|
14
|
+
message: "is already enrolled" }, unless: :ended_on?
|
|
13
15
|
validates :dependants, numericality: { greater_than_or_equal_to: 0 }
|
|
14
16
|
validate :ended_after_it_started
|
|
15
17
|
|
|
@@ -53,21 +53,26 @@ module HrLite
|
|
|
53
53
|
settle!("rejected", actor, note)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# and the person waiting cares about the second
|
|
56
|
+
# Booked onto a payroll month: that month's slip pays it, and publishing
|
|
57
|
+
# the run marks it reimbursed. Agreeing to pay somebody and actually
|
|
58
|
+
# paying them are different days, and the person waiting cares about the second.
|
|
59
59
|
def reimburse!(actor:, period_month:)
|
|
60
|
+
months = [ reimbursed_in, period_month.beginning_of_month ].compact.uniq # moving it touches both runs
|
|
60
61
|
raise ActiveRecord::RecordInvalid.new(self), "not approved" unless approved?
|
|
62
|
+
raise ActiveRecord::RecordInvalid.new(self), "month already paid" if PayrollRun.exists?(period_month: months, status: %w[finalized published])
|
|
61
63
|
|
|
62
64
|
transaction do
|
|
63
|
-
update!(
|
|
65
|
+
update!(reimbursed_in: months.last)
|
|
64
66
|
AuditLog.record!(action: "expense.reimbursed", subject: self, actor: actor,
|
|
65
67
|
changes: { "employee" => HrLite.display_name(user),
|
|
66
68
|
"period" => period_month.strftime("%B %Y") })
|
|
67
69
|
end
|
|
70
|
+
PayrollRun.where(period_month: months, status: "review").find_each do |run|
|
|
71
|
+
run.update!(warnings: run.warnings + [ "#{HrLite.display_name(user)}'s #{category.name} claim changed after compute — recompute" ])
|
|
72
|
+
end
|
|
68
73
|
Notifications.publish(
|
|
69
74
|
"expense.reimbursed",
|
|
70
|
-
title: "Your #{category.name} claim
|
|
75
|
+
title: "Your #{category.name} claim will be paid with #{period_month.strftime('%B %Y')} payroll",
|
|
71
76
|
path: "/expenses", bell_to: [ user ], email_to: [ user ]
|
|
72
77
|
)
|
|
73
78
|
true
|
|
@@ -137,6 +137,9 @@ module HrLite
|
|
|
137
137
|
transaction do
|
|
138
138
|
update!(status: "published", published_at: Time.current, published_by_id: actor&.id)
|
|
139
139
|
audit!("payroll.published", actor, "slips" => salary_slips.count)
|
|
140
|
+
# The claims these slips carry are paid now.
|
|
141
|
+
Expense.awaiting_reimbursement.where(reimbursed_in: period_month, user_id: salary_slips.select(:user_id))
|
|
142
|
+
.update_all(status: "reimbursed", updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
140
143
|
end
|
|
141
144
|
|
|
142
145
|
# config.slip_release_day holds the employee notice until the slip opens
|
|
@@ -50,6 +50,9 @@ module HrLite
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
slip.assign_attributes(attributes)
|
|
53
|
+
if slip.deductions_rows.any? { |row| row.dig("meta", "carried_forward") }
|
|
54
|
+
warnings << "#{HrLite.display_name(user)}'s loan instalment cut (pay too low); the rest is carried forward on the loan"
|
|
55
|
+
end
|
|
53
56
|
slip.save!
|
|
54
57
|
keep_ids << slip.id
|
|
55
58
|
end
|
|
@@ -71,10 +71,8 @@ module HrLite
|
|
|
71
71
|
deductions << { code: "pt", label: "Professional tax", amount: pt } if pt.positive?
|
|
72
72
|
|
|
73
73
|
deductions.concat(extra_deductions)
|
|
74
|
-
loan = loan_instalment
|
|
75
|
-
deductions << loan if loan
|
|
76
74
|
|
|
77
|
-
fy =
|
|
75
|
+
fy =SalarySlip.fy_to_date(@user, @run.period_month)
|
|
78
76
|
tds = Calculators::Tds.call(
|
|
79
77
|
regime: tax_regime,
|
|
80
78
|
structure_monthly_gross: @structure.monthly_gross,
|
|
@@ -90,9 +88,16 @@ module HrLite
|
|
|
90
88
|
rates: @rates[:income_tax],
|
|
91
89
|
override: @tds_override
|
|
92
90
|
)
|
|
91
|
+
loan = loan_instalment(gross_earned - deductions.sum(BigDecimal(0)) { |row| row[:amount] } - tds.monthly)
|
|
92
|
+
deductions << loan if loan
|
|
93
93
|
deductions << { code: "tds", label: "Income tax (TDS)", amount: tds.monthly } if tds.monthly.positive?
|
|
94
94
|
|
|
95
95
|
total_deductions = deductions.sum(BigDecimal(0)) { |row| row[:amount] }
|
|
96
|
+
# Claims booked onto this month: paid back with the salary but not
|
|
97
|
+
# salary, so outside gross and every PF, ESI, PT and TDS base.
|
|
98
|
+
claims = Expense.awaiting_reimbursement.where(user_id: @user.id, reimbursed_in: @run.period_month).includes(:category).map do |claim|
|
|
99
|
+
{ code: "reimbursement", label: "#{claim.category.name} claim (reimbursement, outside gross)", amount: claim.amount }
|
|
100
|
+
end
|
|
96
101
|
|
|
97
102
|
{
|
|
98
103
|
period_month: @run.period_month,
|
|
@@ -104,7 +109,7 @@ module HrLite
|
|
|
104
109
|
lop_days: Money.d(summary[:lop_days]),
|
|
105
110
|
lop_override: @lop_override,
|
|
106
111
|
tds_override: @tds_override,
|
|
107
|
-
earnings: serialize_rows(earnings),
|
|
112
|
+
earnings: serialize_rows(earnings + claims),
|
|
108
113
|
deductions: serialize_rows(deductions),
|
|
109
114
|
employer_costs: employer_costs.transform_values { |v| v.to_s("F") }.to_json,
|
|
110
115
|
tax_details: tds.details.to_json,
|
|
@@ -113,7 +118,7 @@ module HrLite
|
|
|
113
118
|
# A heavy-LOP month can leave TDS (projected from the full structure)
|
|
114
119
|
# larger than what was actually earned. Nobody is paid a negative
|
|
115
120
|
# salary; PayrollRunProcessor warns when this floor bites.
|
|
116
|
-
net_pay: [ gross_earned - total_deductions, BigDecimal(0) ].max,
|
|
121
|
+
net_pay: [ gross_earned + claims.sum(BigDecimal(0)) { |row| row[:amount] } - total_deductions, BigDecimal(0) ].max,
|
|
117
122
|
computed_at: Time.current
|
|
118
123
|
}
|
|
119
124
|
end
|
|
@@ -172,13 +177,16 @@ module HrLite
|
|
|
172
177
|
# The month's loan instalment, as one deduction line. Computed here and
|
|
173
178
|
# BOOKED only when the run is finalized — a draft is recomputed freely,
|
|
174
179
|
# and booking at compute would take a repayment on every pass.
|
|
175
|
-
|
|
176
|
-
|
|
180
|
+
# Never more than `room` (pay left after every other deduction); the rest stays owed.
|
|
181
|
+
def loan_instalment(room)
|
|
182
|
+
due = Loan.active.where(user_id: @user.id).sum(BigDecimal(0)) do |loan|
|
|
177
183
|
loan.instalment_for(@run.period_month)
|
|
178
184
|
end
|
|
179
|
-
return nil unless
|
|
185
|
+
return nil unless due.positive?
|
|
180
186
|
|
|
181
|
-
|
|
187
|
+
amount = room.clamp(BigDecimal(0), due)
|
|
188
|
+
row = { code: "loan_repayment", label: "Loan repayment", amount: amount }
|
|
189
|
+
amount < due ? row.merge(meta: { carried_forward: (due - amount).to_s("F") }) : row
|
|
182
190
|
end
|
|
183
191
|
|
|
184
192
|
# Months left in the Indian FY (Apr–Mar) including the run month itself:
|
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
<% if punches.size > 2 %><div>All punches: <%= punches.join(" · ") %></div><% end %>
|
|
46
46
|
<% if (minutes = @penalties[date]) %><div><span class="hrl-badge hrl-badge--warn">+<%= minutes %> min</span> penalty</div><% end %>
|
|
47
47
|
<% if rec&.flagged? %><div>Flagged: <%= rec.flag_note %></div><% end %>
|
|
48
|
+
<% @refused[date].to_a.each do |log| %>
|
|
49
|
+
<div><%= log.audited_changes["kind"] == "check_out" ? "Check-out" : "Check-in" %> tried <%= log.created_at.strftime("%H:%M") %> — refused: <%= log.audited_changes["error"] %></div>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% if day.kind == :absent && !not_in_yet && @refused[date].nil? && @tracked_from && date >= @tracked_from %>
|
|
52
|
+
<div class="hrl-muted">No check-in tried</div>
|
|
53
|
+
<% end %>
|
|
48
54
|
</div>
|
|
49
55
|
<% if fix && date <= Date.current %>
|
|
50
56
|
<a class="hrl-btn" href="<%= hr_lite.admin_attendance_path(@employee.id, date: date, month: date.strftime("%Y-%m"), anchor: "regularize") %>">Fix<span class="hrl-sr-only"> <%= date.strftime("%-d %b") %></span></a>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
</td>
|
|
33
33
|
<td data-label="">
|
|
34
34
|
<% if doc.readable_by?(hr_current_user) && doc.file.attached? %>
|
|
35
|
-
<a class="hrl-
|
|
35
|
+
<a class="hrl-btn" href="<%= hr_lite.download_document_path(doc) %>">Open</a>
|
|
36
36
|
<% end %>
|
|
37
37
|
</td>
|
|
38
38
|
</tr>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<% content_for(:page_title) { @profile.employee_code } %>
|
|
2
2
|
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @profile.user } %>
|
|
3
3
|
<% content_for(:back, "Employees") %>
|
|
4
|
-
<% own = @profile.user_id == hr_current_user.id # your own pay and record are for someone else to change %>
|
|
4
|
+
<% own = @profile.user_id == hr_current_user.id && !hr_superadmin? # your own pay and record are for someone else to change, unless you are Super Admin %>
|
|
5
5
|
<div class="hrl-page__head">
|
|
6
6
|
<h1 class="hrl-page__title"><%= hr_display_name(@profile.user) %> <span class="hrl-muted">(<%= @profile.employee_code %>)</span></h1>
|
|
7
7
|
<div class="hrl-page__actions">
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
<% elsif expense.approved? %>
|
|
43
43
|
<%= form_with url: hr_lite.reimburse_admin_expense_path(expense), method: :post do %>
|
|
44
44
|
<%= label_tag "reimburse-month-#{expense.id}", "Paid with the payroll of", class: "hrl-sr-only" %>
|
|
45
|
-
<%= month_field_tag :period_month, Date.current.strftime("%Y-%m"), id: "reimburse-month-#{expense.id}", required: true, class: "hrl-input" %>
|
|
46
|
-
<%= submit_tag "
|
|
45
|
+
<%= month_field_tag :period_month, (expense.reimbursed_in || Date.current).strftime("%Y-%m"), id: "reimburse-month-#{expense.id}", required: true, class: "hrl-input" %>
|
|
46
|
+
<%= submit_tag "Pay with payroll", class: "hrl-btn" %>
|
|
47
47
|
<% end %>
|
|
48
48
|
<% end %>
|
|
49
49
|
</td>
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<td data-label="Instalment" class="hrl-num"><%= hrl_money(loan.monthly_instalment) %></td>
|
|
32
32
|
<td data-label="Outstanding" class="hrl-num"><%= hrl_money(loan.outstanding) %></td>
|
|
33
33
|
<td data-label="">
|
|
34
|
-
<a class="hrl-
|
|
34
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_loan_path(loan) %>">Open</a>
|
|
35
35
|
</td>
|
|
36
36
|
</tr>
|
|
37
37
|
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class MakeHrLiteBenefitEnrolmentsUniqueWhileOpen < ActiveRecord::Migration[8.1]
|
|
2
|
+
# One open enrolment per person per plan; ended ones stay as history.
|
|
3
|
+
def change
|
|
4
|
+
remove_index :hr_lite_benefit_enrolments, %i[benefit_id user_id], unique: true, if_exists: true
|
|
5
|
+
add_index :hr_lite_benefit_enrolments, %i[benefit_id user_id], unique: true, where: "ended_on IS NULL",
|
|
6
|
+
name: "index_hr_lite_benefit_enrolments_one_open", if_not_exists: true
|
|
7
|
+
end
|
|
8
|
+
end
|
data/lib/hr_lite/version.rb
CHANGED
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.57.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -505,6 +505,8 @@ files:
|
|
|
505
505
|
- db/migrate/20261016090000_add_penalty_leave_type_to_hr_lite_settings.rb
|
|
506
506
|
- db/migrate/20261016091000_create_hr_lite_penalty_removals.rb
|
|
507
507
|
- db/migrate/20261016100000_add_hours_allowance_repeat_to_hr_lite_employee_profiles.rb
|
|
508
|
+
- db/migrate/20261017161441_make_hr_lite_benefit_enrolments_unique_while_open.rb
|
|
509
|
+
- db/migrate/20261017161443_add_attendance_record_id_index_to_hr_lite_work_mode_events.rb
|
|
508
510
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
509
511
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
510
512
|
- lib/generators/hr_lite/install/templates/initializer.rb
|