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
|
@@ -9,6 +9,11 @@ module HrLite
|
|
|
9
9
|
belongs_to :created_by, class_name: HrLite.config.user_class, optional: true
|
|
10
10
|
belongs_to :finalized_by, class_name: HrLite.config.user_class, optional: true
|
|
11
11
|
belongs_to :published_by, class_name: HrLite.config.user_class, optional: true
|
|
12
|
+
belongs_to :first_approved_by, class_name: HrLite.config.user_class, optional: true
|
|
13
|
+
|
|
14
|
+
# A run with one of these is not paid on the 3rd without a person looking:
|
|
15
|
+
# someone missing, a state levying ₹0, or last year's / unverified rates.
|
|
16
|
+
BLOCKING_WARNING = /\ANo (salary structure|professional-tax slabs)|statutory card/
|
|
12
17
|
|
|
13
18
|
validates :period_month, presence: true, uniqueness: true
|
|
14
19
|
validates :status, inclusion: { in: STATUSES }
|
|
@@ -40,7 +45,8 @@ module HrLite
|
|
|
40
45
|
update!(status: "processing")
|
|
41
46
|
PayrollRunProcessor.call(self)
|
|
42
47
|
transaction do
|
|
43
|
-
|
|
48
|
+
# New numbers: an approval of the old ones no longer counts.
|
|
49
|
+
update!(status: "review", processed_at: Time.current, first_approved_by_id: nil, first_approved_at: nil)
|
|
44
50
|
audit!("payroll.computed", actor,
|
|
45
51
|
"slips" => salary_slips.count, "warnings" => warnings.length,
|
|
46
52
|
"from_status" => previous)
|
|
@@ -60,7 +66,7 @@ module HrLite
|
|
|
60
66
|
raise ActiveRecord::RecordInvalid.new(self), "no slips" if salary_slips.none?
|
|
61
67
|
|
|
62
68
|
transaction do
|
|
63
|
-
update!(status: "finalized", finalized_at: Time.current, finalized_by_id: actor
|
|
69
|
+
update!(status: "finalized", finalized_at: Time.current, finalized_by_id: actor&.id)
|
|
64
70
|
# Finalizing freezes every slip in the run. Who did it, to how many
|
|
65
71
|
# people, is the row an investigation starts from.
|
|
66
72
|
audit!("payroll.finalized", actor, "slips" => salary_slips.count)
|
|
@@ -74,6 +80,34 @@ module HrLite
|
|
|
74
80
|
true
|
|
75
81
|
end
|
|
76
82
|
|
|
83
|
+
# Two people approve a reviewed run, at least one of them able to run
|
|
84
|
+
# payroll; the second approval finalizes and publishes it.
|
|
85
|
+
def approve!(actor:)
|
|
86
|
+
raise_unless %w[review]
|
|
87
|
+
unless HrLite.can?(actor, "payroll.approve", scope: :all)
|
|
88
|
+
raise ActiveRecord::RecordInvalid.new(self), "not an approver"
|
|
89
|
+
end
|
|
90
|
+
return stamp_first_approval!(actor) if first_approved_by_id.nil?
|
|
91
|
+
if first_approved_by_id == actor.id || [ first_approved_by, actor ].none? { |u| HrLite.can?(u, "payroll.manage", scope: :all) }
|
|
92
|
+
raise ActiveRecord::RecordInvalid.new(self), "needs a second, different approver"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
finalize!(actor: actor) && publish!(actor: actor)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def blocking_warnings = warnings.grep(BLOCKING_WARNING)
|
|
99
|
+
|
|
100
|
+
# The 3rd-of-month backstop: a clean run nobody finished approving is
|
|
101
|
+
# approved by the system and says so; a run with blocking warnings waits.
|
|
102
|
+
def auto_approve!
|
|
103
|
+
raise_unless %w[review]
|
|
104
|
+
return false if blocking_warnings.any?
|
|
105
|
+
|
|
106
|
+
update!(auto_approved_at: Time.current)
|
|
107
|
+
audit!("payroll.auto_approved", nil, "slips" => salary_slips.count)
|
|
108
|
+
finalize!(actor: nil) && publish!(actor: nil)
|
|
109
|
+
end
|
|
110
|
+
|
|
77
111
|
def unlock!(actor:)
|
|
78
112
|
raise_unless %w[finalized]
|
|
79
113
|
transaction do
|
|
@@ -92,7 +126,7 @@ module HrLite
|
|
|
92
126
|
def publish!(actor:)
|
|
93
127
|
raise_unless %w[finalized]
|
|
94
128
|
transaction do
|
|
95
|
-
update!(status: "published", published_at: Time.current, published_by_id: actor
|
|
129
|
+
update!(status: "published", published_at: Time.current, published_by_id: actor&.id)
|
|
96
130
|
audit!("payroll.published", actor, "slips" => salary_slips.count)
|
|
97
131
|
end
|
|
98
132
|
|
|
@@ -145,6 +179,16 @@ module HrLite
|
|
|
145
179
|
|
|
146
180
|
private
|
|
147
181
|
|
|
182
|
+
def stamp_first_approval!(actor)
|
|
183
|
+
update!(first_approved_by_id: actor.id, first_approved_at: Time.current)
|
|
184
|
+
audit!("payroll.approved", actor, "slips" => salary_slips.count, "step" => 1)
|
|
185
|
+
others = HrLite.users_holding("payroll.approve", scope: :all).reject { |u| u.id == actor.id }
|
|
186
|
+
Notifications.publish("payroll.approval_needed",
|
|
187
|
+
title: "Payroll #{label}: #{HrLite.display_name(actor)} approved — one more approval publishes it",
|
|
188
|
+
path: "/admin/payroll_runs/#{id}", bell_to: others, email_to: others)
|
|
189
|
+
true
|
|
190
|
+
end
|
|
191
|
+
|
|
148
192
|
# A repayment is booked when the run is FINALIZED, never at compute: a
|
|
149
193
|
# draft is recomputed as often as the operator likes, and each pass would
|
|
150
194
|
# otherwise take another instalment off the loan.
|
data/app/models/hr_lite/role.rb
CHANGED
|
@@ -12,6 +12,9 @@ module HrLite
|
|
|
12
12
|
FINANCE = "Finance".freeze
|
|
13
13
|
LEADERSHIP = "Leadership".freeze
|
|
14
14
|
SUPER_ADMIN = "Super Admin".freeze
|
|
15
|
+
# An outside chartered accountant: reads pay and tax to verify them at
|
|
16
|
+
# year end. Not an employee — see HrLite.employees.
|
|
17
|
+
CA = "CA".freeze
|
|
15
18
|
|
|
16
19
|
has_many :role_grants, dependent: :destroy
|
|
17
20
|
has_many :role_assignments, dependent: :destroy
|
|
@@ -10,9 +10,15 @@ module HrLite
|
|
|
10
10
|
belongs_to :user, class_name: HrLite.config.user_class
|
|
11
11
|
belongs_to :created_by, class_name: HrLite.config.user_class, optional: true
|
|
12
12
|
|
|
13
|
-
encrypted_money :basic, :hra, :special_allowance, :other_earnings
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
encrypted_money :basic, :hra, :special_allowance, :other_earnings, :annual_ctc
|
|
14
|
+
|
|
15
|
+
# Gratuity accrues at 15/26 days of Basic a year (4.81%), inside the CTC.
|
|
16
|
+
GRATUITY_RATE = BigDecimal("0.0481")
|
|
17
|
+
|
|
18
|
+
# The CTC is the input: saving one splits it, so no line is typed by hand.
|
|
19
|
+
before_validation :fill_from_ctc, if: -> { annual_ctc.present? }
|
|
20
|
+
# The admin form saves in this context: people only ever type the CTC.
|
|
21
|
+
validates :annual_ctc, presence: true, on: :ctc_form
|
|
16
22
|
|
|
17
23
|
validates :effective_from, presence: true, uniqueness: { scope: :user_id }
|
|
18
24
|
validates :basic, presence: true
|
|
@@ -49,7 +55,7 @@ module HrLite
|
|
|
49
55
|
earnings, deductions, employer = [
|
|
50
56
|
{ "Basic" => basic, "HRA" => hra, "Special allowance" => special_allowance, "Other" => other_earnings },
|
|
51
57
|
{ "PF" => pf&.employee, "ESI" => esi.employee, "Professional tax" => pts.first },
|
|
52
|
-
{ "PF" => pf && (pf.employer_eps + pf.employer_epf), "ESI" => esi.employer }
|
|
58
|
+
{ "PF" => pf && (pf.employer_eps + pf.employer_epf), "ESI" => esi.employer, "Gratuity" => gratuity }
|
|
53
59
|
].map { |rows| rows.select { |_, amount| Money.d(amount).positive? } }
|
|
54
60
|
monthly_ctc = gross + employer.values.sum(BigDecimal(0))
|
|
55
61
|
in_hand = gross - deductions.values.sum(BigDecimal(0))
|
|
@@ -76,14 +82,22 @@ module HrLite
|
|
|
76
82
|
(earlier || self).monthly_gross
|
|
77
83
|
end
|
|
78
84
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
85
|
+
# Only a CTC-split structure carries gratuity in its CTC; older hand-typed
|
|
86
|
+
# ones never did, and their CTC must not move.
|
|
87
|
+
def gratuity
|
|
88
|
+
Money.round_rupee(Money.d(basic) * GRATUITY_RATE) if annual_ctc.present? && basic
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Owner's split of annual_ctc (the labour codes' 50% wage floor): Basic 50%
|
|
92
|
+
# of the monthly CTC, HRA 50% of Basic in a metro and 40% elsewhere,
|
|
93
|
+
# employer PF, ESI and gratuity paid out of the CTC; Special takes the rest.
|
|
81
94
|
def fill_from_ctc(on: effective_from || Date.current)
|
|
82
95
|
monthly_ctc = Money.round_rupee(Money.d(annual_ctc) / 12)
|
|
83
96
|
self.basic = Money.round_rupee(monthly_ctc / 2)
|
|
84
|
-
self.hra = Money.round_rupee(basic * BigDecimal("0.4"))
|
|
97
|
+
self.hra = Money.round_rupee(basic * BigDecimal(metro ? "0.5" : "0.4"))
|
|
98
|
+
self.other_earnings = nil
|
|
85
99
|
self.special_allowance = 0
|
|
86
|
-
rest = monthly_ctc - Money.d(breakup(on: on).dig(:employer, "PF"))
|
|
100
|
+
rest = monthly_ctc - Money.d(breakup(on: on).dig(:employer, "PF")) - Money.d(gratuity)
|
|
87
101
|
# Employer ESI is a share of the gross beside it: gross + ESI = rest.
|
|
88
102
|
gross = (rest / (1 + StatutoryRateCard.for(on.beginning_of_month)[:esi][:employer_rate])).floor
|
|
89
103
|
self.special_allowance = gross - basic - hra - Money.d(other_earnings)
|
|
@@ -53,14 +53,14 @@ module HrLite
|
|
|
53
53
|
hash.to_h { |key, value| [ key, Money.d(value) ] }
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
56
|
+
# Cards take effect on the 1st of a month. Most open a financial year on
|
|
57
|
+
# 1 April; a mid-year card exists for a statutory change notified inside a
|
|
58
|
+
# year (the EPF wage ceiling, 17 Sep 2026) and repeats that year's tax
|
|
59
|
+
# slabs, because TDS projects one annual figure.
|
|
59
60
|
def effective_from_opens_a_financial_year
|
|
60
|
-
return if effective_from.blank?
|
|
61
|
-
return if effective_from == FinancialYear.start_for(effective_from)
|
|
61
|
+
return if effective_from.blank? || effective_from.day == 1
|
|
62
62
|
|
|
63
|
-
errors.add(:effective_from, "must be
|
|
63
|
+
errors.add(:effective_from, "must be the 1st of a month (1 April for a new financial year)")
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
REQUIRED = {
|
|
@@ -47,6 +47,30 @@ module HrLite
|
|
|
47
47
|
tax_declaration_items.sum(BigDecimal(0)) { |i| i.verified_amount || BigDecimal(0) }
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
Deduction = Struct.new(:section, :label, :claimed, :allowed, :cap, keyword_init: true)
|
|
51
|
+
|
|
52
|
+
# Old-regime lines with the law applied: each section's limit, and HRA
|
|
53
|
+
# worked out from the rent against the salary structure — never typed.
|
|
54
|
+
def deduction_rows(structure:)
|
|
55
|
+
tax_declaration_items.reject { |item| item.declared_amount.nil? || item.marked_for_destruction? }.map do |item|
|
|
56
|
+
allowed = item.section == "hra" ? self.class.hra_exemption(item.allowed(verified: verified?), structure) : item.allowed(verified: verified?)
|
|
57
|
+
Deduction.new(section: item.section, label: item.section_label, claimed: Money.d(item.declared_amount),
|
|
58
|
+
allowed: allowed, cap: item.cap)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def old_regime_deductions(structure:) = deduction_rows(structure: structure).sum(BigDecimal(0), &:allowed)
|
|
63
|
+
|
|
64
|
+
# Annual HRA exemption: the least of the HRA received, rent less 10% of
|
|
65
|
+
# Basic, and 50% of Basic in a metro (40% elsewhere).
|
|
66
|
+
def self.hra_exemption(rent, structure)
|
|
67
|
+
return BigDecimal(0) if structure.nil? || Money.d(rent).zero?
|
|
68
|
+
|
|
69
|
+
basic = Money.d(structure.basic) * 12
|
|
70
|
+
[ Money.d(structure.hra) * 12, Money.d(rent) - basic / 10, basic * BigDecimal(structure.metro ? "0.5" : "0.4") ]
|
|
71
|
+
.min.then { |amount| Money.round_rupee([ amount, BigDecimal(0) ].max) }
|
|
72
|
+
end
|
|
73
|
+
|
|
50
74
|
def submit!(actor:)
|
|
51
75
|
raise ActiveRecord::RecordInvalid.new(self), "not a draft" unless draft? || rejected?
|
|
52
76
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<% content_for(:page_title) { "CA review" } %>
|
|
2
|
+
<% fy_label = HrLite::FinancialYear.label(@fy) %>
|
|
3
|
+
<% reviewer = hr_can?("payroll.verify", scope: :all) %>
|
|
4
|
+
<% resolver = hr_can?("payroll.approve", scope: :all) || hr_can?("payroll.manage", scope: :all) %>
|
|
5
|
+
<% keep = ->(extra = {}) { { fy: @fy, tab: @tab, status: @status }.merge(extra).compact } %>
|
|
6
|
+
<div class="hrl-page__head">
|
|
7
|
+
<h1 class="hrl-page__title">CA review — FY <%= fy_label %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
<p class="hrl-small hrl-muted">Every published salary slip and every filed tax declaration, verified by the CA before 31 March <%= @fy.year + 1 %>. A flag goes to HR and payroll; the fix is a correcting line on a later slip or reopening the declaration — a published slip is never edited.</p>
|
|
10
|
+
|
|
11
|
+
<div class="hrl-kpis">
|
|
12
|
+
<% %w[slips declarations].each do |kind| %>
|
|
13
|
+
<% total = @totals[kind] %>
|
|
14
|
+
<% done = total - @left[kind] %>
|
|
15
|
+
<div class="hrl-kpi">
|
|
16
|
+
<div class="hrl-kpi__value"><%= done %> / <%= total %></div>
|
|
17
|
+
<div class="hrl-kpi__label"><%= kind == "slips" ? "Salary slips" : "Tax declarations" %> verified</div>
|
|
18
|
+
<progress class="hrl-tax-cap" max="<%= [ total, 1 ].max %>" value="<%= done %>" aria-hidden="true"></progress>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @left["flagged"] %></div><div class="hrl-kpi__label">Flags open</div></div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="hrl-row hrl-row--mb" role="tablist">
|
|
25
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @tab == "slips" %>" href="<%= hr_lite.admin_ca_reviews_path(keep.(tab: "slips", status: @status)) %>" aria-current="<%= @tab == "slips" ? "page" : nil %>">Salary slips</a>
|
|
26
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @tab == "declarations" %>" href="<%= hr_lite.admin_ca_reviews_path(keep.(tab: "declarations", status: @status)) %>" aria-current="<%= @tab == "declarations" ? "page" : nil %>">Tax declarations</a>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="hrl-row hrl-row--mb">
|
|
29
|
+
<% [ [ nil, "All" ], [ "unreviewed", "To review" ], [ "flagged", "Flagged" ], [ "verified", "Verified" ], [ "resolved", "Resolved" ] ].each do |value, label| %>
|
|
30
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @status == value %>" href="<%= hr_lite.admin_ca_reviews_path(keep.(status: value)) %>"><%= label %></a>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<section class="hrl-card">
|
|
35
|
+
<% if @rows.empty? %>
|
|
36
|
+
<p class="hrl-card--empty">Nothing here.</p>
|
|
37
|
+
<% else %>
|
|
38
|
+
<% subject_type = @rows.first.class.name %>
|
|
39
|
+
<% if reviewer && @rows.any? { |row| @reviews[row.id].nil? } %>
|
|
40
|
+
<%= button_to "Verify all shown", hr_lite.verify_all_admin_ca_reviews_path, method: :post, class: "hrl-btn",
|
|
41
|
+
params: keep.(subject_type: subject_type, ids: @rows.map(&:id)),
|
|
42
|
+
form: { data: { turbo_confirm: "Verify every unreviewed item on this page?" } } %>
|
|
43
|
+
<% end %>
|
|
44
|
+
<ul class="hrl-feed">
|
|
45
|
+
<% @rows.each do |row| %>
|
|
46
|
+
<% review = @reviews[row.id] %>
|
|
47
|
+
<li class="hrl-feed__item">
|
|
48
|
+
<div class="hrl-feed__body">
|
|
49
|
+
<div class="hrl-row">
|
|
50
|
+
<% if row.is_a?(HrLite::SalarySlip) %>
|
|
51
|
+
<a class="hrl-card__link" href="<%= hr_lite.admin_salary_slip_path(row) %>"><%= hr_display_name(row.user) %> — <%= row.period_month.strftime("%B %Y") %></a>
|
|
52
|
+
<span class="hrl-small hrl-muted">net <%= hrl_money(row.net_pay) %></span>
|
|
53
|
+
<% else %>
|
|
54
|
+
<a class="hrl-card__link" href="<%= hr_lite.admin_tax_declaration_path(row) %>"><%= hr_display_name(row.user) %> — <%= row.label %></a>
|
|
55
|
+
<span class="hrl-small hrl-muted"><%= row.regime.humanize %> regime · <%= row.status.humanize %></span>
|
|
56
|
+
<% end %>
|
|
57
|
+
<span class="hrl-right">
|
|
58
|
+
<% badge = { "verified" => [ "hrl-badge--ok", "Verified" ], "flagged" => [ "hrl-badge--bad", "Flagged" ],
|
|
59
|
+
"resolved" => [ "", "Resolved" ] }.fetch(review&.status, [ "hrl-badge--muted", "To review" ]) %>
|
|
60
|
+
<span class="hrl-badge <%= badge.first %>"><%= badge.last %></span>
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
<% if review&.note.present? %><p class="hrl-small">CA: <%= review.note %></p><% end %>
|
|
64
|
+
<% if review&.resolution_note.present? %><p class="hrl-small hrl-muted">Resolved by <%= hr_display_name(review.resolved_by) %>: <%= review.resolution_note %></p><% end %>
|
|
65
|
+
<div class="hrl-row hrl-mt">
|
|
66
|
+
<% if reviewer && review&.status != "verified" %>
|
|
67
|
+
<%= button_to "Verify", hr_lite.verify_admin_ca_reviews_path, method: :post, class: "hrl-btn",
|
|
68
|
+
params: keep.(subject_type: subject_type, subject_id: row.id) %>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% if reviewer %>
|
|
71
|
+
<details>
|
|
72
|
+
<summary class="hrl-btn">Flag…</summary>
|
|
73
|
+
<%= form_with url: hr_lite.flag_admin_ca_reviews_path, method: :post, local: true do %>
|
|
74
|
+
<% keep.(subject_type: subject_type, subject_id: row.id).each do |key, value| %><%= hidden_field_tag key, value %><% end %>
|
|
75
|
+
<div class="hrl-field"><%= label_tag "note_#{row.id}", "What is wrong" %><%= text_area_tag :note, nil, id: "note_#{row.id}", required: true, rows: 2 %></div>
|
|
76
|
+
<%= submit_tag "Flag it", class: "hrl-btn hrl-btn--primary" %>
|
|
77
|
+
<% end %>
|
|
78
|
+
</details>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% if resolver && review&.status == "flagged" %>
|
|
81
|
+
<%= form_with url: hr_lite.resolve_admin_ca_review_path(review, keep.()), method: :post, local: true, class: "hrl-row" do %>
|
|
82
|
+
<%= label_tag "resolution_#{review.id}", "How it was fixed", class: "hrl-sr-only" %>
|
|
83
|
+
<%= text_field_tag :note, nil, id: "resolution_#{review.id}", placeholder: "How it was fixed" %>
|
|
84
|
+
<%= submit_tag "Mark resolved", class: "hrl-btn" %>
|
|
85
|
+
<% end %>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</li>
|
|
90
|
+
<% end %>
|
|
91
|
+
</ul>
|
|
92
|
+
<%= hrl_pagination %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</section>
|
|
@@ -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 %>
|