hr_lite 0.23.1 → 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 +39 -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/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/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
|
@@ -1,73 +1,75 @@
|
|
|
1
|
-
<% content_for(:page_title) { "Tax
|
|
1
|
+
<% content_for(:page_title) { "Tax" } %>
|
|
2
2
|
<% submittable = @declaration.persisted? && (@declaration.draft? || @declaration.rejected?) %>
|
|
3
|
+
<% locked = @declaration.verified? %>
|
|
3
4
|
<div class="hrl-page__head">
|
|
4
|
-
<h1 class="hrl-page__title">Tax
|
|
5
|
+
<h1 class="hrl-page__title">Tax — <%= @declaration.label %></h1>
|
|
5
6
|
</div>
|
|
6
7
|
|
|
7
|
-
<
|
|
8
|
-
<p class="hrl-muted hrl-small">
|
|
9
|
-
Your whole year's TDS is projected from these numbers, so a declaration
|
|
10
|
-
filed in April spreads the tax evenly instead of landing it in March.
|
|
11
|
-
Claim what you intend to invest; HR records what the proof supports when
|
|
12
|
-
you hand it over.
|
|
13
|
-
</p>
|
|
14
|
-
<p>
|
|
15
|
-
Status: <%= hrl_request_status_badge(@declaration.status == "verified" ? "approved" : @declaration.status) %>
|
|
16
|
-
<% if @declaration.note.present? %>
|
|
17
|
-
<span class="hrl-small hrl-muted">— <%= @declaration.note %></span>
|
|
18
|
-
<% end %>
|
|
19
|
-
</p>
|
|
20
|
-
</section>
|
|
8
|
+
<div id="hrl-tax-preview"><%= render "hr_lite/tax/computation", tax: @tax, both: false %></div>
|
|
21
9
|
|
|
22
|
-
<%= form_with model: @declaration, url: hr_lite.tax_declaration_path, method: :patch,
|
|
23
|
-
|
|
10
|
+
<%= form_with model: @declaration, url: hr_lite.tax_declaration_path, method: :patch, scope: :tax_declaration, local: true,
|
|
11
|
+
multipart: true, data: { hrl_tax_preview: hr_lite.preview_tax_declaration_path } do |f| %>
|
|
24
12
|
<%= render "hr_lite/shared/form_errors", record: @declaration %>
|
|
25
13
|
|
|
26
14
|
<section class="hrl-card">
|
|
27
|
-
<
|
|
28
|
-
<%=
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
<p>
|
|
16
|
+
Declaration: <%= hrl_request_status_badge(@declaration.status == "verified" ? "approved" : @declaration.status) %>
|
|
17
|
+
<% if @declaration.note.present? %><span class="hrl-small hrl-muted">— <%= @declaration.note %></span><% end %>
|
|
18
|
+
</p>
|
|
19
|
+
<fieldset class="hrl-field" <%= "disabled" if locked %>>
|
|
20
|
+
<legend>Tax regime</legend>
|
|
21
|
+
<% [ [ "new", "New regime", "Lower rates; only the standard deduction." ],
|
|
22
|
+
[ "old", "Old regime", "Higher rates; the investments below reduce your tax." ] ].each do |value, name, hint| %>
|
|
23
|
+
<div class="hrl-field--check">
|
|
24
|
+
<%= f.radio_button :regime, value %>
|
|
25
|
+
<%= f.label :regime, value: value do %><span><%= name %><span class="hrl-hint"><%= hint %></span></span><% end %>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</fieldset>
|
|
29
|
+
</details>
|
|
40
30
|
|
|
41
31
|
<details class="hrl-card" <%= "open" if @declaration.regime != "new" || @declaration.errors.any? %>>
|
|
42
|
-
<summary class="hrl-btn">
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
32
|
+
<summary class="hrl-btn">Investments and rent (old regime only)</summary>
|
|
33
|
+
<p class="hrl-small hrl-muted">Enter what you will invest or pay this year and attach the proof. The legal limit is applied for you — anything above it simply does not count. Until HR checks the proof, what you claim is used, so you never overpay tax in the meantime.</p>
|
|
34
|
+
<fieldset <%= "disabled" if locked %>>
|
|
35
|
+
<%= f.fields_for :tax_declaration_items do |item| %>
|
|
36
|
+
<% line = item.object %>
|
|
37
|
+
<div class="hrl-field">
|
|
38
|
+
<%= item.hidden_field :section %>
|
|
39
|
+
<%= item.label :declared_amount, line.section_label %>
|
|
40
|
+
<%= item.text_field :declared_amount, value: line.declared_amount&.to_s("F"), inputmode: "decimal", autocomplete: "off" %>
|
|
41
|
+
<% if line.cap %>
|
|
42
|
+
<progress class="hrl-tax-cap" max="<%= line.cap %>" value="<%= [ line.declared_amount.to_i, line.cap ].min %>" aria-hidden="true"></progress>
|
|
43
|
+
<span class="hrl-hint" data-hrl-cap="<%= line.cap %>"><%= hrl_rupees([ line.declared_amount.to_i, line.cap ].min) %> of the <%= hrl_rupees(line.cap) %> limit counts</span>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% if line.section.start_with?("80d") %>
|
|
46
|
+
<div class="hrl-field--check hrl-mt"><%= item.check_box :senior %><%= item.label :senior, line.section == "80d" ? "I am 60 or older" : "A parent is 60 or older" %></div>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if line.section == "hra" %>
|
|
49
|
+
<%= item.label :landlord_pan, "Landlord's PAN (needed above ₹1,00,000 a year)", class: "hrl-mt" %>
|
|
50
|
+
<%= item.text_field :landlord_pan, autocomplete: "off", maxlength: 10, autocapitalize: "characters" %>
|
|
51
|
+
<% end %>
|
|
52
|
+
<%= item.label :proofs, "Proof (PDF, JPG or PNG, up to 5 MB each)", class: "hrl-mt" %>
|
|
53
|
+
<%= item.file_field :proofs, multiple: true, accept: "application/pdf,image/jpeg,image/png" %>
|
|
54
|
+
<% if line.persisted? && line.proofs.attached? %>
|
|
55
|
+
<ul class="hrl-row hrl-mt" aria-label="Attached proof">
|
|
56
|
+
<% line.proofs.each do |proof| %><li class="hrl-badge hrl-badge--muted"><%= proof.filename %></li><% end %>
|
|
57
|
+
</ul>
|
|
58
|
+
<% end %>
|
|
59
|
+
<% if line.persisted? && line.verified_amount %><span class="hrl-small hrl-muted">HR accepted <%= hrl_money(line.verified_amount) %></span><% end %>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
</fieldset>
|
|
59
63
|
</details>
|
|
60
64
|
|
|
61
|
-
<%
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
<% unless locked %>
|
|
66
|
+
<% if submittable %><p class="hrl-muted hrl-small">Submitting sends it to HR to check the proof. They can send it back if something does not match.</p><% end %>
|
|
67
|
+
<div class="hrl-form-actions">
|
|
68
|
+
<%= f.submit "Save", class: submittable ? "hrl-btn" : "hrl-btn hrl-btn--primary" %>
|
|
69
|
+
<% if submittable %><button type="submit" name="submit_to_hr" value="1" class="hrl-btn hrl-btn--primary">Submit to HR</button><% end %>
|
|
70
|
+
</div>
|
|
71
|
+
<% else %>
|
|
72
|
+
<p class="hrl-small hrl-muted">HR has verified this year's declaration. Ask HR to reopen it to change anything.</p>
|
|
66
73
|
<% end %>
|
|
67
|
-
<div class="hrl-form-actions">
|
|
68
|
-
<%= f.submit "Save draft", class: submittable ? "hrl-btn" : "hrl-btn hrl-btn--primary" %>
|
|
69
|
-
<% if submittable %>
|
|
70
|
-
<button type="submit" name="submit_to_hr" value="1" class="hrl-btn hrl-btn--primary">Submit to HR</button>
|
|
71
|
-
<% end %>
|
|
72
|
-
</div>
|
|
73
74
|
<% end %>
|
|
75
|
+
<% content_for(:scripts) { javascript_include_tag "hr_lite/tax_preview", defer: true } %>
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
<section class="hrl-card hrl-card--private" aria-labelledby="private-records">
|
|
32
32
|
<h2 class="hrl-card__title" id="private-records">Private records</h2>
|
|
33
33
|
<p class="hrl-small"><strong>Private</strong> — only <%= @person.id == hr_current_user.id ? "you" : name %> and people with access can see this.</p>
|
|
34
|
+
<% if @show_balance %><%= render "hr_lite/leave_requests/balance_chips", user: @person, compact: true, bare: true %><% end %>
|
|
34
35
|
<%= render "events", events: @private_events, empty: "Nothing private in #{@year}." %>
|
|
35
36
|
</section>
|
|
36
37
|
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -35,6 +35,7 @@ HrLite::Engine.routes.draw do
|
|
|
35
35
|
end
|
|
36
36
|
resource :tax_declaration, only: %i[show update], controller: "tax_declarations" do
|
|
37
37
|
post :submit
|
|
38
|
+
post :preview
|
|
38
39
|
end
|
|
39
40
|
resources :loans, only: :index
|
|
40
41
|
get "team", to: "team#show"
|
|
@@ -80,7 +81,8 @@ HrLite::Engine.routes.draw do
|
|
|
80
81
|
end
|
|
81
82
|
get "documents/for/:user_id", to: "documents#show", as: :employee_documents
|
|
82
83
|
resources :tax_declarations, only: %i[index show update] do
|
|
83
|
-
member { post :verify; post :reject }
|
|
84
|
+
member { post :verify; post :reject; post :accept_all; get "proofs/:proof_id", action: :proof, as: :proof }
|
|
85
|
+
collection { get :overview; get "people/:user_id", action: :person, as: :person }
|
|
84
86
|
end
|
|
85
87
|
resources :loans, only: %i[index new create show] do
|
|
86
88
|
member { post :close; post :cancel }
|
|
@@ -123,7 +125,7 @@ HrLite::Engine.routes.draw do
|
|
|
123
125
|
resources :payroll_runs, only: %i[index show new create destroy] do
|
|
124
126
|
member do
|
|
125
127
|
post :compute
|
|
126
|
-
post :
|
|
128
|
+
post :approve
|
|
127
129
|
post :unlock
|
|
128
130
|
post :publish
|
|
129
131
|
get :register
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddCtcToHrLiteSalaryStructures < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The agreed Annual CTC is now the input (encrypted, like every amount);
|
|
3
|
+
# metro decides the HRA rate. Existing rows keep their lines and get a CTC
|
|
4
|
+
# the next time they are saved.
|
|
5
|
+
def change
|
|
6
|
+
add_column :hr_lite_salary_structures, :annual_ctc, :text
|
|
7
|
+
add_column :hr_lite_salary_structures, :metro, :boolean, null: false, default: false
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddApprovalToHrLitePayrollRuns < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Two people approve a run: the first is stamped here, the second
|
|
3
|
+
# finalizes and publishes it. auto_approved_at marks a run the 3rd-of-month
|
|
4
|
+
# job approved because nobody had.
|
|
5
|
+
def change
|
|
6
|
+
add_column :hr_lite_payroll_runs, :first_approved_by_id, :bigint
|
|
7
|
+
add_column :hr_lite_payroll_runs, :first_approved_at, :datetime
|
|
8
|
+
add_column :hr_lite_payroll_runs, :auto_approved_at, :datetime
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class GrantHrLitePayrollApprove < ActiveRecord::Migration[8.1]
|
|
2
|
+
# payroll.approve is new in 0.24.0, and RoleSeeds never touches an existing
|
|
3
|
+
# role, so without this nobody on a running install could approve. HR and
|
|
4
|
+
# Super Admin approve; HR also reads every run, sets salary structures and
|
|
5
|
+
# edits employee profiles (owner decision 2026-10-09).
|
|
6
|
+
GRANTS = {
|
|
7
|
+
"HR" => { "payroll.approve" => "all", "payroll.view" => "all", "salary.view" => "all", "salary.manage" => "all", "profile.manage" => "all" },
|
|
8
|
+
"Super Admin" => { "payroll.approve" => "all" }
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def up
|
|
12
|
+
GRANTS.each do |role_name, grants|
|
|
13
|
+
role = HrLite::Role.find_by(name: role_name) or next
|
|
14
|
+
grants.each do |key, scope|
|
|
15
|
+
grant = HrLite::RoleGrant.find_or_initialize_by(role: role, permission_key: key)
|
|
16
|
+
grant.update!(scope: scope)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def down; end
|
|
22
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class SeedHrLiteFy2026RateCards < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Ships the FY 2026-27 card and the October 2026 EPF-ceiling card into an
|
|
3
|
+
# install that already has its cards. Creates only what is missing — a card
|
|
4
|
+
# an accountant already entered is never touched.
|
|
5
|
+
def up
|
|
6
|
+
# A fresh install gets every shipped card from `hr_lite:seed`; this is for
|
|
7
|
+
# one that already seeded, where the seed would never run again.
|
|
8
|
+
return unless HrLite::StatutoryRateCardRecord.exists?
|
|
9
|
+
|
|
10
|
+
require "hr_lite/statutory_seeds"
|
|
11
|
+
created = HrLite::StatutorySeeds.seed_cards!
|
|
12
|
+
say "Seeded #{created.join(', ')}" if created.any?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def down; end
|
|
16
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddTaxFieldsToHrLiteTaxDeclarationItems < ActiveRecord::Migration[8.1]
|
|
2
|
+
# senior lifts the 80D limit (₹25,000 → ₹50,000) for whoever the line covers;
|
|
3
|
+
# landlord_pan is required once rent passes ₹1 lakh a year (encrypted:
|
|
4
|
+
# it is somebody else's government ID). Proofs attach through Active Storage.
|
|
5
|
+
def change
|
|
6
|
+
add_column :hr_lite_tax_declaration_items, :senior, :boolean, null: false, default: false
|
|
7
|
+
add_column :hr_lite_tax_declaration_items, :landlord_pan, :text
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class GrantHrLiteTaxToHr < ActiveRecord::Migration[8.1]
|
|
2
|
+
# HR sees and checks everyone's tax (owner decision 2026-10-09). RoleSeeds
|
|
3
|
+
# never touches an existing role, so a running install needs this.
|
|
4
|
+
def up
|
|
5
|
+
role = HrLite::Role.find_by(name: "HR") or return
|
|
6
|
+
{ "tax.view" => "all", "tax.manage" => "all" }.each do |key, scope|
|
|
7
|
+
HrLite::RoleGrant.find_or_initialize_by(role: role, permission_key: key).update!(scope: scope)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def down; end
|
|
12
|
+
end
|
|
@@ -22,6 +22,8 @@ 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
|
+
"payroll.approval_needed" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
26
|
+
"payroll.overdue" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
25
27
|
"payroll.published" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
26
28
|
"kudos.mentioned" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
27
29
|
"appraisal.shared" => { 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.approve" => [ "Payroll", "Approve a computed payroll run (two approvals finalize and publish it)" ],
|
|
40
41
|
"salary.view" => [ "Payroll", "See salary structures" ],
|
|
41
42
|
"salary.manage" => [ "Payroll", "Set and revise salary structures" ],
|
|
42
43
|
"appraisal.view" => [ "Growth", "See appraisals" ],
|
data/lib/hr_lite/role_seeds.rb
CHANGED
|
@@ -36,9 +36,9 @@ module HrLite
|
|
|
36
36
|
grants: {
|
|
37
37
|
"leave.request" => "self", "leave.view" => "all", "leave.approve" => "all",
|
|
38
38
|
"leave.manage" => "all", "attendance.view" => "all", "attendance.manage" => "all",
|
|
39
|
-
"profile.view" => "all", "payroll.view" => "
|
|
40
|
-
"appraisal.view" => "self", "resignation.view" => "all",
|
|
41
|
-
"document.view" => "all", "tax.view" => "
|
|
39
|
+
"profile.view" => "all", "payroll.view" => "all", "payroll.approve" => "all",
|
|
40
|
+
"salary.view" => "all", "salary.manage" => "all", "profile.manage" => "all", "appraisal.view" => "self", "resignation.view" => "all",
|
|
41
|
+
"document.view" => "all", "tax.view" => "all", "tax.manage" => "all",
|
|
42
42
|
"expense.claim" => "self", "benefit.view" => "all", "benefit.manage" => "all",
|
|
43
43
|
"hr_request.raise" => "self", "hr_request.manage" => "all", "policy.view" => "all",
|
|
44
44
|
"asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
|
|
@@ -18,50 +18,72 @@ module HrLite
|
|
|
18
18
|
def self.r(value) = BigDecimal(value.to_s)
|
|
19
19
|
private_class_method :r
|
|
20
20
|
|
|
21
|
+
FY2025_TAX = {
|
|
22
|
+
"new" => {
|
|
23
|
+
standard_deduction: r("75000"), rebate_cap: r("1200000"), cess_rate: r("0.04"),
|
|
24
|
+
# §115BAC carries marginal relief just above the rebate cap; the
|
|
25
|
+
# old regime does not, so this is a per-regime flag.
|
|
26
|
+
marginal_relief: true,
|
|
27
|
+
slabs: [
|
|
28
|
+
[ r("0"), r("400000"), r("0") ],
|
|
29
|
+
[ r("400000"), r("800000"), r("0.05") ],
|
|
30
|
+
[ r("800000"), r("1200000"), r("0.10") ],
|
|
31
|
+
[ r("1200000"), r("1600000"), r("0.15") ],
|
|
32
|
+
[ r("1600000"), r("2000000"), r("0.20") ],
|
|
33
|
+
[ r("2000000"), r("2400000"), r("0.25") ],
|
|
34
|
+
[ r("2400000"), nil, r("0.30") ]
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"old" => {
|
|
38
|
+
standard_deduction: r("50000"), rebate_cap: r("500000"), cess_rate: r("0.04"),
|
|
39
|
+
slabs: [
|
|
40
|
+
[ r("0"), r("250000"), r("0") ],
|
|
41
|
+
[ r("250000"), r("500000"), r("0.05") ],
|
|
42
|
+
[ r("500000"), r("1000000"), r("0.20") ],
|
|
43
|
+
[ r("1000000"), nil, r("0.30") ]
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}.freeze
|
|
47
|
+
private_constant :FY2025_TAX
|
|
48
|
+
|
|
49
|
+
PF_15K = {
|
|
50
|
+
employee_rate: r("0.12"), employer_rate: r("0.12"), eps_rate: r("0.0833"),
|
|
51
|
+
wage_ceiling: r("15000"), eps_wage_ceiling: r("15000"),
|
|
52
|
+
edli_rate: r("0.005"), edli_ceiling: r("15000"), admin_rate: r("0.005")
|
|
53
|
+
}.freeze
|
|
54
|
+
private_constant :PF_15K
|
|
55
|
+
|
|
56
|
+
ESI = { employee_rate: r("0.0075"), employer_rate: r("0.0325"), gross_ceiling: r("21000") }.freeze
|
|
57
|
+
private_constant :ESI
|
|
58
|
+
|
|
59
|
+
PT = {
|
|
60
|
+
# Neither UP nor Uttarakhand levies professional tax today; both
|
|
61
|
+
# ship empty (PT = 0). Karnataka included as a worked template.
|
|
62
|
+
"none" => [],
|
|
63
|
+
"uttar_pradesh" => [],
|
|
64
|
+
"uttarakhand" => [],
|
|
65
|
+
# Inclusive lower bounds. `above: 24999` with a strict `>` taxed a
|
|
66
|
+
# prorated gross of ₹24,999.50, which is below the real threshold.
|
|
67
|
+
"karnataka" => [ { from: r("25000"), monthly: r("200") } ]
|
|
68
|
+
}.freeze
|
|
69
|
+
private_constant :PT
|
|
70
|
+
|
|
71
|
+
# Cards the gem ships already verified, with where the figures come from.
|
|
72
|
+
# StatutorySeeds stamps the source as the verifier, so nobody has to.
|
|
73
|
+
SOURCES = {
|
|
74
|
+
Date.new(2026, 4, 1) => "Finance Act 2026: slabs, standard deduction and §87A rebate unchanged from " \
|
|
75
|
+
"FY 2025-26 (Income-tax Act 2025 in force from 1 April 2026; salary TDS §392). " \
|
|
76
|
+
"PF ₹15,000 and ESI ₹21,000 ceilings unchanged.",
|
|
77
|
+
Date.new(2026, 10, 1) => "EPF/EPS/EDLI wage ceiling ₹25,000 — Ministry of Labour Notification S.O. 5109(E), " \
|
|
78
|
+
"17 Sep 2026; applied from October 2026 payroll. Tax slabs and ESI as FY 2026-27."
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
21
81
|
CARDS = {
|
|
22
|
-
Date.new(2025, 4, 1) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
esi: { employee_rate: r("0.0075"), employer_rate: r("0.0325"), gross_ceiling: r("21000") },
|
|
29
|
-
pt: {
|
|
30
|
-
# Neither UP nor Uttarakhand levies professional tax today; both
|
|
31
|
-
# ship empty (PT = 0). Karnataka included as a worked template.
|
|
32
|
-
"none" => [],
|
|
33
|
-
"uttar_pradesh" => [],
|
|
34
|
-
"uttarakhand" => [],
|
|
35
|
-
# Inclusive lower bounds. `above: 24999` with a strict `>` taxed a
|
|
36
|
-
# prorated gross of ₹24,999.50, which is below the real threshold.
|
|
37
|
-
"karnataka" => [ { from: r("25000"), monthly: r("200") } ]
|
|
38
|
-
},
|
|
39
|
-
income_tax: {
|
|
40
|
-
"new" => {
|
|
41
|
-
standard_deduction: r("75000"), rebate_cap: r("1200000"), cess_rate: r("0.04"),
|
|
42
|
-
# §115BAC carries marginal relief just above the rebate cap; the
|
|
43
|
-
# old regime does not, so this is a per-regime flag.
|
|
44
|
-
marginal_relief: true,
|
|
45
|
-
slabs: [
|
|
46
|
-
[ r("0"), r("400000"), r("0") ],
|
|
47
|
-
[ r("400000"), r("800000"), r("0.05") ],
|
|
48
|
-
[ r("800000"), r("1200000"), r("0.10") ],
|
|
49
|
-
[ r("1200000"), r("1600000"), r("0.15") ],
|
|
50
|
-
[ r("1600000"), r("2000000"), r("0.20") ],
|
|
51
|
-
[ r("2000000"), r("2400000"), r("0.25") ],
|
|
52
|
-
[ r("2400000"), nil, r("0.30") ]
|
|
53
|
-
]
|
|
54
|
-
},
|
|
55
|
-
"old" => {
|
|
56
|
-
standard_deduction: r("50000"), rebate_cap: r("500000"), cess_rate: r("0.04"),
|
|
57
|
-
slabs: [
|
|
58
|
-
[ r("0"), r("250000"), r("0") ],
|
|
59
|
-
[ r("250000"), r("500000"), r("0.05") ],
|
|
60
|
-
[ r("500000"), r("1000000"), r("0.20") ],
|
|
61
|
-
[ r("1000000"), nil, r("0.30") ]
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
}
|
|
82
|
+
Date.new(2025, 4, 1) => { pf: PF_15K, esi: ESI, pt: PT, income_tax: FY2025_TAX },
|
|
83
|
+
Date.new(2026, 4, 1) => { pf: PF_15K, esi: ESI, pt: PT, income_tax: FY2025_TAX },
|
|
84
|
+
Date.new(2026, 10, 1) => {
|
|
85
|
+
pf: PF_15K.merge(wage_ceiling: r("25000"), eps_wage_ceiling: r("25000"), edli_ceiling: r("25000")),
|
|
86
|
+
esi: ESI, pt: PT, income_tax: FY2025_TAX
|
|
65
87
|
}
|
|
66
88
|
}.freeze
|
|
67
89
|
|
|
@@ -20,8 +20,10 @@ module HrLite
|
|
|
20
20
|
pf: stringify(card[:pf]),
|
|
21
21
|
esi: stringify(card[:esi]),
|
|
22
22
|
income_tax: card[:income_tax].transform_values { |regime| stringify_regime(regime) },
|
|
23
|
-
notes:
|
|
24
|
-
"accountant, then record who verified it."
|
|
23
|
+
notes: StatutoryRateCard::SOURCES[effective_from] ||
|
|
24
|
+
"Shipped with hr_lite #{HrLite::VERSION}. Confirm with your accountant, then record who verified it.",
|
|
25
|
+
verified_by: (StatutoryRateCard::SOURCES.key?(effective_from) ? "hr_lite #{HrLite::VERSION} (official source in the notes)" : nil),
|
|
26
|
+
verified_on: (Date.current if StatutoryRateCard::SOURCES.key?(effective_from))
|
|
25
27
|
)
|
|
26
28
|
"rate card FY #{FinancialYear.label(effective_from)}"
|
|
27
29
|
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.25.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- app/assets/javascripts/hr_lite/nav.js
|
|
71
71
|
- app/assets/javascripts/hr_lite/print.js
|
|
72
72
|
- app/assets/javascripts/hr_lite/select_filter.js
|
|
73
|
+
- app/assets/javascripts/hr_lite/tax_preview.js
|
|
73
74
|
- app/assets/stylesheets/hr_lite/application.css
|
|
74
75
|
- app/assets/stylesheets/hr_lite/hr_lite.css
|
|
75
76
|
- app/controllers/hr_lite/admin/appraisals_controller.rb
|
|
@@ -145,6 +146,7 @@ files:
|
|
|
145
146
|
- app/jobs/hr_lite/daily_digest_job.rb
|
|
146
147
|
- app/jobs/hr_lite/document_expiry_job.rb
|
|
147
148
|
- app/jobs/hr_lite/leave_year_rollover_job.rb
|
|
149
|
+
- app/jobs/hr_lite/payroll_auto_approve_job.rb
|
|
148
150
|
- app/jobs/hr_lite/payroll_auto_draft_job.rb
|
|
149
151
|
- app/jobs/hr_lite/slips_ready_job.rb
|
|
150
152
|
- app/mailers/hr_lite/application_mailer.rb
|
|
@@ -216,6 +218,7 @@ files:
|
|
|
216
218
|
- app/services/hr_lite/payroll_run_processor.rb
|
|
217
219
|
- app/services/hr_lite/pdf_renderer.rb
|
|
218
220
|
- app/services/hr_lite/slip_builder.rb
|
|
221
|
+
- app/services/hr_lite/tax_computation.rb
|
|
219
222
|
- app/services/hr_lite/team_day.rb
|
|
220
223
|
- app/services/hr_lite/timeline.rb
|
|
221
224
|
- app/services/hr_lite/working_calendar.rb
|
|
@@ -299,6 +302,8 @@ files:
|
|
|
299
302
|
- app/views/hr_lite/admin/statutory_rate_cards/index.html.erb
|
|
300
303
|
- app/views/hr_lite/admin/statutory_rate_cards/new.html.erb
|
|
301
304
|
- app/views/hr_lite/admin/tax_declarations/index.html.erb
|
|
305
|
+
- app/views/hr_lite/admin/tax_declarations/overview.html.erb
|
|
306
|
+
- app/views/hr_lite/admin/tax_declarations/person.html.erb
|
|
302
307
|
- app/views/hr_lite/admin/tax_declarations/show.html.erb
|
|
303
308
|
- app/views/hr_lite/appraisals/index.html.erb
|
|
304
309
|
- app/views/hr_lite/appraisals/show.html.erb
|
|
@@ -349,6 +354,7 @@ files:
|
|
|
349
354
|
- app/views/hr_lite/shared/_bell.html.erb
|
|
350
355
|
- app/views/hr_lite/shared/_form_errors.html.erb
|
|
351
356
|
- app/views/hr_lite/shared/_pagination.html.erb
|
|
357
|
+
- app/views/hr_lite/tax/_computation.html.erb
|
|
352
358
|
- app/views/hr_lite/tax_declarations/show.html.erb
|
|
353
359
|
- app/views/hr_lite/team/show.html.erb
|
|
354
360
|
- app/views/hr_lite/timelines/_events.html.erb
|
|
@@ -400,6 +406,12 @@ files:
|
|
|
400
406
|
- db/migrate/20261006120100_add_half_day_part_to_hr_lite_attendance_records.rb
|
|
401
407
|
- db/migrate/20261010090000_create_hr_lite_performance_plans.rb
|
|
402
408
|
- db/migrate/20261010090100_create_hr_lite_awards.rb
|
|
409
|
+
- db/migrate/20261010100000_add_ctc_to_hr_lite_salary_structures.rb
|
|
410
|
+
- db/migrate/20261010100100_add_approval_to_hr_lite_payroll_runs.rb
|
|
411
|
+
- db/migrate/20261010100200_grant_hr_lite_payroll_approve.rb
|
|
412
|
+
- db/migrate/20261010100300_seed_hr_lite_fy2026_rate_cards.rb
|
|
413
|
+
- db/migrate/20261010100400_add_tax_fields_to_hr_lite_tax_declaration_items.rb
|
|
414
|
+
- db/migrate/20261010100500_grant_hr_lite_tax_to_hr.rb
|
|
403
415
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
404
416
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
405
417
|
- lib/generators/hr_lite/install/templates/initializer.rb
|