hr_lite 0.23.1 → 0.26.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 +49 -0
- data/README.md +4 -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/ca_reviews_controller.rb +76 -0
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +14 -4
- data/app/controllers/hr_lite/admin/salary_slips_controller.rb +4 -0
- 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 +7 -0
- data/app/jobs/hr_lite/ca_review_overdue_job.rb +19 -0
- data/app/jobs/hr_lite/ca_review_reminder_job.rb +21 -0
- data/app/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/audit_log.rb +1 -1
- data/app/models/hr_lite/ca_review.rb +66 -0
- data/app/models/hr_lite/payroll_run.rb +47 -3
- data/app/models/hr_lite/role.rb +3 -0
- 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/ca_reviews/index.html.erb +94 -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 +8 -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/db/migrate/20261010110000_create_hr_lite_ca_reviews.rb +21 -0
- data/db/migrate/20261010110100_seed_hr_lite_ca_role.rb +14 -0
- data/lib/hr_lite/notifications.rb +5 -0
- data/lib/hr_lite/permissions.rb +2 -0
- data/lib/hr_lite/role_seeds.rb +9 -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
- data/lib/hr_lite.rb +9 -1
- metadata +20 -1
|
@@ -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>
|
|
@@ -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 }
|
|
@@ -120,10 +122,14 @@ HrLite::Engine.routes.draw do
|
|
|
120
122
|
end
|
|
121
123
|
resources :performance_plans, only: %i[index new create edit update]
|
|
122
124
|
resources :awards, only: %i[index new create edit update]
|
|
125
|
+
resources :ca_reviews, only: :index do
|
|
126
|
+
collection { post :verify; post :flag; post :verify_all }
|
|
127
|
+
member { post :resolve }
|
|
128
|
+
end
|
|
123
129
|
resources :payroll_runs, only: %i[index show new create destroy] do
|
|
124
130
|
member do
|
|
125
131
|
post :compute
|
|
126
|
-
post :
|
|
132
|
+
post :approve
|
|
127
133
|
post :unlock
|
|
128
134
|
post :publish
|
|
129
135
|
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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CreateHrLiteCaReviews < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The CA's year-end mark on one salary slip or tax declaration: verified,
|
|
3
|
+
# or flagged with what is wrong until HR/payroll resolves it.
|
|
4
|
+
def change
|
|
5
|
+
create_table :hr_lite_ca_reviews do |t|
|
|
6
|
+
t.string :subject_type, null: false
|
|
7
|
+
t.bigint :subject_id, null: false
|
|
8
|
+
# verified | flagged | resolved
|
|
9
|
+
t.string :status, null: false
|
|
10
|
+
t.text :note
|
|
11
|
+
t.bigint :reviewed_by_id, null: false
|
|
12
|
+
t.datetime :reviewed_at, null: false
|
|
13
|
+
t.bigint :resolved_by_id
|
|
14
|
+
t.datetime :resolved_at
|
|
15
|
+
t.text :resolution_note
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
add_index :hr_lite_ca_reviews, %i[subject_type subject_id], unique: true
|
|
19
|
+
add_check_constraint :hr_lite_ca_reviews, "status IN ('verified', 'flagged', 'resolved')", name: "hr_lite_ca_reviews_status_check"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class SeedHrLiteCaRole < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The CA role is new in 0.26.0 and payroll.verify is a new key. RoleSeeds
|
|
3
|
+
# creates the missing role; Super Admin, an existing role, gets the key here.
|
|
4
|
+
def up
|
|
5
|
+
return unless HrLite::Role.exists?
|
|
6
|
+
|
|
7
|
+
require "hr_lite/role_seeds"
|
|
8
|
+
HrLite::RoleSeeds.call
|
|
9
|
+
super_admin = HrLite::Role.find_by(name: "Super Admin") or return
|
|
10
|
+
HrLite::RoleGrant.find_or_initialize_by(role: super_admin, permission_key: "payroll.verify").update!(scope: "all")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def down; end
|
|
14
|
+
end
|
|
@@ -22,6 +22,11 @@ module HrLite
|
|
|
22
22
|
"attendance.missed_check_out" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
23
23
|
"attendance.short_day" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
24
24
|
"payroll.finalized" => { bell: false, email: false, leadership_email: true, leadership_bell: true },
|
|
25
|
+
"ca.flagged" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
26
|
+
"ca.reminder" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
27
|
+
"ca.overdue" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
28
|
+
"payroll.approval_needed" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
29
|
+
"payroll.overdue" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
25
30
|
"payroll.published" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
26
31
|
"kudos.mentioned" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
27
32
|
"appraisal.shared" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
data/lib/hr_lite/permissions.rb
CHANGED
|
@@ -37,6 +37,8 @@ module HrLite
|
|
|
37
37
|
"payroll.view" => [ "Payroll", "See payroll runs and salary slips" ],
|
|
38
38
|
"payroll.manage" => [ "Payroll", "Create, compute, finalize, unlock and publish payroll" ],
|
|
39
39
|
"payroll.export" => [ "Payroll", "Download the payout register, including bank details" ],
|
|
40
|
+
"payroll.verify" => [ "Payroll", "Verify or flag each salary slip and tax declaration at year end (CA review)" ],
|
|
41
|
+
"payroll.approve" => [ "Payroll", "Approve a computed payroll run (two approvals finalize and publish it)" ],
|
|
40
42
|
"salary.view" => [ "Payroll", "See salary structures" ],
|
|
41
43
|
"salary.manage" => [ "Payroll", "Set and revise salary structures" ],
|
|
42
44
|
"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"
|
|
@@ -70,6 +70,12 @@ module HrLite
|
|
|
70
70
|
"asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
+
Role::CA => {
|
|
74
|
+
description: "Outside chartered accountant: reads every slip and tax declaration and verifies them before 31 March.",
|
|
75
|
+
grants: {
|
|
76
|
+
"payroll.view" => "all", "salary.view" => "all", "tax.view" => "all", "payroll.verify" => "all"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
73
79
|
Role::SUPER_ADMIN => {
|
|
74
80
|
description: "Everything, including pay, appraisals and who holds which role.",
|
|
75
81
|
grants: Permissions::KEYS.to_h { |key| [ key, "all" ] }
|