hr_lite 0.23.0 → 0.25.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 +46 -0
- data/README.md +2 -1
- data/app/assets/javascripts/hr_lite/tax_preview.js +38 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +12 -1
- data/app/controllers/hr_lite/admin/attendances_controller.rb +2 -0
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +14 -4
- data/app/controllers/hr_lite/admin/salary_structures_controller.rb +6 -16
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +46 -0
- data/app/controllers/hr_lite/tax_declarations_controller.rb +19 -1
- data/app/controllers/hr_lite/timelines_controller.rb +2 -1
- data/app/helpers/hr_lite/application_helper.rb +5 -0
- data/app/jobs/hr_lite/payroll_auto_approve_job.rb +21 -0
- data/app/jobs/hr_lite/payroll_auto_draft_job.rb +1 -1
- data/app/models/hr_lite/payroll_run.rb +47 -3
- data/app/models/hr_lite/salary_structure.rb +22 -8
- data/app/models/hr_lite/statutory_rate_card_record.rb +6 -6
- data/app/models/hr_lite/tax_declaration.rb +24 -0
- data/app/models/hr_lite/tax_declaration_item.rb +56 -4
- data/app/services/hr_lite/calculators/tds.rb +7 -6
- data/app/services/hr_lite/payroll_run_processor.rb +9 -0
- data/app/services/hr_lite/slip_builder.rb +1 -1
- data/app/services/hr_lite/tax_computation.rb +61 -0
- data/app/services/hr_lite/timeline.rb +10 -1
- data/app/views/hr_lite/admin/attendances/show.html.erb +25 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +3 -1
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +17 -7
- data/app/views/hr_lite/admin/salary_structures/_form.html.erb +15 -7
- data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +1 -1
- data/app/views/hr_lite/admin/tax_declarations/index.html.erb +1 -0
- data/app/views/hr_lite/admin/tax_declarations/overview.html.erb +39 -0
- data/app/views/hr_lite/admin/tax_declarations/person.html.erb +10 -0
- data/app/views/hr_lite/admin/tax_declarations/show.html.erb +19 -2
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +6 -4
- data/app/views/hr_lite/tax/_computation.html.erb +56 -0
- data/app/views/hr_lite/tax_declarations/show.html.erb +61 -59
- data/app/views/hr_lite/timelines/show.html.erb +1 -0
- data/config/routes.rb +4 -2
- data/db/migrate/20261010100000_add_ctc_to_hr_lite_salary_structures.rb +9 -0
- data/db/migrate/20261010100100_add_approval_to_hr_lite_payroll_runs.rb +10 -0
- data/db/migrate/20261010100200_grant_hr_lite_payroll_approve.rb +22 -0
- data/db/migrate/20261010100300_seed_hr_lite_fy2026_rate_cards.rb +16 -0
- data/db/migrate/20261010100400_add_tax_fields_to_hr_lite_tax_declaration_items.rb +9 -0
- data/db/migrate/20261010100500_grant_hr_lite_tax_to_hr.rb +12 -0
- data/lib/hr_lite/notifications.rb +2 -0
- data/lib/hr_lite/permissions.rb +1 -0
- data/lib/hr_lite/role_seeds.rb +3 -3
- data/lib/hr_lite/statutory_rate_card.rb +65 -43
- data/lib/hr_lite/statutory_seeds.rb +4 -2
- data/lib/hr_lite/version.rb +1 -1
- metadata +13 -1
|
@@ -4,22 +4,55 @@ module HrLite
|
|
|
4
4
|
# `verified_amount` is what the proof actually supported, which is often
|
|
5
5
|
# less than what was claimed and is the number payroll uses once HR has
|
|
6
6
|
# looked. Both are encrypted: what somebody invests is their business.
|
|
7
|
+
# The legal limit is applied on top, so nobody's arithmetic decides it.
|
|
7
8
|
class TaxDeclarationItem < ApplicationRecord
|
|
8
9
|
include EncryptedMoney
|
|
9
10
|
|
|
10
|
-
SECTIONS = %w[80c 80d
|
|
11
|
+
SECTIONS = %w[80c 80ccd_1b 80d 80d_parents 24b hra other].freeze
|
|
12
|
+
|
|
13
|
+
# Annual limits (old regime). 80D doubles for a senior; HRA is computed
|
|
14
|
+
# from rent; "other" has no single limit.
|
|
15
|
+
CAPS = { "80c" => 150_000, "80ccd_1b" => 50_000, "24b" => 200_000,
|
|
16
|
+
"80d" => 25_000, "80d_parents" => 25_000 }.freeze
|
|
17
|
+
SENIOR_80D_CAP = 50_000
|
|
18
|
+
RENT_NEEDS_LANDLORD_PAN = 100_000
|
|
19
|
+
PROOF_TYPES = %w[application/pdf image/jpeg image/png].freeze
|
|
20
|
+
PROOF_MAX_BYTES = 5.megabytes
|
|
11
21
|
|
|
12
22
|
encrypted_money :declared_amount, :verified_amount
|
|
23
|
+
encrypts :landlord_pan
|
|
24
|
+
normalizes :landlord_pan, with: ->(pan) { pan.to_s.strip.upcase.presence }
|
|
13
25
|
|
|
14
26
|
belongs_to :declaration, class_name: "HrLite::TaxDeclaration"
|
|
27
|
+
has_many_attached :proofs
|
|
15
28
|
|
|
16
29
|
validates :section, inclusion: { in: SECTIONS }
|
|
17
30
|
validate :amounts_are_not_negative
|
|
31
|
+
validate :landlord_pan_for_large_rent
|
|
32
|
+
validate :proofs_are_documents
|
|
18
33
|
|
|
34
|
+
# Labels name the Income-tax Act 2025 section with the 1961 one people know.
|
|
19
35
|
def section_label
|
|
20
|
-
{ "80c" => "80C —
|
|
21
|
-
"80ccd_1b" => "80CCD(1B) —
|
|
22
|
-
"
|
|
36
|
+
{ "80c" => "Sec 123 (old 80C) — PF, PPF, ELSS, LIC, tuition fees, home-loan principal",
|
|
37
|
+
"80ccd_1b" => "Sec 124 (old 80CCD(1B)) — your own extra NPS",
|
|
38
|
+
"80d" => "Sec 126 (old 80D) — health insurance for you and family",
|
|
39
|
+
"80d_parents" => "Sec 126 (old 80D) — health insurance for parents",
|
|
40
|
+
"24b" => "Home-loan interest (old 24(b))",
|
|
41
|
+
"hra" => "Rent paid in the year (for the HRA exemption)",
|
|
42
|
+
"other" => "Other deductions" }.fetch(section, section)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cap
|
|
46
|
+
return SENIOR_80D_CAP if senior && section.start_with?("80d")
|
|
47
|
+
|
|
48
|
+
CAPS[section]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# What payroll counts for this line: the claim until HR has checked the
|
|
52
|
+
# proof, then what the proof supported — never above the legal limit.
|
|
53
|
+
def allowed(verified:)
|
|
54
|
+
base = verified ? Money.d(verified_amount) : Money.d(declared_amount)
|
|
55
|
+
cap ? [ base, BigDecimal(cap) ].min : base
|
|
23
56
|
end
|
|
24
57
|
|
|
25
58
|
private
|
|
@@ -28,5 +61,24 @@ module HrLite
|
|
|
28
61
|
errors.add(:declared_amount, "cannot be negative") if declared_amount&.negative?
|
|
29
62
|
errors.add(:verified_amount, "cannot be negative") if verified_amount&.negative?
|
|
30
63
|
end
|
|
64
|
+
|
|
65
|
+
def landlord_pan_for_large_rent
|
|
66
|
+
return unless section == "hra" && Money.d(declared_amount) > RENT_NEEDS_LANDLORD_PAN
|
|
67
|
+
|
|
68
|
+
if landlord_pan.blank?
|
|
69
|
+
errors.add(:landlord_pan, "is needed when rent is above ₹1,00,000 a year")
|
|
70
|
+
elsif !landlord_pan.match?(/\A[A-Z]{5}[0-9]{4}[A-Z]\z/)
|
|
71
|
+
errors.add(:landlord_pan, "is not a valid PAN (like ABCDE1234F)")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def proofs_are_documents
|
|
76
|
+
proofs.each do |proof|
|
|
77
|
+
next if PROOF_TYPES.include?(proof.blob.content_type) && proof.blob.byte_size <= PROOF_MAX_BYTES
|
|
78
|
+
|
|
79
|
+
errors.add(:proofs, "must be PDF, JPG or PNG files of up to 5 MB")
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
end
|
|
31
83
|
end
|
|
32
84
|
end
|
|
@@ -67,15 +67,16 @@ module HrLite
|
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def self.slab_tax_for(taxable, slabs)
|
|
71
|
-
|
|
72
|
-
next BigDecimal(0) if taxable <= lower
|
|
70
|
+
def self.slab_tax_for(taxable, slabs) = slab_rows(taxable, slabs).sum(BigDecimal(0)) { |row| row[:tax] }
|
|
71
|
+
private_class_method :slab_tax_for
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
# Each slab with the income that falls in it and the tax on that part.
|
|
74
|
+
def self.slab_rows(taxable, slabs)
|
|
75
|
+
slabs.map do |lower, upper, rate|
|
|
76
|
+
span = taxable <= lower ? BigDecimal(0) : (upper && taxable > upper ? upper : taxable) - lower
|
|
77
|
+
{ from: lower, to: upper, rate: rate, income: span, tax: span * rate }
|
|
76
78
|
end
|
|
77
79
|
end
|
|
78
|
-
private_class_method :slab_tax_for
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
end
|
|
@@ -61,6 +61,15 @@ module HrLite
|
|
|
61
61
|
"it genuinely levies no PT."
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Only approved leave and fixes reach DayStatus; one still waiting is paid as loss of pay.
|
|
65
|
+
month = @run.period_month.all_month
|
|
66
|
+
pending = LeaveRequest.pending.overlapping_range(month.begin, month.end).count +
|
|
67
|
+
RegularizationRequest.pending.where(date: month).count
|
|
68
|
+
if pending.positive?
|
|
69
|
+
warnings << "#{pending} leave or attendance #{pending == 1 ? 'request is' : 'requests are'} still pending for " \
|
|
70
|
+
"#{@run.label} — they count as loss of pay until approved; approve or reject, then recompute."
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
@run.salary_slips.where.not(id: keep_ids).destroy_all
|
|
65
74
|
@run.update!(warnings: warnings)
|
|
66
75
|
end
|
|
@@ -138,7 +138,7 @@ module HrLite
|
|
|
138
138
|
# A rejected declaration lowers nothing: its self-declared total stays editable and unchecked.
|
|
139
139
|
return @profile.declared_annual_deductions if declaration.draft? || declaration.rejected?
|
|
140
140
|
|
|
141
|
-
declaration.
|
|
141
|
+
declaration.old_regime_deductions(structure: @structure)
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
# Splits this month's one-off lines into earnings and deductions, in the
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Somebody's income tax for the financial year, worked out now from what
|
|
3
|
+
# is true now: the salary structure in force, the slips already paid, and
|
|
4
|
+
# their declaration with the legal limits applied. Both regimes, with every
|
|
5
|
+
# step shown, so "why is my TDS X" answers itself. Uses the same TDS
|
|
6
|
+
# calculator payroll uses, so the screen and the slip never disagree.
|
|
7
|
+
class TaxComputation
|
|
8
|
+
Sheet = Struct.new(:regime, :gross, :hra_exemption, :standard_deduction, :deductions, :taxable,
|
|
9
|
+
:slabs, :annual_tax, :tds_paid, :remaining, :months_left, :monthly, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
attr_reader :user, :month, :structure, :declaration, :paid
|
|
12
|
+
|
|
13
|
+
def initialize(user:, on: Date.current, declaration: nil)
|
|
14
|
+
@user = user
|
|
15
|
+
@month = on.beginning_of_month
|
|
16
|
+
@profile = EmployeeProfile.find_by(user_id: user.id)
|
|
17
|
+
@structure = SalaryStructure.effective_for(user, @month)
|
|
18
|
+
@declaration = declaration || TaxDeclaration.for(user, @month)
|
|
19
|
+
@paid = SalarySlip.fy_to_date(user, @month)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def regime = declaration&.regime || @profile&.tax_regime || "new"
|
|
23
|
+
|
|
24
|
+
def months_left = month.month >= 4 ? 16 - month.month : 4 - month.month
|
|
25
|
+
|
|
26
|
+
def paid_gross = paid[:gross] + Money.d(@profile&.fy_opening_gross)
|
|
27
|
+
|
|
28
|
+
def projected_gross = Money.d(structure&.monthly_gross) * months_left
|
|
29
|
+
|
|
30
|
+
def sheets = @sheets ||= TaxDeclaration::REGIMES.to_h { |name| [ name, sheet(name) ] }
|
|
31
|
+
|
|
32
|
+
def chosen = sheets[regime]
|
|
33
|
+
|
|
34
|
+
# The regime that costs less this year, and by how much.
|
|
35
|
+
def saving
|
|
36
|
+
new_tax, old_tax = sheets.values_at("new", "old").map(&:annual_tax)
|
|
37
|
+
new_tax <= old_tax ? [ "new", old_tax - new_tax ] : [ "old", new_tax - old_tax ]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def sheet(name)
|
|
43
|
+
rates = StatutoryRateCard.for(month)[:income_tax]
|
|
44
|
+
table = rates[name]
|
|
45
|
+
rows = name == "old" && declaration && structure ? declaration.deduction_rows(structure: structure) : []
|
|
46
|
+
hra = rows.find { |row| row.section == "hra" }&.allowed || BigDecimal(0)
|
|
47
|
+
tds_paid = paid[:tds] + Money.d(@profile&.fy_opening_tds)
|
|
48
|
+
result = Calculators::Tds.call(
|
|
49
|
+
regime: name, structure_monthly_gross: Money.d(structure&.monthly_gross),
|
|
50
|
+
gross_earned_this_month: Money.d(structure&.monthly_gross), fy_gross_paid: paid_gross,
|
|
51
|
+
fy_tds_paid: tds_paid, months_remaining: months_left,
|
|
52
|
+
declared_annual_deductions: rows.sum(BigDecimal(0), &:allowed), rates: rates
|
|
53
|
+
)
|
|
54
|
+
Sheet.new(regime: name, gross: result.projected_annual_gross, hra_exemption: hra,
|
|
55
|
+
standard_deduction: table[:standard_deduction], deductions: rows.reject { |row| row.section == "hra" },
|
|
56
|
+
taxable: result.taxable, slabs: Calculators::Tds.slab_rows(result.taxable, table[:slabs]),
|
|
57
|
+
annual_tax: result.annual_tax, tds_paid: tds_paid,
|
|
58
|
+
remaining: [ result.annual_tax - tds_paid, BigDecimal(0) ].max, months_left: months_left, monthly: result.monthly)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -18,7 +18,7 @@ module HrLite
|
|
|
18
18
|
|
|
19
19
|
def private_events
|
|
20
20
|
newest_first(pay_items + loans + expenses + appraisals + plans + attendance_fixes + comp_offs +
|
|
21
|
-
assets + documents + resignations + probation + hr_requests)
|
|
21
|
+
assets + documents + resignations + probation + hr_requests + open_leave)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
@@ -124,6 +124,15 @@ module HrLite
|
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
# Leave that never happened: still waiting, or turned down.
|
|
128
|
+
def open_leave
|
|
129
|
+
return [] unless visible?("leave.view")
|
|
130
|
+
|
|
131
|
+
LeaveRequest.where(**mine, status: %w[pending rejected]).overlapping_range(@range.begin, @range.end).includes(:leave_type).map do |leave|
|
|
132
|
+
event(leave.start_date, "Leave", "#{leave.leave_type.name} — #{leave.status}", leave.date_range_label)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
127
136
|
def attendance_fixes
|
|
128
137
|
return [] unless visible?("attendance.view")
|
|
129
138
|
|
|
@@ -24,6 +24,31 @@
|
|
|
24
24
|
<p class="hrl-small hrl-muted hrl-mt">Tap a date to fix its punches.</p>
|
|
25
25
|
</section>
|
|
26
26
|
|
|
27
|
+
<% if @punches.any? %>
|
|
28
|
+
<section class="hrl-card">
|
|
29
|
+
<h2 class="hrl-card__title">Where they punched</h2>
|
|
30
|
+
<% @punches.each do |record| %>
|
|
31
|
+
<div class="hrl-feed__item hrl-small">
|
|
32
|
+
<div class="hrl-feed__body">
|
|
33
|
+
<strong><%= record.date.strftime("%a %-d %b") %></strong>
|
|
34
|
+
<% [ [ "In", record.check_in_at, record.check_in_lat, record.check_in_lng ],
|
|
35
|
+
[ "Out", record.check_out_at, record.check_out_lat, record.check_out_lng ] ].each do |label, at, lat, lng| %>
|
|
36
|
+
<% next unless at %>
|
|
37
|
+
<div><%= label %> <%= at.strftime("%H:%M") %> · <%= HrLite::OfficeLocation.place(lat, lng, @offices) || "No GPS" %>
|
|
38
|
+
<% if lat && lng %>
|
|
39
|
+
· <a class="hrl-card__link" href="https://www.google.com/maps?q=<%= lat %>,<%= lng %>" target="_blank" rel="noopener noreferrer">Map<span class="hrl-sr-only"> of <%= label.downcase %> <%= record.date.strftime("%-d %b") %></span></a>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% if (gap = record.punch_gap_m) %>
|
|
44
|
+
<div class="<%= gap > HrLite::AttendanceRecord::PUNCH_GAP_ALERT_M ? "hrl-badge hrl-badge--warn" : "hrl-muted" %>"><%= (gap / 1000.0).round(1) %> km apart</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
49
|
+
</section>
|
|
50
|
+
<% end %>
|
|
51
|
+
|
|
27
52
|
<% if @edit_date %>
|
|
28
53
|
<section class="hrl-card" id="regularize">
|
|
29
54
|
<h2 class="hrl-card__title">Fix <%= @edit_date.strftime("%A, %d %B") %></h2>
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_appraisal_path(@profile) %>">New appraisal</a>
|
|
10
10
|
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_designation_change_path(@profile) %>">Role change</a>
|
|
11
11
|
<a class="hrl-btn" href="<%= hr_lite.new_admin_performance_plan_path(user_id: @profile.user_id) %>">Start PIP</a>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% if hr_can?("salary.manage", scope: :all) %>
|
|
12
14
|
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_employee_salary_structure_path(@profile) %>">New structure</a>
|
|
13
15
|
<% end %>
|
|
14
16
|
</div>
|
|
@@ -84,7 +86,7 @@
|
|
|
84
86
|
</dl>
|
|
85
87
|
</section>
|
|
86
88
|
|
|
87
|
-
<% if
|
|
89
|
+
<% if hr_can?("salary.view", scope: :all) %>
|
|
88
90
|
<section class="hrl-card">
|
|
89
91
|
<h2 class="hrl-card__title">Salary structures</h2>
|
|
90
92
|
<% if @structures.any? %>
|
|
@@ -26,28 +26,38 @@
|
|
|
26
26
|
<span class="hrl-badge"><%= @run.status.humanize %></span>
|
|
27
27
|
<%# `processing` is offered too: a run stranded there by a killed process
|
|
28
28
|
would otherwise have no control at all and block that month forever. %>
|
|
29
|
-
<%
|
|
29
|
+
<% manager = hr_can?("payroll.manage", scope: :all) %>
|
|
30
|
+
<% if manager && (@run.draft? || @run.review? || @run.processing?) %>
|
|
30
31
|
<%= button_to @run.draft? ? "Compute" : "Recompute", hr_lite.compute_admin_payroll_run_path(@run),
|
|
31
|
-
method: :post, class: "hrl-btn
|
|
32
|
+
method: :post, class: "hrl-btn" %>
|
|
32
33
|
<% end %>
|
|
33
|
-
<% if @run.review? %>
|
|
34
|
-
<%= button_to "
|
|
34
|
+
<% if @run.review? && hr_can?("payroll.approve", scope: :all) && @run.first_approved_by_id != hr_current_user.id %>
|
|
35
|
+
<%= button_to @run.first_approved_by_id ? "Approve and publish" : "Approve", hr_lite.approve_admin_payroll_run_path(@run),
|
|
36
|
+
method: :post, class: "hrl-btn hrl-btn--primary",
|
|
37
|
+
form: { data: { turbo_confirm: @run.first_approved_by_id ? "Approve #{@run.label}? This finalizes the run and publishes the slips." : "Approve #{@run.label}?" } } %>
|
|
35
38
|
<% end %>
|
|
36
|
-
<% if @run.finalized? %>
|
|
39
|
+
<% if manager && @run.finalized? %>
|
|
37
40
|
<%= button_to "Unlock", hr_lite.unlock_admin_payroll_run_path(@run), method: :post, class: "hrl-btn" %>
|
|
38
41
|
<%= button_to "Publish to employees", hr_lite.publish_admin_payroll_run_path(@run), method: :post,
|
|
39
42
|
class: "hrl-btn hrl-btn--primary",
|
|
40
43
|
form: { data: { turbo_confirm: "Publish #{@run.label}? Employees will be notified." } } %>
|
|
41
44
|
<% end %>
|
|
42
|
-
<% if @slips.any? %>
|
|
45
|
+
<% if @slips.any? && hr_can?("payroll.export", scope: :all) %>
|
|
43
46
|
<a class="hrl-btn" href="<%= hr_lite.register_admin_payroll_run_path(@run, format: :csv) %>">Register CSV</a>
|
|
44
47
|
<% end %>
|
|
45
|
-
<% if @run.draft? %>
|
|
48
|
+
<% if manager && @run.draft? %>
|
|
46
49
|
<%= button_to "Delete draft", hr_lite.admin_payroll_run_path(@run), method: :delete,
|
|
47
50
|
class: "hrl-btn hrl-btn--danger",
|
|
48
51
|
form: { data: { turbo_confirm: "Delete this draft run?" } } %>
|
|
49
52
|
<% end %>
|
|
50
53
|
</div>
|
|
54
|
+
<p class="hrl-small hrl-muted hrl-mt">
|
|
55
|
+
<% if @run.review? %>
|
|
56
|
+
<%= @run.first_approved_by ? "Approved by #{hr_display_name(@run.first_approved_by)} on #{@run.first_approved_at.strftime('%-d %b')} — one more approver (HR or Super Admin) publishes it." : "Needs two approvals (HR and Super Admin). If nobody approves by the 3rd, a run with no blocking warnings is approved automatically." %>
|
|
57
|
+
<% elsif @run.auto_approved_at %>
|
|
58
|
+
Approved automatically on <%= @run.auto_approved_at.strftime("%-d %b") %> — nobody had approved it by the 3rd.
|
|
59
|
+
<% end %>
|
|
60
|
+
</p>
|
|
51
61
|
</section>
|
|
52
62
|
|
|
53
63
|
<section class="hrl-card">
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
<%= form_with model: structure, url: url, method: method, scope: :salary_structure, local: true do |f| %>
|
|
3
3
|
<%= render "hr_lite/shared/form_errors", record: structure %>
|
|
4
4
|
<% breakup = structure.breakup(on: structure.effective_from || Date.current) if structure.basic %>
|
|
5
|
-
<div class="hrl-field"
|
|
5
|
+
<div class="hrl-field">
|
|
6
|
+
<%= f.label :annual_ctc, "Annual CTC (₹ a year)" %>
|
|
7
|
+
<%= f.number_field :annual_ctc, step: 1, min: 1, required: true, value: (structure.annual_ctc || breakup&.dig(:annual_ctc))&.round %>
|
|
8
|
+
<p class="hrl-hint">The only amount you type. Saving splits it: Basic 50%, HRA 50% of Basic in a metro (40% elsewhere), employer PF, ESI and gratuity (4.81% of Basic) inside the CTC, the rest as special allowance.</p>
|
|
9
|
+
</div>
|
|
6
10
|
<div class="hrl-field">
|
|
7
11
|
<%= f.label :effective_from, "Effective from (1st of month)" %>
|
|
8
12
|
<%= f.date_field :effective_from %>
|
|
9
13
|
</div>
|
|
10
|
-
<div class="hrl-field"><%= f.label :
|
|
11
|
-
<div class="hrl-field"><%= f.label :hra, "HRA" %><%= f.number_field :hra, step: 0.01, value: structure.hra&.to_s("F") %></div>
|
|
12
|
-
<div class="hrl-field"><%= f.label :special_allowance %><%= f.number_field :special_allowance, step: 0.01, value: structure.special_allowance&.to_s("F") %></div>
|
|
13
|
-
<div class="hrl-field"><%= f.label :other_earnings %><%= f.number_field :other_earnings, step: 0.01, value: structure.other_earnings&.to_s("F") %></div>
|
|
14
|
+
<div class="hrl-field hrl-field--check"><%= f.check_box :metro %><%= f.label :metro, "Works in a metro (Delhi, Mumbai, Kolkata, Chennai)" %></div>
|
|
14
15
|
<div class="hrl-field hrl-field--check"><%= f.check_box :pf_applicable %><%= f.label :pf_applicable, "PF applicable" %></div>
|
|
15
16
|
<div class="hrl-field hrl-field--check"><%= f.check_box :pf_on_full_basic %><%= f.label :pf_on_full_basic, "PF on full basic (no ₹15k ceiling)" %></div>
|
|
16
17
|
<div class="hrl-field hrl-field--check"><%= f.check_box :esi_applicable %><%= f.label :esi_applicable, "ESI applicable (auto-exempt above gross ceiling)" %></div>
|
|
@@ -23,10 +24,17 @@
|
|
|
23
24
|
<%= f.select :pt_state, pt_states.map { |s| [ s.humanize, s ] } %>
|
|
24
25
|
</div>
|
|
25
26
|
<div class="hrl-field"><%= f.label :notes %><%= f.text_area :notes %></div>
|
|
26
|
-
<% if breakup
|
|
27
|
+
<% if breakup %>
|
|
28
|
+
<h3 class="hrl-card__title">Monthly split</h3>
|
|
29
|
+
<dl class="hrl-deflist">
|
|
30
|
+
<% breakup[:earnings].each do |label, amount| %><dt><%= label %></dt><dd><%= hrl_money(amount) %></dd><% end %>
|
|
31
|
+
<dt><strong>Gross</strong></dt><dd><strong><%= hrl_money(breakup[:monthly_gross]) %></strong></dd>
|
|
32
|
+
<% breakup[:employer].each do |label, amount| %><dt>Employer <%= label %></dt><dd><%= hrl_money(amount) %></dd><% end %>
|
|
33
|
+
<dt><strong>CTC</strong></dt><dd><strong><%= hrl_money(breakup[:monthly_ctc]) %></strong> a month</dd>
|
|
34
|
+
</dl>
|
|
35
|
+
<% end %>
|
|
27
36
|
<div class="hrl-form-actions">
|
|
28
37
|
<%= f.submit "Save structure", class: "hrl-btn hrl-btn--primary" %>
|
|
29
|
-
<%= f.submit "Fill from CTC", name: "fill_from_ctc", class: "hrl-btn" %>
|
|
30
38
|
<a class="hrl-btn" href="<%= hr_lite.admin_employee_path(@profile) %>">Cancel</a>
|
|
31
39
|
</div>
|
|
32
40
|
<% end %>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<%= render "hr_lite/shared/form_errors", record: card %>
|
|
13
13
|
|
|
14
14
|
<div class="hrl-field">
|
|
15
|
-
<%= f.label :effective_from, "
|
|
15
|
+
<%= f.label :effective_from, "Takes effect on (1 April for a new financial year, or the 1st of the month a mid-year change applies from)" %>
|
|
16
16
|
<%= f.date_field :effective_from %>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<a class="hrl-btn <%= "hrl-btn--primary" if @status == status %>"
|
|
8
8
|
href="<%= hr_lite.admin_tax_declarations_path(status: status) %>"><%= status.humanize %></a>
|
|
9
9
|
<% end %>
|
|
10
|
+
<a class="hrl-btn" href="<%= hr_lite.overview_admin_tax_declarations_path %>">Everyone's tax</a>
|
|
10
11
|
</div>
|
|
11
12
|
</section>
|
|
12
13
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Everyone's tax" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Everyone's tax — <%= HrLite::FinancialYear.label(Date.current) %></h1>
|
|
4
|
+
<div class="hrl-page__actions"><a class="hrl-btn" href="<%= hr_lite.admin_tax_declarations_path %>">Declarations</a></div>
|
|
5
|
+
</div>
|
|
6
|
+
<p class="hrl-small hrl-muted">Worked out now from each salary structure, the slips already paid and the declaration (limits applied). Open a declaration for the full working.</p>
|
|
7
|
+
|
|
8
|
+
<section class="hrl-card">
|
|
9
|
+
<div class="hrl-table-wrap">
|
|
10
|
+
<table class="hrl-table hrl-table--stack">
|
|
11
|
+
<thead><tr><th>Person</th><th>Regime</th><th>Tax for the year</th><th>Deducted so far</th><th>TDS a month</th><th>Declaration</th></tr></thead>
|
|
12
|
+
<tbody>
|
|
13
|
+
<% @profiles.each do |profile| %>
|
|
14
|
+
<% declaration = @declarations[profile.user_id] %>
|
|
15
|
+
<% tax = HrLite::TaxComputation.new(user: profile.user, declaration: declaration) %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td data-label="Person"><a class="hrl-card__link" href="<%= hr_lite.person_admin_tax_declarations_path(profile.user_id) %>"><%= hr_display_name(profile.user) %></a> <span class="hrl-small hrl-muted"><%= profile.employee_code %></span></td>
|
|
18
|
+
<% if tax.structure %>
|
|
19
|
+
<td data-label="Regime"><%= tax.regime.humanize %><% winner, by = tax.saving %><% if winner != tax.regime && by.positive? %><br><span class="hrl-small hrl-muted"><%= winner.humanize %> saves <%= hrl_rupees(by) %></span><% end %></td>
|
|
20
|
+
<td data-label="Tax for the year" class="hrl-num"><%= hrl_rupees(tax.chosen.annual_tax) %></td>
|
|
21
|
+
<td data-label="Deducted so far" class="hrl-num"><%= hrl_rupees(tax.chosen.tds_paid) %></td>
|
|
22
|
+
<td data-label="TDS a month" class="hrl-num"><%= hrl_rupees(tax.chosen.monthly) %></td>
|
|
23
|
+
<% else %>
|
|
24
|
+
<td data-label="Regime" colspan="4" class="hrl-muted">No salary structure</td>
|
|
25
|
+
<% end %>
|
|
26
|
+
<td data-label="Declaration">
|
|
27
|
+
<% if declaration %>
|
|
28
|
+
<a class="hrl-card__link" href="<%= hr_lite.admin_tax_declaration_path(declaration) %>"><%= declaration.status.humanize %></a>
|
|
29
|
+
<% else %>
|
|
30
|
+
<span class="hrl-muted">Not filed</span>
|
|
31
|
+
<% end %>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
</tbody>
|
|
36
|
+
</table>
|
|
37
|
+
</div>
|
|
38
|
+
<%= hrl_pagination %>
|
|
39
|
+
</section>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Tax — #{hr_display_name(@person)}" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Tax — <%= hr_display_name(@person) %></h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" href="<%= hr_lite.overview_admin_tax_declarations_path %>">Everyone's tax</a>
|
|
6
|
+
<% if @tax.declaration %><a class="hrl-btn" href="<%= hr_lite.admin_tax_declaration_path(@tax.declaration) %>">Declaration</a><% end %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
<% unless @tax.declaration %><p class="hrl-small hrl-muted">No declaration filed this year — the old-regime figure carries no investments yet.</p><% end %>
|
|
10
|
+
<%= render "hr_lite/tax/computation", tax: @tax, both: true %>
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
</section>
|
|
19
19
|
|
|
20
|
+
<%= render "hr_lite/tax/computation", tax: @tax, both: true %>
|
|
21
|
+
|
|
20
22
|
<%= form_with model: @declaration, url: hr_lite.admin_tax_declaration_path(@declaration),
|
|
21
23
|
method: :patch, scope: :tax_declaration, local: true do |f| %>
|
|
22
24
|
<section class="hrl-card">
|
|
@@ -27,7 +29,18 @@
|
|
|
27
29
|
<span class="hrl-small hrl-muted">
|
|
28
30
|
Claimed <%= hrl_money(item.object.declared_amount) %>
|
|
29
31
|
<%= "— #{item.object.label}" if item.object.label.present? %>
|
|
32
|
+
<%= "· limit #{hrl_money(item.object.cap)}" if item.object.cap %>
|
|
33
|
+
<%= "· landlord PAN #{item.object.landlord_pan}" if item.object.landlord_pan.present? %>
|
|
30
34
|
</span>
|
|
35
|
+
<% if item.object.proofs.attached? %>
|
|
36
|
+
<span class="hrl-row">
|
|
37
|
+
<% item.object.proofs.each do |proof| %>
|
|
38
|
+
<a class="hrl-card__link" href="<%= hr_lite.proof_admin_tax_declaration_path(@declaration, proof_id: proof.id) %>">Open <%= proof.filename %></a>
|
|
39
|
+
<% end %>
|
|
40
|
+
</span>
|
|
41
|
+
<% else %>
|
|
42
|
+
<span class="hrl-small hrl-badge hrl-badge--warn">No proof attached</span>
|
|
43
|
+
<% end %>
|
|
31
44
|
<%= item.text_field :verified_amount,
|
|
32
45
|
value: item.object.verified_amount&.to_s("F"), inputmode: "decimal",
|
|
33
46
|
placeholder: "Verified amount" %>
|
|
@@ -41,8 +54,12 @@
|
|
|
41
54
|
|
|
42
55
|
<section class="hrl-card">
|
|
43
56
|
<h2 class="hrl-card__title">Decide</h2>
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
<div class="hrl-row">
|
|
58
|
+
<%= button_to "Accept all and verify", hr_lite.accept_all_admin_tax_declaration_path(@declaration),
|
|
59
|
+
method: :post, class: "hrl-btn hrl-btn--primary" %>
|
|
60
|
+
<%= button_to "Verify the amounts recorded above", hr_lite.verify_admin_tax_declaration_path(@declaration),
|
|
61
|
+
method: :post, class: "hrl-btn" %>
|
|
62
|
+
</div>
|
|
46
63
|
<%= form_with url: hr_lite.reject_admin_tax_declaration_path(@declaration),
|
|
47
64
|
method: :post, local: true do %>
|
|
48
65
|
<div class="hrl-field">
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<%# locals: user (default: you), compact, bare (no card of its own — sits inside another card) %>
|
|
2
|
+
<% user = local_assigns[:user] || hr_current_user %>
|
|
3
|
+
<%= tag.send(local_assigns[:bare] ? :div : :section, class: ("hrl-card" unless local_assigns[:bare])) do %>
|
|
4
|
+
<%= tag.send(local_assigns[:bare] ? :h3 : :h2, class: "hrl-card__title") do %>Leave balance <%= HrLite::LeaveYear.label(HrLite::LeaveYear.current_key) %><% end %>
|
|
3
5
|
<% types = HrLite::LeaveType.active.where(paid: true).where.not(annual_quota: nil) %>
|
|
4
6
|
<% if types.any? %>
|
|
5
7
|
<div class="hrl-row">
|
|
6
8
|
<% types.each do |type| %>
|
|
7
|
-
<% balance = HrLite::LeaveBalance.for(
|
|
9
|
+
<% balance = HrLite::LeaveBalance.for(user, type, HrLite::LeaveYear.current_key) %>
|
|
8
10
|
<span class="hrl-badge hrl-badge--type" style="--hrl-type: <%= type.color %>">
|
|
9
11
|
<%= type.name %> · <%= hrl_days(balance.available) %> of <%= hrl_days(balance.entitled) %> left
|
|
10
12
|
</span>
|
|
@@ -16,4 +18,4 @@
|
|
|
16
18
|
<% else %>
|
|
17
19
|
<p class="hrl-muted hrl-small">No leave types configured yet.</p>
|
|
18
20
|
<% end %>
|
|
19
|
-
|
|
21
|
+
<% end %>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<%# locals: tax (HrLite::TaxComputation), both (true on the admin sheet: every regime's working, open) %>
|
|
2
|
+
<% sheet = tax.chosen %>
|
|
3
|
+
<% winner, by = tax.saving %>
|
|
4
|
+
<% names = { "new" => "New regime", "old" => "Old regime" } %>
|
|
5
|
+
<section class="hrl-card" aria-live="polite">
|
|
6
|
+
<h2 class="hrl-card__title">Income tax <%= HrLite::FinancialYear.label(HrLite::FinancialYear.start_for(tax.month)) %></h2>
|
|
7
|
+
<% if tax.structure.nil? %>
|
|
8
|
+
<p class="hrl-muted">No salary structure yet — tax is worked out once one is set.</p>
|
|
9
|
+
<% else %>
|
|
10
|
+
<div class="hrl-kpis hrl-kpis--money">
|
|
11
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_rupees(sheet.annual_tax) %></div><div class="hrl-kpi__label">Tax for the year (<%= names[sheet.regime] %>)</div></div>
|
|
12
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_rupees(sheet.monthly) %></div><div class="hrl-kpi__label">TDS a month, next <%= pluralize(sheet.months_left, "month") %></div></div>
|
|
13
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_rupees(sheet.tds_paid) %></div><div class="hrl-kpi__label">Deducted so far</div></div>
|
|
14
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_rupees(sheet.remaining) %></div><div class="hrl-kpi__label">Still to deduct</div></div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="hrl-tax-compare" role="group" aria-label="Old and new regime compared">
|
|
18
|
+
<% tax.sheets.each do |name, s| %>
|
|
19
|
+
<div class="hrl-tax-compare__col<%= " hrl-tax-compare__col--chosen" if name == sheet.regime %>">
|
|
20
|
+
<strong><%= names[name] %></strong><% if name == sheet.regime %> <span class="hrl-badge">Chosen</span><% end %>
|
|
21
|
+
<div class="hrl-tax-compare__amount"><%= hrl_rupees(s.annual_tax) %></div>
|
|
22
|
+
<div class="hrl-small hrl-muted">a year · <%= hrl_rupees(s.monthly) %> a month</div>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
<p class="hrl-small">
|
|
27
|
+
<% if by.zero? %>Both regimes cost the same this year.
|
|
28
|
+
<% else %><strong><%= names[winner] %></strong> costs <%= hrl_rupees(by) %> less this year<%= winner == sheet.regime ? " — you are on it." : "." %><% end %>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<% (both ? tax.sheets.values : [ sheet ]).each do |s| %>
|
|
32
|
+
<details class="hrl-tax-working" <%= "open" if both %>>
|
|
33
|
+
<summary class="hrl-btn">How the <%= names[s.regime].downcase %> tax is worked out</summary>
|
|
34
|
+
<table class="hrl-table hrl-tax-working__table">
|
|
35
|
+
<tbody>
|
|
36
|
+
<tr><td>Salary paid so far this year</td><td class="hrl-num"><%= hrl_rupees(tax.paid_gross) %></td></tr>
|
|
37
|
+
<tr><td>Expected for the rest of the year (<%= hrl_rupees(tax.structure.monthly_gross) %> × <%= s.months_left %>)</td><td class="hrl-num"><%= hrl_rupees(tax.projected_gross) %></td></tr>
|
|
38
|
+
<tr class="hrl-tax-working__total"><td>Gross salary for the year</td><td class="hrl-num"><%= hrl_rupees(s.gross) %></td></tr>
|
|
39
|
+
<% if s.hra_exemption.positive? %><tr><td>Less HRA exemption (from rent)</td><td class="hrl-num">− <%= hrl_rupees(s.hra_exemption) %></td></tr><% end %>
|
|
40
|
+
<tr><td>Less standard deduction</td><td class="hrl-num">− <%= hrl_rupees(s.standard_deduction) %></td></tr>
|
|
41
|
+
<% s.deductions.each do |row| %>
|
|
42
|
+
<tr><td>Less <%= row.label %><br><span class="hrl-small hrl-muted">claimed <%= hrl_rupees(row.claimed) %><%= " · limit #{hrl_rupees(row.cap)}" if row.cap %></span></td><td class="hrl-num">− <%= hrl_rupees(row.allowed) %></td></tr>
|
|
43
|
+
<% end %>
|
|
44
|
+
<tr class="hrl-tax-working__total"><td>Taxable income</td><td class="hrl-num"><%= hrl_rupees(s.taxable) %></td></tr>
|
|
45
|
+
<% s.slabs.select { |row| row[:income].positive? }.each do |row| %>
|
|
46
|
+
<tr><td><%= hrl_rupees(row[:from]) %> – <%= row[:to] ? hrl_rupees(row[:to]) : "above" %> at <%= (row[:rate] * 100).to_i %>% on <%= hrl_rupees(row[:income]) %></td><td class="hrl-num"><%= hrl_rupees(row[:tax]) %></td></tr>
|
|
47
|
+
<% end %>
|
|
48
|
+
<tr class="hrl-tax-working__total"><td>Tax for the year, after any §87A rebate, with 4% cess</td><td class="hrl-num"><%= hrl_rupees(s.annual_tax) %></td></tr>
|
|
49
|
+
<tr><td>Already deducted (TDS)</td><td class="hrl-num">− <%= hrl_rupees(s.tds_paid) %></td></tr>
|
|
50
|
+
<tr class="hrl-tax-working__total"><td>Each remaining month (<%= s.months_left %>)</td><td class="hrl-num"><%= hrl_rupees(s.monthly) %></td></tr>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</details>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</section>
|