From 704224c30e1d05acd4f830843583637a07c3cbbc Mon Sep 17 00:00:00 2001 From: frappe-pr-bot Date: Sun, 17 May 2026 10:03:30 +0000 Subject: [PATCH 01/14] chore: update POT file --- hrms/locale/main.pot | 248 +++++++++++++++++++------------------------ 1 file changed, 110 insertions(+), 138 deletions(-) diff --git a/hrms/locale/main.pot b/hrms/locale/main.pot index da96726de6..c16a88815b 100644 --- a/hrms/locale/main.pot +++ b/hrms/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Frappe HR VERSION\n" "Report-Msgid-Bugs-To: contact@frappe.io\n" -"POT-Creation-Date: 2026-05-10 09:59+0000\n" -"PO-Revision-Date: 2026-05-10 09:59+0000\n" +"POT-Creation-Date: 2026-05-17 10:03+0000\n" +"PO-Revision-Date: 2026-05-17 10:03+0000\n" "Last-Translator: contact@frappe.io\n" "Language-Team: contact@frappe.io\n" "MIME-Version: 1.0\n" @@ -221,8 +221,8 @@ msgstr "" msgid "Example: SAL-{first_name}-{date_of_birth.year}
This will generate a password like SAL-Jane-1972" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:314 -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:322 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:315 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:323 msgid "Total Leaves Allocated are more than the number of days in the allocation period" msgstr "" @@ -341,7 +341,7 @@ msgstr "" msgid "Absent Records" msgstr "" -#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:56 +#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:58 msgid "Account No" msgstr "" @@ -460,7 +460,7 @@ msgstr "" msgid "Actual Overtime Duration" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:476 +#: hrms/hr/doctype/leave_application/leave_application.py:469 msgid "Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation." msgstr "" @@ -522,7 +522,7 @@ msgstr "" msgid "Additional Information " msgstr "" -#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:46 +#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:48 msgid "Additional PF" msgstr "" @@ -565,7 +565,7 @@ msgstr "" msgid "Adjust Allocation" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:452 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:453 msgid "Adjustment Created Successfully" msgstr "" @@ -579,15 +579,15 @@ msgstr "" msgid "Advance" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:83 +#: hrms/hr/doctype/employee_advance/employee_advance.py:78 msgid "Advance Account Required" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:87 +#: hrms/hr/doctype/employee_advance/employee_advance.py:82 msgid "Advance Account is mandatory. Please set the {0} in the Company {1} and submit this document." msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:130 +#: hrms/hr/doctype/employee_advance/employee_advance.py:125 msgid "Advance Account {} currency should be same as Salary Currency of Employee {}. Please select same currency Advance Account" msgstr "" @@ -603,7 +603,7 @@ msgstr "" msgid "Advanced Filters" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:476 +#: hrms/hr/doctype/expense_claim/expense_claim.py:464 msgid "All Exchange Gain/Loss amount of {0} has been booked through {1}" msgstr "" @@ -714,7 +714,7 @@ msgstr "" #. Label of the applicable_after (Int) field in DocType 'Leave Type' #: hrms/hr/doctype/leave_type/leave_type.json -msgid "Allow Leave Application After (Working Days)" +msgid "Allow Leave Application After (Calendar Days)" msgstr "" #. Label of the allow_multiple_shift_assignments (Check) field in DocType 'HR @@ -842,7 +842,7 @@ msgstr "" msgid "Annual Allocation" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:406 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:407 msgid "Annual Allocation Exceeded" msgstr "" @@ -925,11 +925,11 @@ msgstr "" msgid "Application Web Form Route" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:248 +#: hrms/hr/doctype/leave_application/leave_application.py:241 msgid "Application period cannot be across two allocation records" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:245 +#: hrms/hr/doctype/leave_application/leave_application.py:238 msgid "Application period cannot be outside leave allocation period" msgstr "" @@ -1117,7 +1117,7 @@ msgstr "" msgid "Approval Status" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:202 +#: hrms/hr/doctype/expense_claim/expense_claim.py:198 msgid "Approval Status must be 'Approved' or 'Rejected'" msgstr "" @@ -1399,7 +1399,7 @@ msgstr "" msgid "Attendance for employee {0} is already marked for the date {1}: {2}" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:641 +#: hrms/hr/doctype/leave_application/leave_application.py:634 msgid "Attendance for employee {0} is already marked for the following dates: {1}" msgstr "" @@ -1539,11 +1539,11 @@ msgstr "" msgid "Awaiting Response" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:216 +#: hrms/hr/doctype/leave_application/leave_application.py:209 msgid "Backdated Leave Application is restricted. Please set the {} in {}" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.js:290 +#: hrms/hr/doctype/expense_claim/expense_claim.js:281 msgid "Bank Entries" msgstr "" @@ -1776,15 +1776,15 @@ msgstr "" msgid "Cannot Modify Time" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:357 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:358 msgid "Cannot allocate leaves outside the allocation period {0} - {1}" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:486 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:487 msgid "Cannot allocate more leaves due to maximum leave allocation limit of {0} in leave policy assignment" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:478 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:479 msgid "Cannot allocate more leaves due to maximum leaves allowed limit of {0} in {1} leave type." msgstr "" @@ -1820,7 +1820,7 @@ msgstr "" msgid "Cannot create or change transactions against an Appraisal Cycle with status {0}." msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:658 +#: hrms/hr/doctype/leave_application/leave_application.py:651 msgid "Cannot find active Leave Period" msgstr "" @@ -1901,7 +1901,7 @@ msgstr "" msgid "Check Vacancies On Job Offer Creation" msgstr "" -#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:488 +#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:492 #: hrms/hr/utils.py:947 msgid "Check {0} for more details" msgstr "" @@ -1990,13 +1990,6 @@ msgstr "" msgid "Claims" msgstr "" -#. Option for the 'Status' (Select) field in DocType 'Interview' -#. Option for the 'Result' (Select) field in DocType 'Interview Feedback' -#: hrms/hr/doctype/interview/interview.json -#: hrms/hr/doctype/interview_feedback/interview_feedback.json -msgid "Cleared" -msgstr "" - #: hrms/payroll/doctype/salary_slip/salary_slip.js:300 msgid "Click {0} to change the configuration and then resave salary slip" msgstr "" @@ -2491,7 +2484,7 @@ msgstr "" msgid "Deduction Reports" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.js:84 +#: hrms/hr/doctype/employee_advance/employee_advance.js:79 msgid "Deduction from Salary" msgstr "" @@ -2565,7 +2558,7 @@ msgstr "" msgid "Department Wise Openings" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:161 +#: hrms/hr/doctype/expense_claim/expense_claim.py:157 msgid "Department {0} does not belong to company: {1}" msgstr "" @@ -3341,8 +3334,8 @@ msgstr "" msgid "Employee Transfer cannot be submitted before Transfer Date" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:120 -#: hrms/hr/doctype/employee_advance/employee_advance.py:190 +#: hrms/hr/doctype/employee_advance/employee_advance.py:115 +#: hrms/hr/doctype/employee_advance/employee_advance.py:185 msgid "Employee advance account {0} should be of type {1}." msgstr "" @@ -3397,7 +3390,7 @@ msgstr "" msgid "Employee {0} has already applied for Shift {1}: {2} that overlaps within this period" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:530 +#: hrms/hr/doctype/leave_application/leave_application.py:523 msgid "Employee {0} has already applied for {1} between {2} and {3} : {4}" msgstr "" @@ -3953,7 +3946,7 @@ msgstr "" #. Label of the expense_type (Data) field in DocType 'Expense Claim Type' #. Label of a Link in the Expenses Workspace #. Label of a Workspace Sidebar Item -#: hrms/hr/doctype/expense_claim/expense_claim.py:747 +#: hrms/hr/doctype/expense_claim/expense_claim.py:662 #: hrms/hr/doctype/expense_claim_detail/expense_claim_detail.json #: hrms/hr/doctype/expense_claim_type/expense_claim_type.json #: hrms/hr/workspace/expenses/expenses.json @@ -4028,7 +4021,7 @@ msgstr "" msgid "Expire Carry Forwarded Leaves (Days)" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:608 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:609 msgid "Expire Leaves" msgstr "" @@ -4070,7 +4063,7 @@ msgstr "" msgid "Failed to setup defaults for country {0}." msgstr "" -#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:485 +#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:489 msgid "Failed to submit some leave policy assignments:" msgstr "" @@ -4649,7 +4642,7 @@ msgstr "" #: frontend/src/components/SalarySlipItem.vue:13 #: hrms/payroll/doctype/salary_slip/salary_slip.json #: hrms/payroll/report/income_tax_deductions/income_tax_deductions.py:54 -#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:42 +#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:44 #: hrms/payroll/report/salary_register/salary_register.py:211 msgid "Gross Pay" msgstr "" @@ -4689,7 +4682,7 @@ msgstr "" #. Name of a DocType #. Label of a Link in the HR Setup Workspace #: hrms/hr/doctype/hr_settings/hr_settings.json -#: hrms/hr/doctype/leave_application/leave_application.py:218 +#: hrms/hr/doctype/leave_application/leave_application.py:211 #: hrms/hr/workspace/hr_setup/hr_setup.json msgid "HR Settings" msgstr "" @@ -4730,7 +4723,7 @@ msgstr "" msgid "Half Day Date" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:916 +#: hrms/hr/doctype/leave_application/leave_application.py:909 msgid "Half Day Date cannot be a holiday" msgstr "" @@ -4738,7 +4731,7 @@ msgstr "" msgid "Half Day Date is mandatory" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:919 +#: hrms/hr/doctype/leave_application/leave_application.py:912 msgid "Half Day Date should be between From Date and To Date" msgstr "" @@ -4870,7 +4863,7 @@ msgstr "" msgid "House rented dates should be atleast 15 days apart" msgstr "" -#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:60 +#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:62 msgid "IFSC" msgstr "" @@ -5200,11 +5193,11 @@ msgstr "" msgid "Install Frappe HR" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:489 +#: hrms/hr/doctype/leave_application/leave_application.py:482 msgid "Insufficient Balance" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:487 +#: hrms/hr/doctype/leave_application/leave_application.py:480 msgid "Insufficient leave balance for Leave Type {0}" msgstr "" @@ -5398,7 +5391,7 @@ msgstr "" msgid "Invalid Benefit Amounts" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:360 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:361 msgid "Invalid Dates" msgstr "" @@ -5419,7 +5412,7 @@ msgstr "" msgid "Invalid Shift Times" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:995 +#: hrms/hr/doctype/expense_claim/expense_claim.py:876 msgid "Invalid parameters provided. Please pass the required arguments." msgstr "" @@ -5882,7 +5875,7 @@ msgstr "" msgid "Leave Application" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:811 +#: hrms/hr/doctype/leave_application/leave_application.py:804 msgid "Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}." msgstr "" @@ -5975,7 +5968,7 @@ msgstr "" msgid "Leave Block List Name" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:1447 +#: hrms/hr/doctype/leave_application/leave_application.py:1440 msgid "Leave Blocked" msgstr "" @@ -6168,7 +6161,7 @@ msgstr "" msgid "Leave Type {0} cannot be allocated since it is leave without pay" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:597 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:598 msgid "Leave Type {0} cannot be carry-forwarded" msgstr "" @@ -6187,7 +6180,7 @@ msgstr "" msgid "Leave Without Pay does not match with approved {} records" msgstr "" -#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:151 +#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:155 msgid "Leave allocation is skipped for {0}, because number of leaves to be allocated is 0." msgstr "" @@ -6207,7 +6200,7 @@ msgstr "" msgid "Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:286 +#: hrms/hr/doctype/leave_application/leave_application.py:279 msgid "Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}" msgstr "" @@ -6216,7 +6209,7 @@ msgstr "" msgid "Leave for optional holiday" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:557 +#: hrms/hr/doctype/leave_application/leave_application.py:550 msgid "Leave of type {0} cannot be longer than {1}." msgstr "" @@ -6270,7 +6263,7 @@ msgstr "" msgid "Leaves Allocated" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:602 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:603 msgid "Leaves Expired" msgstr "" @@ -6400,7 +6393,7 @@ msgstr "" msgid "Lower Range" msgstr "" -#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:61 +#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:63 msgid "MICR" msgstr "" @@ -6573,7 +6566,7 @@ msgstr "" msgid "Maximum Consecutive Leaves Allowed" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:567 +#: hrms/hr/doctype/leave_application/leave_application.py:560 msgid "Maximum Consecutive Leaves Exceeded" msgstr "" @@ -6656,13 +6649,13 @@ msgstr "" msgid "Minimum Year for Gratuity" msgstr "" -#. Description of the 'Allow Leave Application After (Working Days)' (Int) +#. Description of the 'Allow Leave Application After (Calendar Days)' (Int) #. field in DocType 'Leave Type' #: hrms/hr/doctype/leave_type/leave_type.json -msgid "Minimum working days required since Date of Joining to apply for this leave" +msgid "Minimum calendar days required since Date of Joining to apply for this leave" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:95 +#: hrms/hr/doctype/employee_advance/employee_advance.py:90 msgid "Missing Advance Account" msgstr "" @@ -6678,7 +6671,7 @@ msgstr "" msgid "Missing Relieving Date" msgstr "" -#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:18 +#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:20 msgid "Missing Salary Components" msgstr "" @@ -6691,7 +6684,7 @@ msgstr "" msgid "Mode of Travel" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:493 +#: hrms/hr/doctype/expense_claim/expense_claim.py:481 msgid "Mode of payment is required to make a payment" msgstr "" @@ -6766,7 +6759,7 @@ msgstr "" #. Label of the net_pay (Currency) field in DocType 'Salary Structure' #: hrms/payroll/doctype/salary_slip/salary_slip.json #: hrms/payroll/doctype/salary_structure/salary_structure.json -#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:49 +#: hrms/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py:51 #: hrms/payroll/report/salary_register/salary_register.py:251 msgid "Net Pay" msgstr "" @@ -6836,7 +6829,7 @@ msgstr "" msgid "New shift assignments will be created after this date." msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:426 +#: hrms/hr/doctype/employee_advance/employee_advance.py:380 msgid "No Bank/Cash Account found for currency {0}. Please create one under company {1}." msgstr "" @@ -7270,7 +7263,7 @@ msgstr "" msgid "Only interviewers can submit feedback" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:224 +#: hrms/hr/doctype/leave_application/leave_application.py:217 msgid "Only users with the {0} role can create backdated leave applications" msgstr "" @@ -7305,7 +7298,7 @@ msgstr "" msgid "Opening closed." msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:664 +#: hrms/hr/doctype/leave_application/leave_application.py:657 msgid "Optional Holiday List not set for leave period {0}" msgstr "" @@ -7336,7 +7329,7 @@ msgstr "" msgid "Outgoing Salary" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:326 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:327 msgid "Over Allocation" msgstr "" @@ -7500,15 +7493,15 @@ msgstr "" msgid "PAN Number" msgstr "" -#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:43 +#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:45 msgid "PF Account" msgstr "" -#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:44 +#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:46 msgid "PF Amount" msgstr "" -#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:51 +#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:53 msgid "PF Loan" msgstr "" @@ -7581,7 +7574,7 @@ msgstr "" msgid "Pay via Salary Slip" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:191 +#: hrms/hr/doctype/expense_claim/expense_claim.py:187 msgid "Payable Account is mandatory to submit an Expense Claim" msgstr "" @@ -8044,15 +8037,15 @@ msgstr "" msgid "Please set account in Salary Component {0}" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:719 +#: hrms/hr/doctype/leave_application/leave_application.py:712 msgid "Please set default template for Leave Approval Notification in HR Settings." msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:694 +#: hrms/hr/doctype/leave_application/leave_application.py:687 msgid "Please set default template for Leave Status Notification in HR Settings." msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:79 +#: hrms/hr/doctype/employee_advance/employee_advance.py:74 msgid "Please set the Advance Account {0} or in {1}" msgstr "" @@ -8060,7 +8053,7 @@ msgstr "" msgid "Please set the Appraisal Template for all the {0} or select the template in the Employees table below." msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.js:527 +#: hrms/hr/doctype/expense_claim/expense_claim.js:514 msgid "Please set the Company" msgstr "" @@ -8126,11 +8119,6 @@ msgstr "" msgid "Please update your status for this training event" msgstr "" -#. Label of the posted_on (Datetime) field in DocType 'Job Opening' -#: hrms/hr/doctype/job_opening/job_opening.json -msgid "Posted On" -msgstr "" - #. Label of the posting_date (Date) field in DocType 'Gratuity' #: hrms/payroll/doctype/gratuity/gratuity.json msgid "Posting date" @@ -8484,8 +8472,8 @@ msgstr "" msgid "Reduction is more than {0}'s available leave balance {1} for leave type {2}" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:405 -#: hrms/hr/doctype/leave_application/leave_application.py:561 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:406 +#: hrms/hr/doctype/leave_application/leave_application.py:554 #: hrms/payroll/doctype/additional_salary/additional_salary.py:218 #: hrms/payroll/doctype/payroll_entry/payroll_entry.py:141 msgid "Reference: {0}" @@ -8716,8 +8704,8 @@ msgstr "" msgid "Retirement Age (In Years)" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:481 -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:489 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:482 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:490 msgid "Retry Failed" msgstr "" @@ -8735,7 +8723,7 @@ msgstr "" msgid "Retrying allocations" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:236 +#: hrms/hr/doctype/employee_advance/employee_advance.py:231 msgid "Return amount cannot be greater than unclaimed amount" msgstr "" @@ -8763,7 +8751,7 @@ msgstr "" #. Label of the role_allowed_to_create_backdated_leave_application (Link) field #. in DocType 'HR Settings' #: hrms/hr/doctype/hr_settings/hr_settings.json -#: hrms/hr/doctype/leave_application/leave_application.py:217 +#: hrms/hr/doctype/leave_application/leave_application.py:210 msgid "Role Allowed to Create Backdated Leave Application" msgstr "" @@ -8813,11 +8801,11 @@ msgstr "" msgid "Row #{0}: Timesheet amount will overwrite the Earning component amount for the Salary Component {1}" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:963 +#: hrms/hr/doctype/expense_claim/expense_claim.py:844 msgid "Row No {0}: Amount cannot be greater than the Outstanding Amount against Expense Claim {1}. Outstanding Amount is {2}" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:561 +#: hrms/hr/doctype/expense_claim/expense_claim.py:549 msgid "Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2}" msgstr "" @@ -8829,7 +8817,7 @@ msgstr "" msgid "Row {0}# Paid Amount cannot be greater than Total amount" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:228 +#: hrms/hr/doctype/employee_advance/employee_advance.py:223 msgid "Row {0}# Paid Amount cannot be greater than requested advance amount" msgstr "" @@ -8849,7 +8837,7 @@ msgstr "" msgid "Row {0}: {1}" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:486 +#: hrms/hr/doctype/expense_claim/expense_claim.py:474 msgid "Row {0}: {1} is required in the expenses table to book an expense claim." msgstr "" @@ -9173,7 +9161,7 @@ msgstr "" msgid "Salary Withholding {0} already exists for employee {1} for the selected period" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:402 +#: hrms/hr/doctype/leave_application/leave_application.py:395 msgid "Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range." msgstr "" @@ -9183,7 +9171,7 @@ msgstr "" msgid "Salary breakup based on Earning and Deduction." msgstr "" -#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:15 +#: hrms/payroll/report/provident_fund_deductions/provident_fund_deductions.py:17 msgid "Salary components of type Provident Fund, Additional Provident Fund or Provident Fund Loan are not set up." msgstr "" @@ -9210,7 +9198,7 @@ msgstr "" msgid "Sanctioned Amount (Company Currency)" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:585 +#: hrms/hr/doctype/expense_claim/expense_claim.py:573 msgid "Sanctioned Amount cannot be greater than Claim Amount in Row {0}." msgstr "" @@ -9286,7 +9274,7 @@ msgstr "" msgid "Select Users" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.js:572 +#: hrms/hr/doctype/expense_claim/expense_claim.js:559 msgid "Select an employee to get the employee advance." msgstr "" @@ -9344,11 +9332,11 @@ msgstr "" msgid "Select your Leave Approver i.e. the person who approves or rejects your leaves." msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.js:607 +#: hrms/hr/doctype/expense_claim/expense_claim.js:594 msgid "Selected employee advance is not of employee {0}" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:554 +#: hrms/hr/doctype/expense_claim/expense_claim.py:542 msgid "Selected employee advance is not of employee {}" msgstr "" @@ -9376,11 +9364,11 @@ msgstr "" msgid "Self-Study" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:175 +#: hrms/hr/doctype/expense_claim/expense_claim.py:171 msgid "Self-approval for Expense Claims is not allowed" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:902 +#: hrms/hr/doctype/leave_application/leave_application.py:895 msgid "Self-approval for leaves is not allowed" msgstr "" @@ -9498,7 +9486,7 @@ msgstr "" msgid "Set optional filters to fetch employees in the appraisee list" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:746 +#: hrms/hr/doctype/expense_claim/expense_claim.py:661 msgid "Set the default account for the {0} {1}" msgstr "" @@ -9981,7 +9969,7 @@ msgstr "" msgid "Submission Date" msgstr "" -#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:496 +#: hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:500 msgid "Submission Failed" msgstr "" @@ -10222,7 +10210,7 @@ msgstr "" msgid "The day of the month when leaves should be allocated" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:445 +#: hrms/hr/doctype/leave_application/leave_application.py:438 msgid "The day(s) on which you are applying for leave are holidays. You need not apply for leave." msgstr "" @@ -10353,10 +10341,6 @@ msgstr "" msgid "This is based on the attendance of this Employee" msgstr "" -#: hrms/www/hrms.py:20 -msgid "This method is only meant for developer mode" -msgstr "" - #: hrms/payroll/doctype/additional_salary/additional_salary.py:231 msgid "This will overwrite the tax component {0} in the salary slip and tax won't be calculated based on the Income Tax Slabs" msgstr "" @@ -10437,7 +10421,7 @@ msgid "To date can not greater than employee's relieving date" msgstr "" #: hrms/hr/doctype/leave_allocation/leave_allocation.py:214 -#: hrms/hr/doctype/leave_application/leave_application.py:230 +#: hrms/hr/doctype/leave_application/leave_application.py:223 msgid "To date cannot be before from date" msgstr "" @@ -10546,7 +10530,7 @@ msgstr "" #. Label of the total_deduction (Currency) field in DocType 'Salary Structure' #: hrms/payroll/doctype/salary_slip/salary_slip.json #: hrms/payroll/doctype/salary_structure/salary_structure.json -#: hrms/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py:148 +#: hrms/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py:155 #: hrms/payroll/report/salary_register/salary_register.py:244 msgid "Total Deduction" msgstr "" @@ -10611,7 +10595,7 @@ msgstr "" msgid "Total Goal Score" msgstr "" -#: hrms/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py:141 +#: hrms/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py:148 msgid "Total Gross Pay" msgstr "" @@ -10661,7 +10645,7 @@ msgstr "" msgid "Total Loan Repayment" msgstr "" -#: hrms/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py:155 +#: hrms/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py:162 msgid "Total Net Pay" msgstr "" @@ -10731,7 +10715,7 @@ msgstr "" msgid "Total Self Score" msgstr "" -#: hrms/hr/doctype/expense_claim/expense_claim.py:579 +#: hrms/hr/doctype/expense_claim/expense_claim.py:567 msgid "Total advance amount cannot be greater than total sanctioned amount" msgstr "" @@ -10753,11 +10737,11 @@ msgstr "" msgid "Total in words (Company Currency)" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:401 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:402 msgid "Total leaves allocated cannot exceed annual allocation of {0}." msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:287 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:288 msgid "Total leaves allocated is mandatory for Leave Type {0}" msgstr "" @@ -11156,11 +11140,6 @@ msgstr "" msgid "Upload images or documents" msgstr "" -#: frontend/src/components/FormView.vue:124 -#: frontend/src/components/FormView.vue:163 -msgid "Uploading..." -msgstr "" - #. Label of the upper_range (Currency) field in DocType 'Job Applicant' #. Label of the upper_range (Currency) field in DocType 'Job Opening' #. Label of the upper_range (Currency) field in DocType 'Job Opening Template' @@ -11214,13 +11193,6 @@ msgstr "" msgid "Value missing" msgstr "" -#. Label of the variable (Currency) field in DocType 'Salary Structure -#. Assignment' -#: hrms/payroll/doctype/bulk_salary_structure_assignment/bulk_salary_structure_assignment.js:185 -#: hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json -msgid "Variable" -msgstr "" - #. Label of the variable_based_on_taxable_salary (Check) field in DocType #. 'Salary Component' #. Label of the variable_based_on_taxable_salary (Check) field in DocType @@ -11297,15 +11269,15 @@ msgstr "" msgid "WARNING: Loan Management module has been separated from ERPNext." msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:472 +#: hrms/hr/doctype/leave_application/leave_application.py:465 msgid "Warning: Insufficient leave balance for Leave Type {0} in this allocation." msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:480 +#: hrms/hr/doctype/leave_application/leave_application.py:473 msgid "Warning: Insufficient leave balance for Leave Type {0}." msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:418 +#: hrms/hr/doctype/leave_application/leave_application.py:411 msgid "Warning: Leave application contains following block dates" msgstr "" @@ -11491,7 +11463,7 @@ msgstr "" msgid "Yes, Proceed" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:428 +#: hrms/hr/doctype/leave_application/leave_application.py:421 msgid "You are not authorized to approve leaves on Block Dates" msgstr "" @@ -11523,8 +11495,8 @@ msgstr "" msgid "You cannot reverse more than the total LWP days {0}. You have already reversed {1} days for this employee." msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:457 -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:620 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:458 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:621 msgid "You do not have permission to complete this action" msgstr "" @@ -11604,7 +11576,7 @@ msgstr "" msgid "created" msgstr "" -#: hrms/hr/doctype/employee_advance/employee_advance.py:80 +#: hrms/hr/doctype/employee_advance/employee_advance.py:75 msgid "here" msgstr "" @@ -11723,8 +11695,8 @@ msgstr "" msgid "{0} already has an active Shift Assignment {1} for some/all of these dates." msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:201 -msgid "{0} applicable after {1} working days" +#: hrms/hr/doctype/leave_application/leave_application.py:194 +msgid "{0} applicable after {1} calendar days" msgstr "" #: frontend/src/components/LeaveBalance.vue:37 @@ -11769,19 +11741,19 @@ msgstr "" msgid "{0} is not allowed to submit Interview Feedback for the Interview: {1}" msgstr "" -#: hrms/hr/doctype/leave_application/leave_application.py:672 +#: hrms/hr/doctype/leave_application/leave_application.py:665 msgid "{0} is not in Optional Holiday List" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:395 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:396 msgid "{0} leaves allocated successfully" msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:603 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:604 msgid "{0} leaves from allocation for {1} leave type have expired and will be processed during the next scheduled job. It is recommended to expire them now before creating new leave policy assignments." msgstr "" -#: hrms/hr/doctype/leave_allocation/leave_allocation.py:390 +#: hrms/hr/doctype/leave_allocation/leave_allocation.py:391 msgid "{0} leaves were manually allocated by {1} on {2}" msgstr "" From 52d3dbd22410deefd7383a9f3eb044a719617f87 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 5 May 2026 14:37:30 +0530 Subject: [PATCH 02/14] feat: Employee CTC Breakup report --- .../employee_ctc_break_up.js | 52 ++++ .../employee_ctc_break_up.json | 34 +++ .../employee_ctc_break_up.py | 262 ++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js create mode 100644 hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json create mode 100644 hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js new file mode 100644 index 0000000000..832c15ab6b --- /dev/null +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js @@ -0,0 +1,52 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Employee CTC Break-up"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + { + fieldname: "employee", + label: __("Employee"), + fieldtype: "Link", + options: "Employee", + reqd: 1, + get_query: function () { + let company = frappe.query_report.get_filter_value("company"); + return { + filters: { + company: company, + }, + }; + }, + }, + { + fieldname: "salary_structure_assignment", + label: __("Salary Structure Assignment"), + fieldtype: "Link", + options: "Salary Structure Assignment", + reqd: 1, + get_query: function () { + let employee = frappe.query_report.get_filter_value("employee"); + if (!employee) return; + return { + filters: { + employee: employee, + docstatus: 1, + }, + }; + }, + }, + ], + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (data?.bold) value = `${value}`; + return value; + }, +}; diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json new file mode 100644 index 0000000000..b1edf08492 --- /dev/null +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json @@ -0,0 +1,34 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2026-04-15 23:59:12.988472", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": null, + "modified": "2026-04-15 23:59:12.988472", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee CTC Break-up", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Structure", + "report_name": "Employee CTC Break-up", + "report_type": "Script Report", + "roles": [ + { + "role": "HR Manager" + }, + { + "role": "System Manager" + }, + { + "role": "HR User" + } + ], + "timeout": 0 +} diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py new file mode 100644 index 0000000000..024a5aeffd --- /dev/null +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -0,0 +1,262 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import flt + +from hrms.payroll.doctype.salary_structure.salary_structure import make_salary_slip + + +class SalaryBreakupReport: + def __init__(self, employee, salary_structure_assignment): + self.employee = employee + self.ctc = frappe.db.get_value("Employee", employee, "ctc") + self.salary_structure, self.currency = frappe.get_value( + "Salary Structure Assignment", salary_structure_assignment, ["salary_structure", "currency"] + ) + self.salary_slip = make_salary_slip( + self.salary_structure, employee=self.employee, for_preview=1, as_print=False + ) + self.net_pay = self.salary_slip.net_pay + self.gross_pay = self.salary_slip.gross_pay + + self.salary_components = [] + self.earning_components = [] + self.deduction_components = [] + self.tax_components = [] + self.total_net_earnings = [] + self.total_gross_earnings = [] + + def get_data(self): + self.set_salary_component_details() + self.calculate_yearly_amounts_and_percent_of_ctc() + self.indent_salary_components() + self.separate_salary_components_by_type() + self.set_type_and_formula() + self.set_totals_row_for_component_types() + self.set_net_and_gross_earning_rows() + + return ( + self.earning_components + + self.deduction_components + + self.tax_components + + self.total_net_earnings + + self.total_gross_earnings + ) + + def set_salary_component_details(self): + salary_component_details = frappe.db.get_all( + "Salary Detail", + filters={"parent": self.salary_structure}, + fields=["salary_component", "amount_based_on_formula", "formula"], + ) + + self.salary_components = [ + { + "salary_component": component.salary_component, + "monthly": component.amount, + "abbr": component.abbr, + "is_tax_component": component.variable_based_on_taxable_salary, + "component_type": component.parentfield, + } + for component in self.salary_slip.earnings + self.salary_slip.deductions + ] + + for component in self.salary_components: + component_details = next( + ( + detail + for detail in salary_component_details + if component.get("salary_component") == detail.salary_component + ), + {}, + ) + component.update(component_details) + + def calculate_yearly_amounts_and_percent_of_ctc(self): + for component in self.salary_components: + annual_amount = component.get("monthly", 0) * 12 + component.update( + { + "annual": flt(annual_amount, 2), + "percent_of_ctc": self.calculate_percent_of_ctc(annual_amount), + } + ) + + def separate_salary_components_by_type(self): + self.earning_components = [ + component for component in self.salary_components if component.get("component_type") == "earnings" + ] + self.deduction_components = [ + component + for component in self.salary_components + if component.get("component_type") == "deductions" and not component.get("is_tax_component") + ] + self.tax_components = [ + component for component in self.salary_components if component.get("is_tax_component") + ] + + def set_type_and_formula(self): + for component in self.earning_components + self.deduction_components: + component["type"] = "Formula" if component.get("amount_based_on_formula") else "Fixed" + component["formula"] = ( + component.get("formula") or "-" + if component.get("amount_based_on_formula") + else component.get("monthly") or "-" + ) + + def set_totals_row_for_component_types(self): + def calculate_total(period, components): + total = 0 + for component in components: + total += component.get(period) + return total + + def set_totals_row(component_type): + components = { + "Earnings": self.earning_components, + "Deductions": self.deduction_components, + "Tax Deductions": self.tax_components, + }.get(component_type) + totals_row = { + "salary_component": component_type, + "type": "", + "formula": "", + "bold": True, + "monthly": calculate_total("monthly", components), + "annual": calculate_total("annual", components), + "percent_of_ctc": self.calculate_percent_of_ctc(calculate_total("annual", components)), + } + components.insert(0, totals_row) + + for component_type in ("Earnings", "Deductions", "Tax Deductions"): + set_totals_row(component_type) + + def set_net_and_gross_earning_rows(self): + self.total_net_earnings = [ + { + "salary_component": "Total Net Earnings", + "type": "", + "formula": "", + "monthly": self.net_pay, + "annual": self.net_pay * 12, + "percent_of_ctc": self.calculate_percent_of_ctc(self.net_pay), + "bold": True, + } + ] + self.total_gross_earnings = [ + { + "salary_component": "Total Gross Earnings", + "type": "", + "formula": "", + "monthly": self.gross_pay, + "annual": self.gross_pay * 12, + "percent_of_ctc": self.calculate_percent_of_ctc(self.gross_pay), + "bold": True, + } + ] + + def calculate_percent_of_ctc(self, amount): + return flt(amount * 100 / self.ctc, 2) + + def indent_salary_components(self): + for component in self.salary_components: + component["indent"] = 1 + + def get_summary(self): + monthly_ctc = flt(self.ctc / 12, 2) + return [ + {"value": self.ctc, "label": _("Annual CTC"), "datatype": "Currency", "currency": self.currency}, + { + "value": monthly_ctc, + "label": _("Monthly CTC"), + "datatype": "Currency", + "currency": self.currency, + }, + { + "value": self.gross_pay, + "label": _("Monthly Gross Pay"), + "datatype": "Currency", + "currency": self.currency, + }, + { + "value": self.net_pay, + "label": _("Monthly Net Pay"), + "datatype": "Currency", + "currency": self.currency, + }, + ] + + def get_columns(self) -> list[dict]: + """Return columns for the report. + + One field definition per column, just like a DocType field definition. + """ + return [ + { + "label": _("Component"), + "fieldname": "salary_component", + "fieldtype": "Data", + "width": 300, + }, + { + "label": _("Type"), + "fieldname": "type", + "fieldtype": "Data", + "width": 200, + }, + { + "label": _("Formula/Amount"), + "fieldname": "formula", + "fieldtype": "Data", + "width": 200, + }, + { + "label": _("Monthly"), + "fieldname": "monthly", + "fieldtype": "Currency", + "width": 200, + "options": "currency", + }, + { + "label": _("Annual"), + "fieldname": "annual", + "fieldtype": "Currency", + "width": 200, + "options": "currency", + }, + { + "label": _("Percent of CTC (%)"), + "fieldname": "percent_of_ctc", + "fieldtype": "Percent", + "width": 200, + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + "value": self.currency, + }, + ] + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + + salary_structure_assignment = filters.get("salary_structure_assignment") + employee = filters.get("employee") + salary_breakup_report = SalaryBreakupReport(employee, salary_structure_assignment) + + data = salary_breakup_report.get_data() + summary = salary_breakup_report.get_summary() + columns = salary_breakup_report.get_columns() + + return columns, data, None, None, summary From a5e23ef54530876ea7d6617a3216eeb9da1c6a30 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 5 May 2026 16:55:01 +0530 Subject: [PATCH 03/14] feat: support for all available payroll frequencies --- .../employee_ctc_break_up.py | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index 024a5aeffd..69aedcbf6e 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -20,7 +20,14 @@ def __init__(self, employee, salary_structure_assignment): ) self.net_pay = self.salary_slip.net_pay self.gross_pay = self.salary_slip.gross_pay - + self.payroll_frequency = self.salary_slip.payroll_frequency + self.cycle_multiplier = { + "Monthly": 12, + "Fortnightly": 24, + "Bimonthly": 6, + "Weekly": 52, + "Daily": 365, + }.get(self.payroll_frequency) self.salary_components = [] self.earning_components = [] self.deduction_components = [] @@ -55,7 +62,7 @@ def set_salary_component_details(self): self.salary_components = [ { "salary_component": component.salary_component, - "monthly": component.amount, + "per_cycle": component.amount, "abbr": component.abbr, "is_tax_component": component.variable_based_on_taxable_salary, "component_type": component.parentfield, @@ -76,7 +83,7 @@ def set_salary_component_details(self): def calculate_yearly_amounts_and_percent_of_ctc(self): for component in self.salary_components: - annual_amount = component.get("monthly", 0) * 12 + annual_amount = component.get("per_cycle", 0) * self.cycle_multiplier component.update( { "annual": flt(annual_amount, 2), @@ -103,7 +110,7 @@ def set_type_and_formula(self): component["formula"] = ( component.get("formula") or "-" if component.get("amount_based_on_formula") - else component.get("monthly") or "-" + else component.get("per_cycle") or "-" ) def set_totals_row_for_component_types(self): @@ -124,7 +131,7 @@ def set_totals_row(component_type): "type": "", "formula": "", "bold": True, - "monthly": calculate_total("monthly", components), + "per_cycle": calculate_total("per_cycle", components), "annual": calculate_total("annual", components), "percent_of_ctc": self.calculate_percent_of_ctc(calculate_total("annual", components)), } @@ -139,9 +146,9 @@ def set_net_and_gross_earning_rows(self): "salary_component": "Total Net Earnings", "type": "", "formula": "", - "monthly": self.net_pay, - "annual": self.net_pay * 12, - "percent_of_ctc": self.calculate_percent_of_ctc(self.net_pay), + "per_cycle": self.net_pay, + "annual": self.net_pay * self.cycle_multiplier, + "percent_of_ctc": self.calculate_percent_of_ctc(self.net_pay * self.cycle_multiplier), "bold": True, } ] @@ -150,9 +157,9 @@ def set_net_and_gross_earning_rows(self): "salary_component": "Total Gross Earnings", "type": "", "formula": "", - "monthly": self.gross_pay, - "annual": self.gross_pay * 12, - "percent_of_ctc": self.calculate_percent_of_ctc(self.gross_pay), + "per_cycle": self.gross_pay, + "annual": self.gross_pay * self.cycle_multiplier, + "percent_of_ctc": self.calculate_percent_of_ctc(self.gross_pay * self.cycle_multiplier), "bold": True, } ] @@ -165,24 +172,24 @@ def indent_salary_components(self): component["indent"] = 1 def get_summary(self): - monthly_ctc = flt(self.ctc / 12, 2) + per_cycle_ctc = flt(self.ctc / self.cycle_multiplier, 2) return [ {"value": self.ctc, "label": _("Annual CTC"), "datatype": "Currency", "currency": self.currency}, { - "value": monthly_ctc, - "label": _("Monthly CTC"), + "value": per_cycle_ctc, + "label": _(f"{self.payroll_frequency} CTC"), "datatype": "Currency", "currency": self.currency, }, { "value": self.gross_pay, - "label": _("Monthly Gross Pay"), + "label": _(f"{self.payroll_frequency} Gross Pay"), "datatype": "Currency", "currency": self.currency, }, { "value": self.net_pay, - "label": _("Monthly Net Pay"), + "label": _(f"{self.payroll_frequency} Net Pay"), "datatype": "Currency", "currency": self.currency, }, @@ -213,8 +220,8 @@ def get_columns(self) -> list[dict]: "width": 200, }, { - "label": _("Monthly"), - "fieldname": "monthly", + "label": _(self.payroll_frequency), + "fieldname": "per_cycle", "fieldtype": "Currency", "width": 200, "options": "currency", From 245bdc77f04ea98991c0b6d3a6674b4202de95d2 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Wed, 6 May 2026 11:48:20 +0530 Subject: [PATCH 04/14] feat: validate filters and ctc --- .../employee_ctc_break_up.py | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index 69aedcbf6e..09d249938c 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -3,7 +3,7 @@ import frappe from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, get_link_to_form from hrms.payroll.doctype.salary_structure.salary_structure import make_salary_slip @@ -12,6 +12,8 @@ class SalaryBreakupReport: def __init__(self, employee, salary_structure_assignment): self.employee = employee self.ctc = frappe.db.get_value("Employee", employee, "ctc") + self.validate_ctc() + self.salary_structure, self.currency = frappe.get_value( "Salary Structure Assignment", salary_structure_assignment, ["salary_structure", "currency"] ) @@ -35,6 +37,16 @@ def __init__(self, employee, salary_structure_assignment): self.total_net_earnings = [] self.total_gross_earnings = [] + def validate_ctc(self): + if not self.ctc: + frappe.throw( + _("Please set cost to company(CTC) for employee {0} in the {1}").format( + frappe.bold(self.employee), + get_link_to_form("Employee", self.employee + "#salary_information", "employee master."), + ), + title=_("CTC Missing for Employee"), + ) + def get_data(self): self.set_salary_component_details() self.calculate_yearly_amounts_and_percent_of_ctc() @@ -257,9 +269,22 @@ def execute(filters: dict | None = None): dictionary and should return columns and data. It is called by the framework every time the report is refreshed or a filter is updated. """ - - salary_structure_assignment = filters.get("salary_structure_assignment") employee = filters.get("employee") + salary_structure_assignment = filters.get("salary_structure_assignment") + + missing_filter = ( + "Employee" + if not employee + else "Salary Structure Assignment" + if not salary_structure_assignment + else None + ) + if missing_filter: + frappe.throw( + _("Please set {0} to get CTC report").format(frappe.bold(missing_filter)), + title=_("Missing value for filters"), + ) + salary_breakup_report = SalaryBreakupReport(employee, salary_structure_assignment) data = salary_breakup_report.get_data() From 8219b1be58cfacc664d7c95da09edc8211eb6a7a Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Thu, 14 May 2026 11:41:49 +0530 Subject: [PATCH 05/14] feat: CTC summary card --- .../employee_ctc_break_up.js | 6 +- .../employee_ctc_break_up.py | 48 ++++- .../employee_profile_card.html | 167 ++++++++++++++++++ 3 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js index 832c15ab6b..a7b80b67ab 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js @@ -46,7 +46,11 @@ frappe.query_reports["Employee CTC Break-up"] = { ], formatter: function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (data?.bold) value = `${value}`; + if (data?.bold && value) value = `${value}`; + if (column.fieldname == "type" && value) { + let indicator_color = value === "Fixed" ? "blue" : "orange"; + value = `${value}`; + } return value; }, }; diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index 09d249938c..98b6cb5c18 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -4,6 +4,8 @@ import frappe from frappe import _ from frappe.utils import flt, get_link_to_form +from frappe.utils.formatters import format_value +from frappe.utils.jinja import render_template from hrms.payroll.doctype.salary_structure.salary_structure import make_salary_slip @@ -14,8 +16,10 @@ def __init__(self, employee, salary_structure_assignment): self.ctc = frappe.db.get_value("Employee", employee, "ctc") self.validate_ctc() - self.salary_structure, self.currency = frappe.get_value( - "Salary Structure Assignment", salary_structure_assignment, ["salary_structure", "currency"] + self.salary_structure, self.currency, self.assignment_date, self.income_tax_slab = frappe.get_value( + "Salary Structure Assignment", + salary_structure_assignment, + ["salary_structure", "currency", "from_date", "income_tax_slab"], ) self.salary_slip = make_salary_slip( self.salary_structure, employee=self.employee, for_preview=1, as_print=False @@ -183,6 +187,9 @@ def indent_salary_components(self): for component in self.salary_components: component["indent"] = 1 + def format_currency(self, amount): + return format_value(amount, currency=self.currency) + def get_summary(self): per_cycle_ctc = flt(self.ctc / self.cycle_multiplier, 2) return [ @@ -218,38 +225,44 @@ def get_columns(self) -> list[dict]: "fieldname": "salary_component", "fieldtype": "Data", "width": 300, + "fixed": 1, }, { "label": _("Type"), "fieldname": "type", "fieldtype": "Data", - "width": 200, + "width": 150, + "fixed": 1, }, { "label": _("Formula/Amount"), "fieldname": "formula", "fieldtype": "Data", - "width": 200, + "width": 250, + "fixed": 1, }, { "label": _(self.payroll_frequency), "fieldname": "per_cycle", "fieldtype": "Currency", - "width": 200, + "width": 250, "options": "currency", + "fixed": 1, }, { "label": _("Annual"), "fieldname": "annual", "fieldtype": "Currency", - "width": 200, + "width": 250, "options": "currency", + "fixed": 1, }, { "label": _("Percent of CTC (%)"), "fieldname": "percent_of_ctc", "fieldtype": "Percent", "width": 200, + "fixed": 1, }, { "fieldname": "currency", @@ -261,6 +274,24 @@ def get_columns(self) -> list[dict]: }, ] + def get_message(self): + path = "hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html" + context = frappe.get_doc("Employee", self.employee).as_dict() + context.update( + { + "salary_structure": self.salary_structure, + "per_cycle": self.payroll_frequency, + "annual_ctc": self.format_currency(self.ctc), + "per_cycle_ctc": self.format_currency(flt(self.ctc / self.cycle_multiplier, 2)), + "per_cycle_gross_pay": self.format_currency(self.gross_pay), + "per_cycle_net_pay": self.format_currency(self.net_pay), + "assignment_date": frappe.utils.global_date_format(self.assignment_date, "long"), + "income_tax_slab": self.income_tax_slab, + } + ) + employee_profile_card = render_template(path, context=context, is_path=True) + return employee_profile_card + def execute(filters: dict | None = None): """Return columns and data for the report. @@ -288,7 +319,6 @@ def execute(filters: dict | None = None): salary_breakup_report = SalaryBreakupReport(employee, salary_structure_assignment) data = salary_breakup_report.get_data() - summary = salary_breakup_report.get_summary() columns = salary_breakup_report.get_columns() - - return columns, data, None, None, summary + message = salary_breakup_report.get_message() + return columns, data, message, None, None diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html b/hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html new file mode 100644 index 0000000000..f2a1a88cef --- /dev/null +++ b/hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html @@ -0,0 +1,167 @@ + + +
+ +
+
+ {% if image %} + + {% else %} +
{{frappe.utils.get_abbr(employee_name)}}
+ {% endif %} +
+ +
+ {{ employee_name }} + {% if designation %} + · + {{designation}} + {% endif %} +
+
Salary Structure: {{salary_structure}}
+ {% if income_tax_slab %}
Income Tax Slab: {{income_tax_slab}}
{% endif %} +
Assignment Date: {{assignment_date}}
+
+
+
+ +
+ +
+
+
+ {{ annual_ctc }} +
+
+ Annual CTC +
+
+ +
+
+ {{ per_cycle_ctc }} +
+ +
+ {{per_cycle}} CTC +
+
+
+
+
+
+ {{ per_cycle_gross_pay }} +
+ +
+ {{per_cycle}} Gross Pay +
+
+ +
+
+ {{ per_cycle_net_pay }} +
+ +
+ {{per_cycle}} Net Pay +
+
+ +
+ +
+
From 351c2a2d9921f25e1dcd6faeab5ff0e58a86ecc9 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Thu, 14 May 2026 11:54:11 +0530 Subject: [PATCH 06/14] chore: render template use is safe, asking the rules to be ignored for this --- .../report/employee_ctc_break_up/employee_ctc_break_up.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index 98b6cb5c18..173b16f8f7 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -289,6 +289,7 @@ def get_message(self): "income_tax_slab": self.income_tax_slab, } ) + # nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti employee_profile_card = render_template(path, context=context, is_path=True) return employee_profile_card From 0717efae48d7e7e9242a8e7ef1e8ebd0b5b08641 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Thu, 14 May 2026 12:41:16 +0530 Subject: [PATCH 07/14] feat: custom button on salary structure assigment to see CTC breakup report feat: added the report to the payroll workspace sidebar fix: changed reference report from salary structure to salary structure assignment --- .../salary_structure_assignment.js | 10 ++++++ .../report/employee_ctc_break_up/__init__.py | 0 .../employee_ctc_break_up.json | 5 ++- hrms/workspace_sidebar/payroll.json | 31 ++++++++++++++++++- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 hrms/payroll/report/employee_ctc_break_up/__init__.py diff --git a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index 513fb9a245..0cd10dc254 100644 --- a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -69,6 +69,16 @@ frappe.ui.form.on("Salary Structure Assignment", { ); frm.page.set_inner_btn_group_as_primary(__("Create")); + frm.add_custom_button( + __("See CTC Break-up"), + function () { + frappe.set_route("query-report", "Employee CTC Break-up", { + employee: frm.doc.employee, + salary_structure_assignment: frm.doc.name, + }); + }, + __("Actions"), + ); frm.add_custom_button( __("Preview Salary Slip"), function () { diff --git a/hrms/payroll/report/employee_ctc_break_up/__init__.py b/hrms/payroll/report/employee_ctc_break_up/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json index b1edf08492..ba2cfadd01 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.json @@ -9,14 +9,13 @@ "filters": [], "idx": 0, "is_standard": "Yes", - "letter_head": null, - "modified": "2026-04-15 23:59:12.988472", + "modified": "2026-05-14 12:08:50.414476", "modified_by": "Administrator", "module": "Payroll", "name": "Employee CTC Break-up", "owner": "Administrator", "prepared_report": 0, - "ref_doctype": "Salary Structure", + "ref_doctype": "Salary Structure Assignment", "report_name": "Employee CTC Break-up", "report_type": "Script Report", "roles": [ diff --git a/hrms/workspace_sidebar/payroll.json b/hrms/workspace_sidebar/payroll.json index 3501a13da9..00e691e371 100644 --- a/hrms/workspace_sidebar/payroll.json +++ b/hrms/workspace_sidebar/payroll.json @@ -15,6 +15,7 @@ "label": "Home", "link_to": "Payroll", "link_type": "Workspace", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -27,6 +28,7 @@ "label": "Dashboard", "link_to": "Payroll", "link_type": "Dashboard", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -39,6 +41,7 @@ "label": "Payroll Entry", "link_to": "Payroll Entry", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -51,6 +54,7 @@ "label": "Salary Structure Assignment", "link_to": "Salary Structure Assignment", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -63,6 +67,7 @@ "label": "Salary Slip", "link_to": "Salary Slip", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -75,6 +80,7 @@ "label": "Additional Salary", "link_to": "Additional Salary", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -87,6 +93,7 @@ "label": "Salary Withholding", "link_to": "Salary Withholding", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -98,9 +105,22 @@ "keep_closed": 1, "label": "Reports", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Section Break" }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Employee CTC Break-up", + "link_to": "Employee CTC Break-up", + "link_type": "Report", + "open_in_new_tab": 1, + "show_arrow": 0, + "type": "Link" + }, { "child": 1, "collapsible": 1, @@ -110,6 +130,7 @@ "label": "Salary Register", "link_to": "Salary Register", "link_type": "Report", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -122,6 +143,7 @@ "label": "Income Tax Deductions", "link_to": "Income Tax Deductions", "link_type": "Report", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -134,6 +156,7 @@ "label": "Professional Tax Deductions", "link_to": "Professional Tax Deductions", "link_type": "Report", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -146,6 +169,7 @@ "label": "General Ledger", "link_to": "General Ledger", "link_type": "Report", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -158,6 +182,7 @@ "label": "Accounts Payable", "link_to": "Accounts Payable", "link_type": "Report", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -169,6 +194,7 @@ "keep_closed": 1, "label": "Setup", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Section Break" }, @@ -181,6 +207,7 @@ "label": "Salary Component", "link_to": "Salary Component", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -193,6 +220,7 @@ "label": "Salary Structure", "link_to": "Salary Structure", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" }, @@ -205,11 +233,12 @@ "label": "Settings", "link_to": "Payroll Settings", "link_type": "DocType", + "open_in_new_tab": 1, "show_arrow": 0, "type": "Link" } ], - "modified": "2026-01-08 14:16:38.399025", + "modified": "2026-05-14 12:18:07.894971", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll", From 159412752bc21450a170e98b80553aa55cb36fd0 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Thu, 14 May 2026 13:44:09 +0530 Subject: [PATCH 08/14] feat: new field ctc in salary structure assignment feat: fetch ctc from salary structure assignment instead of employee master in the breakup report --- .../salary_structure/test_salary_structure.py | 2 +- .../salary_structure_assignment.js | 6 +++ .../salary_structure_assignment.json | 9 +++- .../salary_structure_assignment.py | 1 + .../employee_ctc_break_up.py | 44 ++++++------------- 5 files changed, 29 insertions(+), 33 deletions(-) diff --git a/hrms/payroll/doctype/salary_structure/test_salary_structure.py b/hrms/payroll/doctype/salary_structure/test_salary_structure.py index 32e78a38cd..81e163528d 100644 --- a/hrms/payroll/doctype/salary_structure/test_salary_structure.py +++ b/hrms/payroll/doctype/salary_structure/test_salary_structure.py @@ -231,7 +231,7 @@ def create_salary_structure_assignment( leave_encashment_amount_per_day=None, ): if not currency: - currency = "INR" or "INR" + currency = "INR" if not allow_duplicate and frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee)) diff --git a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index 0cd10dc254..4f383e6aa2 100644 --- a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -72,6 +72,12 @@ frappe.ui.form.on("Salary Structure Assignment", { frm.add_custom_button( __("See CTC Break-up"), function () { + if (!frm.doc.ctc) { + frm.scroll_to_field("ctc"); + frappe.throw( + __("Please set employee's total cost to company to see CTC breakup."), + ); + } frappe.set_route("query-report", "Employee CTC Break-up", { employee: frm.doc.employee, salary_structure_assignment: frm.doc.name, diff --git a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index e07e1c6fa3..ee35e6583c 100644 --- a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -22,6 +22,7 @@ "currency", "section_break_7", "base", + "ctc", "column_break_9", "variable", "amended_from", @@ -242,11 +243,17 @@ "fieldtype": "Currency", "label": "Leave Encashment Amount Per Day", "options": "currency" + }, + { + "allow_on_submit": 1, + "fieldname": "ctc", + "fieldtype": "Currency", + "label": "Total Cost To Company (CTC)" } ], "is_submittable": 1, "links": [], - "modified": "2026-02-26 14:37:43.779340", + "modified": "2026-05-14 13:33:55.869281", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", diff --git a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index 1abad6c98a..7f6ee935db 100644 --- a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -30,6 +30,7 @@ class SalaryStructureAssignment(Document): amended_from: DF.Link | None base: DF.Currency company: DF.Link + ctc: DF.Currency currency: DF.Link department: DF.Link | None designation: DF.Link | None diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index 173b16f8f7..07c0936606 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -13,14 +13,16 @@ class SalaryBreakupReport: def __init__(self, employee, salary_structure_assignment): self.employee = employee - self.ctc = frappe.db.get_value("Employee", employee, "ctc") - self.validate_ctc() + self.salary_structure_assignment = salary_structure_assignment - self.salary_structure, self.currency, self.assignment_date, self.income_tax_slab = frappe.get_value( - "Salary Structure Assignment", - salary_structure_assignment, - ["salary_structure", "currency", "from_date", "income_tax_slab"], + self.salary_structure, self.currency, self.assignment_date, self.income_tax_slab, self.ctc = ( + frappe.get_value( + "Salary Structure Assignment", + salary_structure_assignment, + ["salary_structure", "currency", "from_date", "income_tax_slab", "ctc"], + ) ) + self.validate_ctc() self.salary_slip = make_salary_slip( self.salary_structure, employee=self.employee, for_preview=1, as_print=False ) @@ -46,7 +48,11 @@ def validate_ctc(self): frappe.throw( _("Please set cost to company(CTC) for employee {0} in the {1}").format( frappe.bold(self.employee), - get_link_to_form("Employee", self.employee + "#salary_information", "employee master."), + get_link_to_form( + "Salary Structure Assignment", + self.salary_structure_assignment, + "Salary Structure Assignment", + ), ), title=_("CTC Missing for Employee"), ) @@ -190,30 +196,6 @@ def indent_salary_components(self): def format_currency(self, amount): return format_value(amount, currency=self.currency) - def get_summary(self): - per_cycle_ctc = flt(self.ctc / self.cycle_multiplier, 2) - return [ - {"value": self.ctc, "label": _("Annual CTC"), "datatype": "Currency", "currency": self.currency}, - { - "value": per_cycle_ctc, - "label": _(f"{self.payroll_frequency} CTC"), - "datatype": "Currency", - "currency": self.currency, - }, - { - "value": self.gross_pay, - "label": _(f"{self.payroll_frequency} Gross Pay"), - "datatype": "Currency", - "currency": self.currency, - }, - { - "value": self.net_pay, - "label": _(f"{self.payroll_frequency} Net Pay"), - "datatype": "Currency", - "currency": self.currency, - }, - ] - def get_columns(self) -> list[dict]: """Return columns for the report. From d8c8f59f38d7317efe93f63bb3e31dff465432bb Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Thu, 14 May 2026 14:22:13 +0530 Subject: [PATCH 09/14] fix: clear salary structure assignment on employee change --- .../report/employee_ctc_break_up/employee_ctc_break_up.js | 3 +++ .../report/employee_ctc_break_up/employee_ctc_break_up.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js index a7b80b67ab..d51d66ae27 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js @@ -25,6 +25,9 @@ frappe.query_reports["Employee CTC Break-up"] = { }, }; }, + on_change: function () { + frappe.query_report.set_filter_value("salary_structure_assignment", ""); + }, }, { fieldname: "salary_structure_assignment", diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index 07c0936606..3a9b292e2c 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -18,7 +18,7 @@ def __init__(self, employee, salary_structure_assignment): self.salary_structure, self.currency, self.assignment_date, self.income_tax_slab, self.ctc = ( frappe.get_value( "Salary Structure Assignment", - salary_structure_assignment, + {"name": salary_structure_assignment, "employee": employee}, ["salary_structure", "currency", "from_date", "income_tax_slab", "ctc"], ) ) From 1b914d612b59fe7d13149ea039d14f942c5f14fa Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 18 May 2026 12:38:48 +0530 Subject: [PATCH 10/14] test: added posting date flag for tests in salary breakup report, added tests --- .../employee_ctc_break_up.py | 16 +- .../test_employee_ctc_breakup.py | 254 ++++++++++++++++++ 2 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index 3a9b292e2c..103ac1e2fc 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -24,7 +24,11 @@ def __init__(self, employee, salary_structure_assignment): ) self.validate_ctc() self.salary_slip = make_salary_slip( - self.salary_structure, employee=self.employee, for_preview=1, as_print=False + self.salary_structure, + employee=self.employee, + for_preview=1, + as_print=False, + posting_date=frappe.flags.posting_date if frappe.flags.in_test else None, ) self.net_pay = self.salary_slip.net_pay self.gross_pay = self.salary_slip.gross_pay @@ -189,6 +193,9 @@ def set_net_and_gross_earning_rows(self): def calculate_percent_of_ctc(self, amount): return flt(amount * 100 / self.ctc, 2) + def get_per_cycle_ctc(self): + return flt(self.ctc / self.cycle_multiplier, 2) + def indent_salary_components(self): for component in self.salary_components: component["indent"] = 1 @@ -258,13 +265,14 @@ def get_columns(self) -> list[dict]: def get_message(self): path = "hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html" - context = frappe.get_doc("Employee", self.employee).as_dict() - context.update( + context = dict( { + "employee_name": frappe.get_value("Employee", self.employee, "employee_name"), + "designation": frappe.get_value("Employee", self.employee, "designation"), "salary_structure": self.salary_structure, "per_cycle": self.payroll_frequency, "annual_ctc": self.format_currency(self.ctc), - "per_cycle_ctc": self.format_currency(flt(self.ctc / self.cycle_multiplier, 2)), + "per_cycle_ctc": self.format_currency(self.get_per_cycle_ctc()), "per_cycle_gross_pay": self.format_currency(self.gross_pay), "per_cycle_net_pay": self.format_currency(self.net_pay), "assignment_date": frappe.utils.global_date_format(self.assignment_date, "long"), diff --git a/hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py b/hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py new file mode 100644 index 0000000000..7ee2a69fb6 --- /dev/null +++ b/hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py @@ -0,0 +1,254 @@ +import frappe +from frappe.utils import add_months, flt, get_year_start, getdate + +from erpnext.setup.doctype.employee.test_employee import make_employee + +from hrms.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( + create_payroll_period, +) +from hrms.payroll.doctype.salary_structure.test_salary_structure import ( + create_salary_structure_assignment, + make_salary_structure, +) +from hrms.payroll.report.employee_ctc_break_up.employee_ctc_break_up import SalaryBreakupReport +from hrms.tests.utils import HRMSTestSuite + + +class TestEmployeeCTCBreakup(HRMSTestSuite): + """ + `make_salary_structure` has the following setup + Earning Components: + - Basic (base set in salary structure assignment) + - HRA (fixed 3000) + - Special Allowance (base/2) + Deduction Components: + - Professional Tax (fixed 200) + Income Tax Details (only applicable if test_tax is set to True in salary structure): + - Slabs + - 0 to 250000: 0% + - 250000 to 500000: 5% if annual taxable income exceeds 50000 else 0% + - 500000 to 1000000: 20% + - Above 1000000: 30% + - Tax exemption of 50000 + - 4% cess + """ + + def test_ctc_summary_cards(self): + test_data = { + "Monthly": { + # if + "ctc": 1116000, + "base": 60000, + "currency": "INR", + # test + "annual_ctc": "₹ 1,116,000.00", + "per_cycle_ctc": 93000, + "gross_pay": 93000, + "net_pay": 92800, # gross pay - professional tax + }, + "Fortnightly": { + # if + "ctc": 648000, + "base": 16000, + "currency": "USD", + # test + "annual_ctc": "$ 648,000.00", + "per_cycle_ctc": 27000, + "gross_pay": 27000, + "net_pay": 26800, # gross pay - professional tax + }, + } + + for payroll_frequency, data in test_data.items(): + employee = make_employee(f"test_ctc{payroll_frequency}@example.com", company="_Test Company") + salary_structure = make_salary_structure( + "Test CTC Breakup", payroll_frequency=payroll_frequency, currency=data.get("currency") + ) + salary_structure_assignment = create_salary_structure_assignment( + employee, salary_structure.name, base=data.get("base"), currency=data.get("currency") + ) + salary_structure_assignment.ctc = data.get("ctc") + salary_structure_assignment.save() + + ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) + + self.assertEqual(ctc_breakup.format_currency(ctc_breakup.ctc), data["annual_ctc"]) + self.assertEqual(ctc_breakup.get_per_cycle_ctc(), data["per_cycle_ctc"]) + self.assertEqual(ctc_breakup.gross_pay, data["gross_pay"]) + self.assertEqual(ctc_breakup.net_pay, data["net_pay"]) + + def test_income_tax_net_pay_calculation_at_the_start_of_payroll_period(self): + employee = make_employee("test_ctc@example.com", company="_Test Company") + salary_structure = make_salary_structure( + "Test CTC Breakup with Tax", payroll_frequency="Monthly", currency="INR", test_tax=True + ) + salary_structure_assignment = create_salary_structure_assignment( + employee, salary_structure.name, base=60000, currency="INR", from_date=get_year_start(getdate()) + ) + salary_structure_assignment.ctc = 1116000 + salary_structure_assignment.save() + + """With the given setup, the annual tax will be, + taxable_income = 1116000 (total earnings) - 2400 (Professional Tax) - 50000 (Tax Exemption) = 1063600 + Tax amount = 12500 (5% of 250000) + 100000 (20% of 500000) + 19080(30% of 63600) = 131580 + Cess = 4% of tax = 5263.2 + Total tax amount = 136843.2 + Monthly tax = flt(136843.25 / 12) = 11404 + Monthly net pay = Gross pay - Professional Tax - Monthly tax = 93000 - 200 - 11404 = 81396 + """ + monthly_income_tax = 11404 + annual_tax = flt(monthly_income_tax * 12, 2) + frappe.flags.posting_date = get_year_start(getdate()) + ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) + self.assertEqual(ctc_breakup.net_pay, 81396) + income_tax_row = ctc_breakup.get_data()[-3] + self.assertEqual(income_tax_row.get("per_cycle"), monthly_income_tax) + self.assertEqual(income_tax_row.get("annual"), annual_tax) + + def test_income_tax_net_pay_calculation_at_the_end_of_payroll_period(self): + employee = make_employee("test_ctc@example.com", company="_Test Company") + salary_structure = make_salary_structure( + "Test CTC Breakup with Tax", payroll_frequency="Monthly", currency="INR", test_tax=True + ) + salary_structure_assignment = create_salary_structure_assignment( + employee, salary_structure.name, base=60000, currency="INR", from_date=get_year_start(getdate()) + ) + salary_structure_assignment.ctc = 1116000 + salary_structure_assignment.save() + + """With the given setup, the annual tax will be for last two months, + taxable_income = 186000 (total earnings) - 400 (Professional Tax) - 50000 (Tax Exemption) = 135600 + Tax amount = 0 (taxable earning < 250000) + Monthly net pay = Gross pay - Professional Tax - Monthly tax = 93000 - 200 - 11404 = 81396 + """ + monthly_income_tax = annual_tax = 0 + + frappe.flags.posting_date = add_months(get_year_start(getdate()), 10) + ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) + self.assertEqual(ctc_breakup.net_pay, 92800) + income_tax_row = ctc_breakup.get_data()[-3] + self.assertEqual(income_tax_row.get("per_cycle"), monthly_income_tax) + self.assertEqual(income_tax_row.get("annual"), annual_tax) + + def test_report(self): + employee = make_employee("test_ctc@example.com", company="_Test Company") + salary_structure = make_salary_structure( + "Test CTC Breakup with Tax", payroll_frequency="Monthly", currency="INR", test_tax=True + ) + salary_structure_assignment = create_salary_structure_assignment( + employee, salary_structure.name, base=60000, currency="INR", from_date=get_year_start(getdate()) + ) + salary_structure_assignment.ctc = 1116000 + salary_structure_assignment.save() + frappe.flags.posting_date = get_year_start(getdate()) + ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) + data = ctc_breakup.get_data() + self.assertEqual(len(data), 11) + + earning_components = ctc_breakup.earning_components + deduction_components = ctc_breakup.deduction_components + tax_components = ctc_breakup.tax_components + net_pay_row = ctc_breakup.total_net_earnings + gross_pay_row = ctc_breakup.total_gross_earnings + + earning_components_totals = earning_components[0] + self.assertEqual(earning_components_totals.get("salary_component"), "Earnings") + self.assertEqual(earning_components_totals.get("type"), "") + self.assertEqual(earning_components_totals.get("formula"), "") + self.assertEqual(earning_components_totals.get("per_cycle"), 93000) + self.assertEqual(earning_components_totals.get("annual"), 1116000) + self.assertEqual(earning_components_totals.get("percent_of_ctc"), 100) + + basic_earning_component = earning_components[1] + self.assertEqual(basic_earning_component.get("salary_component"), "Basic Salary") + self.assertEqual(basic_earning_component.get("type"), "Formula") + self.assertEqual(basic_earning_component.get("formula"), "base") + self.assertEqual(basic_earning_component.get("per_cycle"), 60000) + self.assertEqual(basic_earning_component.get("annual"), 720000) + self.assertEqual(basic_earning_component.get("percent_of_ctc"), 64.52) + + hra_earning_component = earning_components[2] + self.assertEqual(hra_earning_component.get("salary_component"), "HRA") + self.assertEqual(hra_earning_component.get("type"), "Fixed") + self.assertEqual(hra_earning_component.get("formula"), 3000) + self.assertEqual(hra_earning_component.get("per_cycle"), 3000) + self.assertEqual(hra_earning_component.get("annual"), 36000) + self.assertEqual(hra_earning_component.get("percent_of_ctc"), 3.23) + + special_allowance_earning_component = earning_components[3] + self.assertEqual(special_allowance_earning_component.get("salary_component"), "Special Allowance") + self.assertEqual(special_allowance_earning_component.get("type"), "Formula") + self.assertEqual(special_allowance_earning_component.get("formula"), "BS\n*.5") + self.assertEqual(special_allowance_earning_component.get("per_cycle"), 30000) + self.assertEqual(special_allowance_earning_component.get("annual"), 360000) + self.assertEqual(special_allowance_earning_component.get("percent_of_ctc"), 32.26) + + deduction_components_totals = deduction_components[0] + self.assertEqual(deduction_components_totals.get("salary_component"), "Deductions") + self.assertEqual(deduction_components_totals.get("type"), "") + self.assertEqual(deduction_components_totals.get("formula"), "") + self.assertEqual(deduction_components_totals.get("per_cycle"), 200) + self.assertEqual(deduction_components_totals.get("annual"), 2400) + self.assertEqual(deduction_components_totals.get("percent_of_ctc"), 0.22) + + professional_tax_deduction_component = deduction_components[1] + self.assertEqual(professional_tax_deduction_component.get("salary_component"), "Professional Tax") + self.assertEqual(professional_tax_deduction_component.get("type"), "Fixed") + self.assertEqual(professional_tax_deduction_component.get("formula"), 200) + self.assertEqual(professional_tax_deduction_component.get("per_cycle"), 200) + self.assertEqual(professional_tax_deduction_component.get("annual"), 2400) + self.assertEqual(professional_tax_deduction_component.get("percent_of_ctc"), 0.22) + + tax_components_totals = tax_components[0] + self.assertEqual(tax_components_totals.get("salary_component"), "Tax Deductions") + self.assertEqual(tax_components_totals.get("type"), "") + self.assertEqual(tax_components_totals.get("formula"), "") + self.assertEqual(tax_components_totals.get("per_cycle"), 11404) + self.assertEqual(tax_components_totals.get("annual"), 136848) + self.assertEqual(tax_components_totals.get("percent_of_ctc"), 12.26) + + self.assertEqual(net_pay_row[0].get("salary_component"), "Total Net Earnings") + self.assertEqual(net_pay_row[0].get("per_cycle"), 81396) + self.assertEqual(net_pay_row[0].get("annual"), 976752) + self.assertEqual(net_pay_row[0].get("percent_of_ctc"), 87.52) + + self.assertEqual(gross_pay_row[0].get("salary_component"), "Total Gross Earnings") + self.assertEqual(gross_pay_row[0].get("per_cycle"), 93000) + self.assertEqual(gross_pay_row[0].get("annual"), 1116000) + self.assertEqual(gross_pay_row[0].get("percent_of_ctc"), 100) + + self.assertEqual( + flt( + basic_earning_component.get("percent_of_ctc") + + hra_earning_component.get("percent_of_ctc") + + special_allowance_earning_component.get("percent_of_ctc"), + 0, + ), + earning_components_totals.get("percent_of_ctc"), + ) + self.assertEqual( + professional_tax_deduction_component.get("percent_of_ctc"), + deduction_components_totals.get("percent_of_ctc"), + ) + self.assertEqual( + net_pay_row[0].get("percent_of_ctc") + + tax_components_totals.get("percent_of_ctc") + + deduction_components_totals.get("percent_of_ctc"), + earning_components_totals.get("percent_of_ctc"), + ) + + def test_ctc_validation(self): + employee = make_employee("test_ctc@example.com", company="_Test Company") + salary_structure = make_salary_structure( + "Test CTC Breakup with Tax", payroll_frequency="Monthly", currency="INR", test_tax=True + ) + salary_structure_assignment = create_salary_structure_assignment( + employee, salary_structure.name, base=60000, currency="INR", from_date=get_year_start(getdate()) + ) + self.assertRaises( + frappe.ValidationError, SalaryBreakupReport, employee, salary_structure_assignment.name + ) + salary_structure_assignment.ctc = 1116000 + salary_structure_assignment.save() + ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) + self.assertEqual(ctc_breakup.ctc, 1116000) From cd411ecdc0e215d79dc6b983686e7313cfbf7955 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 18 May 2026 13:21:30 +0530 Subject: [PATCH 11/14] chore: wrapping strings in translation function --- .../employee_profile_card.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html b/hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html index f2a1a88cef..cd6d51a1cb 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html +++ b/hrms/payroll/report/employee_ctc_break_up/employee_profile_card.html @@ -103,9 +103,9 @@ {{designation}} {% endif %}
-
Salary Structure: {{salary_structure}}
- {% if income_tax_slab %}
Income Tax Slab: {{income_tax_slab}}
{% endif %} -
Assignment Date: {{assignment_date}}
+
{{_("Salary Structure: ") + salary_structure}}
+ {% if income_tax_slab %}
{{_("Income Tax Slab: ") + income_tax_slab}}
{% endif %} +
{{ _("Assignment Date: ") + assignment_date}}
@@ -120,7 +120,7 @@ {{ annual_ctc }}
- Annual CTC + {{_("Annual CTC")}}
@@ -132,7 +132,7 @@
- {{per_cycle}} CTC + {{_(per_cycle + " CTC")}}
@@ -145,7 +145,7 @@
- {{per_cycle}} Gross Pay + {{_(per_cycle +" Gross Pay")}}
@@ -157,7 +157,7 @@
- {{per_cycle}} Net Pay + {{_(per_cycle + " Net Pay")}}
From 79c7f9e7ba093f03bbafa0b28a72ad8660b4271f Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 18 May 2026 14:29:23 +0530 Subject: [PATCH 12/14] test(Salary Slip): remove duplicate salary component --- hrms/payroll/doctype/salary_slip/test_salary_slip.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/hrms/payroll/doctype/salary_slip/test_salary_slip.py b/hrms/payroll/doctype/salary_slip/test_salary_slip.py index f7b52c2d53..2342dfce84 100644 --- a/hrms/payroll/doctype/salary_slip/test_salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/test_salary_slip.py @@ -2230,16 +2230,6 @@ def make_earning_salary_component( if setup or test_tax: make_salary_component(data, test_tax, company_list) - data.append( - { - "salary_component": "Basic Salary", - "abbr": "BS", - "condition": "base < 10000", - "formula": "base*.2", - "type": "Earning", - "amount_based_on_formula": 1, - } - ) return data From c4a2d4c29d6c95639b387e76b842666aeeadbf40 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Thu, 14 May 2026 14:20:32 +0530 Subject: [PATCH 13/14] fix(payroll_entry): ignore permissions when canceling payment ledger and journal entries --- hrms/payroll/doctype/payroll_entry/payroll_entry.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/hrms/payroll/doctype/payroll_entry/payroll_entry.py b/hrms/payroll/doctype/payroll_entry/payroll_entry.py index 109245251c..10516f2fe2 100644 --- a/hrms/payroll/doctype/payroll_entry/payroll_entry.py +++ b/hrms/payroll/doctype/payroll_entry/payroll_entry.py @@ -218,9 +218,13 @@ def cancel_linked_journal_entries(self): ) # cancel linked payment ledger entry for pl in journal_entry_payment_ledgers: - frappe.get_doc("Payment Ledger Entry", pl).cancel() + payment_ledger_entry = frappe.get_doc("Payment Ledger Entry", pl) + payment_ledger_entry.flags.ignore_permissions = True + payment_ledger_entry.cancel() - frappe.get_doc("Journal Entry", je).cancel() + journal_entry = frappe.get_doc("Journal Entry", je) + journal_entry.flags.ignore_permissions = True + journal_entry.cancel() def cancel_linked_payment_ledger_entries(self): payment_ledgers = frappe.get_all( @@ -231,7 +235,9 @@ def cancel_linked_payment_ledger_entries(self): # cancel payment ledger entry for pl in payment_ledgers: - frappe.get_doc("Payment Ledger Entry", pl).cancel() + payment_ledger_entry = frappe.get_doc("Payment Ledger Entry", pl) + payment_ledger_entry.flags.ignore_permissions = True + payment_ledger_entry.cancel() def get_linked_salary_slips(self): return frappe.get_all("Salary Slip", {"payroll_entry": self.name}, ["name", "docstatus"]) From e0c34de94ecb8049a102cdf8844fdce27c3190d1 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Thu, 14 May 2026 16:42:14 +0530 Subject: [PATCH 14/14] test(payroll_entry): add test for payroll entry cancellation by HR Manager --- .../payroll_entry/test_payroll_entry.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/hrms/payroll/doctype/payroll_entry/test_payroll_entry.py b/hrms/payroll/doctype/payroll_entry/test_payroll_entry.py index cd78f4df9e..b61b5f8756 100644 --- a/hrms/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/hrms/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -444,6 +444,41 @@ def test_payroll_entry_cancellation(self): journal_entries = get_linked_journal_entries(payroll_entry.name, docstatus=2) self.assertEqual(len(journal_entries), 2) + def test_payroll_entry_cancellation_with_hr_manager(self): + company_doc = frappe.get_doc("Company", "_Test Company") + employee = make_employee("test_hr_manager_employee@payroll.com", company=company_doc.name) + + setup_salary_structure(employee, company_doc) + dates = get_start_end_dates("Monthly", nowdate()) + payroll_entry = make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + company=company_doc.name, + cost_center="Main - _TC", + payment_account="Cash - _TC", + ) + + hr_user = frappe.get_doc( + { + "doctype": "User", + "email": "test_hr_manager@payroll.com", + "first_name": "Test HR Manager", + "enabled": 1, + } + ).insert(ignore_if_duplicate=True) + hr_user.add_roles("HR Manager") + frappe.set_user(hr_user.name) + + payroll_entry.submit() + self.assertEqual(payroll_entry.status, "Submitted") + + payroll_entry.cancel() + self.assertEqual(payroll_entry.status, "Cancelled") + + frappe.set_user("Administrator") + def test_payroll_entry_status(self): company_doc = frappe.get_doc("Company", "_Test Company") employee = make_employee("test_employee@payroll.com", company=company_doc.name)