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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +2 -1
  4. data/app/assets/javascripts/hr_lite/tax_preview.js +38 -0
  5. data/app/assets/stylesheets/hr_lite/hr_lite.css +12 -1
  6. data/app/controllers/hr_lite/admin/attendances_controller.rb +2 -0
  7. data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +14 -4
  8. data/app/controllers/hr_lite/admin/salary_structures_controller.rb +6 -16
  9. data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +46 -0
  10. data/app/controllers/hr_lite/tax_declarations_controller.rb +19 -1
  11. data/app/controllers/hr_lite/timelines_controller.rb +2 -1
  12. data/app/helpers/hr_lite/application_helper.rb +5 -0
  13. data/app/jobs/hr_lite/payroll_auto_approve_job.rb +21 -0
  14. data/app/jobs/hr_lite/payroll_auto_draft_job.rb +1 -1
  15. data/app/models/hr_lite/payroll_run.rb +47 -3
  16. data/app/models/hr_lite/salary_structure.rb +22 -8
  17. data/app/models/hr_lite/statutory_rate_card_record.rb +6 -6
  18. data/app/models/hr_lite/tax_declaration.rb +24 -0
  19. data/app/models/hr_lite/tax_declaration_item.rb +56 -4
  20. data/app/services/hr_lite/calculators/tds.rb +7 -6
  21. data/app/services/hr_lite/payroll_run_processor.rb +9 -0
  22. data/app/services/hr_lite/slip_builder.rb +1 -1
  23. data/app/services/hr_lite/tax_computation.rb +61 -0
  24. data/app/services/hr_lite/timeline.rb +10 -1
  25. data/app/views/hr_lite/admin/attendances/show.html.erb +25 -0
  26. data/app/views/hr_lite/admin/employees/show.html.erb +3 -1
  27. data/app/views/hr_lite/admin/payroll_runs/show.html.erb +17 -7
  28. data/app/views/hr_lite/admin/salary_structures/_form.html.erb +15 -7
  29. data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +1 -1
  30. data/app/views/hr_lite/admin/tax_declarations/index.html.erb +1 -0
  31. data/app/views/hr_lite/admin/tax_declarations/overview.html.erb +39 -0
  32. data/app/views/hr_lite/admin/tax_declarations/person.html.erb +10 -0
  33. data/app/views/hr_lite/admin/tax_declarations/show.html.erb +19 -2
  34. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +6 -4
  35. data/app/views/hr_lite/tax/_computation.html.erb +56 -0
  36. data/app/views/hr_lite/tax_declarations/show.html.erb +61 -59
  37. data/app/views/hr_lite/timelines/show.html.erb +1 -0
  38. data/config/routes.rb +4 -2
  39. data/db/migrate/20261010100000_add_ctc_to_hr_lite_salary_structures.rb +9 -0
  40. data/db/migrate/20261010100100_add_approval_to_hr_lite_payroll_runs.rb +10 -0
  41. data/db/migrate/20261010100200_grant_hr_lite_payroll_approve.rb +22 -0
  42. data/db/migrate/20261010100300_seed_hr_lite_fy2026_rate_cards.rb +16 -0
  43. data/db/migrate/20261010100400_add_tax_fields_to_hr_lite_tax_declaration_items.rb +9 -0
  44. data/db/migrate/20261010100500_grant_hr_lite_tax_to_hr.rb +12 -0
  45. data/lib/hr_lite/notifications.rb +2 -0
  46. data/lib/hr_lite/permissions.rb +1 -0
  47. data/lib/hr_lite/role_seeds.rb +3 -3
  48. data/lib/hr_lite/statutory_rate_card.rb +65 -43
  49. data/lib/hr_lite/statutory_seeds.rb +4 -2
  50. data/lib/hr_lite/version.rb +1 -1
  51. metadata +13 -1
@@ -1,73 +1,75 @@
1
- <% content_for(:page_title) { "Tax declaration" } %>
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 declaration — <%= @declaration.label %></h1>
5
+ <h1 class="hrl-page__title">Tax — <%= @declaration.label %></h1>
5
6
  </div>
6
7
 
7
- <section class="hrl-card">
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
- scope: :tax_declaration, local: true do |f| %>
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
- <div class="hrl-field">
28
- <%= f.label :regime, "Tax regime", id: "hrl-tax-regime", for: nil %>
29
- <div role="radiogroup" aria-labelledby="hrl-tax-regime">
30
- <% [ [ "new", "New regime", "Lower rates, no deductions." ],
31
- [ "old", "Old regime", "The deductions below reduce your tax." ] ].each do |value, name, hint| %>
32
- <div class="hrl-field--check">
33
- <%= f.radio_button :regime, value %>
34
- <%= f.label :regime, value: value do %><span><%= name %><span class="hrl-hint"><%= hint %></span></span><% end %>
35
- </div>
36
- <% end %>
37
- </div>
38
- </div>
39
- </section>
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">Deductions (old regime only)</summary>
43
- <%= f.fields_for :tax_declaration_items do |item| %>
44
- <div class="hrl-field">
45
- <%= item.hidden_field :section %>
46
- <%= item.label :declared_amount, item.object.section_label %>
47
- <%= item.text_field :declared_amount,
48
- value: item.object.declared_amount&.to_s("F"), inputmode: "decimal" %>
49
- <%= item.label :label, "What it is (optional)", class: "hrl-mt hrl-muted" %>
50
- <%= item.text_field :label %>
51
- <% if item.object.persisted? && item.object.verified_amount %>
52
- <span class="hrl-small hrl-muted">
53
- HR verified <%= hrl_money(item.object.verified_amount) %>
54
- </span>
55
- <% end %>
56
- </div>
57
- <% end %>
58
- <p>Total claimed: <strong><%= hrl_money(@declaration.declared_total) %></strong></p>
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
- <% if submittable %>
62
- <p class="hrl-muted hrl-small">
63
- Submitting locks it for HR to check. They can send it back if the proof
64
- does not match.
65
- </p>
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 :finalize
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 },
@@ -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" ],
@@ -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" => "self",
40
- "appraisal.view" => "self", "resignation.view" => "all",
41
- "document.view" => "all", "tax.view" => "self",
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
- pf: {
24
- employee_rate: r("0.12"), employer_rate: r("0.12"), eps_rate: r("0.0833"),
25
- wage_ceiling: r("15000"), eps_wage_ceiling: r("15000"),
26
- edli_rate: r("0.005"), edli_ceiling: r("15000"), admin_rate: r("0.005")
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: "Shipped with hr_lite #{HrLite::VERSION}. Confirm with your " \
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
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.23.0"
2
+ VERSION = "0.25.0"
3
3
  end
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.23.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