diff --git a/hrms/api/__init__.py b/hrms/api/__init__.py index 6ea7f1bf70..d2c1773153 100644 --- a/hrms/api/__init__.py +++ b/hrms/api/__init__.py @@ -701,6 +701,7 @@ def get_currency_symbols() -> dict: @frappe.whitelist() def get_company_cost_center_and_expense_account(company: str) -> dict: + frappe.has_permission("Company", "read", company, throw=True) return frappe.db.get_value( "Company", company, ["cost_center", "default_expense_claim_payable_account"], as_dict=True ) @@ -779,6 +780,11 @@ def upload_base64_file( @frappe.whitelist() def delete_attachment(filename: str): + attached_to_doctype, attached_to_name = frappe.db.get_value( + "File", filename, ["attached_to_doctype", "attached_to_name"] + ) + if attached_to_doctype and attached_to_name: + frappe.has_permission(attached_to_doctype, "write", attached_to_name, throw=True) frappe.delete_doc("File", filename) diff --git a/hrms/api/roster.py b/hrms/api/roster.py index 6a2c249d03..3b3f40e978 100644 --- a/hrms/api/roster.py +++ b/hrms/api/roster.py @@ -69,6 +69,7 @@ def get_events( @frappe.whitelist() def get_schedule_from_assignment(shift_schedule_assignment: str): + frappe.has_permission("Shift Schedule Assignment", "read", shift_schedule_assignment, throw=True) shift_schedule = frappe.db.get_value( "Shift Schedule Assignment", shift_schedule_assignment, "shift_schedule" ) diff --git a/hrms/controllers/employee_boarding_controller.py b/hrms/controllers/employee_boarding_controller.py index bb99c762dd..26b777eef5 100644 --- a/hrms/controllers/employee_boarding_controller.py +++ b/hrms/controllers/employee_boarding_controller.py @@ -157,6 +157,7 @@ def on_cancel(self): @frappe.whitelist() def get_onboarding_details(parent: str, parenttype: str): + frappe.has_permission(parenttype, "read", parent, throw=True) return frappe.get_all( "Employee Boarding Activity", fields=[ diff --git a/hrms/hr/doctype/appraisal/appraisal.py b/hrms/hr/doctype/appraisal/appraisal.py index 41f0c5fb69..d252191807 100644 --- a/hrms/hr/doctype/appraisal/appraisal.py +++ b/hrms/hr/doctype/appraisal/appraisal.py @@ -307,6 +307,7 @@ def set_goal_score(self, update=False): @frappe.whitelist() def get_feedback_history(employee: str, appraisal: str) -> dict: + frappe.has_permission("Appraisal", "read", appraisal, throw=True) data = frappe._dict() data.feedback_history = frappe.get_list( "Employee Performance Feedback", diff --git a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.py b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.py index 5b1fe17a39..7ea41a2011 100644 --- a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.py +++ b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.py @@ -62,6 +62,7 @@ def check_if_appraisals_exist(self): @frappe.whitelist() def set_employees(self): """Pull employees in appraisee list based on selected filters""" + self.check_permission("write") employees = self.get_employees_for_appraisal() appraisal_templates = self.get_appraisal_template_map() @@ -234,6 +235,7 @@ def validate_active_appraisal_cycle(appraisal_cycle: str) -> None: @frappe.whitelist() def get_appraisal_cycle_summary(cycle_name: str) -> dict: + frappe.has_permission("Appraisal Cycle", "read", cycle_name, throw=True) summary = frappe._dict() summary["appraisees"] = frappe.db.count( @@ -283,6 +285,8 @@ def get_employees_without_feedback(cycle_name: str | None = None) -> int: "Appraisal Cycle", {"status": "In Progress"}, order_by="start_date desc" ) + frappe.has_permission("Appraisal Cycle", "read", cycle_name, throw=True) + filtered_records = SubQuery( frappe.qb.from_(Feedback) .select(Feedback.employee) diff --git a/hrms/hr/doctype/attendance/attendance.py b/hrms/hr/doctype/attendance/attendance.py index 8b198e6b81..25d90bc694 100644 --- a/hrms/hr/doctype/attendance/attendance.py +++ b/hrms/hr/doctype/attendance/attendance.py @@ -432,6 +432,7 @@ def process_bulk_attendance_in_batches(data, chunk_size=20): def get_unmarked_days( employee: str, from_date: str | date, to_date: str | date, exclude_holidays: str | int = 0 ) -> list: + frappe.has_permission("Employee", "read", employee, throw=True) joining_date, relieving_date = frappe.get_cached_value( "Employee", employee, ["date_of_joining", "relieving_date"] ) diff --git a/hrms/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/hrms/hr/doctype/employee_attendance_tool/employee_attendance_tool.py index e930a2f3ea..239670fecb 100644 --- a/hrms/hr/doctype/employee_attendance_tool/employee_attendance_tool.py +++ b/hrms/hr/doctype/employee_attendance_tool/employee_attendance_tool.py @@ -203,6 +203,7 @@ def mark_employee_attendance( attendance.insert() attendance.submit() if mark_half_day: + frappe.has_permission("Attendance", "write", throw=True) if isinstance(half_day_employee_list, str): half_day_employee_list = json.loads(half_day_employee_list) Attendance = frappe.qb.DocType("Attendance") diff --git a/hrms/hr/doctype/employee_checkin/employee_checkin.py b/hrms/hr/doctype/employee_checkin/employee_checkin.py index 3bbb592558..0fd63c30e5 100644 --- a/hrms/hr/doctype/employee_checkin/employee_checkin.py +++ b/hrms/hr/doctype/employee_checkin/employee_checkin.py @@ -182,6 +182,12 @@ def add_log_based_on_employee_field( if not employee_field_value or not timestamp: frappe.throw(_("'employee_field_value' and 'timestamp' are required.")) + allowed_employee_fieldnames = {"name", "employee", "attendance_device_id"} + if employee_fieldname not in allowed_employee_fieldnames: + frappe.throw( + _("'employee_fieldname' must be one of {0}.").format(", ".join(allowed_employee_fieldnames)) + ) + employee = frappe.db.get_values( "Employee", {employee_fieldname: employee_field_value}, diff --git a/hrms/hr/doctype/employee_onboarding/employee_onboarding.py b/hrms/hr/doctype/employee_onboarding/employee_onboarding.py index 289a98ca3e..caf431f80d 100644 --- a/hrms/hr/doctype/employee_onboarding/employee_onboarding.py +++ b/hrms/hr/doctype/employee_onboarding/employee_onboarding.py @@ -92,6 +92,7 @@ def on_cancel(self): @frappe.whitelist() def mark_onboarding_as_completed(self): + self.check_permission("write") for activity in self.activities: frappe.db.set_value("Task", activity.task, "status", "Completed") frappe.db.set_value("Project", self.project, "status", "Completed") diff --git a/hrms/hr/doctype/expense_claim/expense_claim.js b/hrms/hr/doctype/expense_claim/expense_claim.js index 8be97dfc9b..613b4169b4 100644 --- a/hrms/hr/doctype/expense_claim/expense_claim.js +++ b/hrms/hr/doctype/expense_claim/expense_claim.js @@ -520,6 +520,16 @@ frappe.ui.form.on("Expense Claim Detail", { if (!d.expense_type) { return; } + + // Fetch description only if empty + if (!d.description) { + frappe.db.get_value("Expense Claim Type", d.expense_type, "description").then((r) => { + if (r.message && r.message.description) { + frappe.model.set_value(cdt, cdn, "description", r.message.description); + } + }); + } + return frappe.call({ method: "hrms.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center", args: { diff --git a/hrms/hr/doctype/expense_claim/expense_claim.py b/hrms/hr/doctype/expense_claim/expense_claim.py index a5e749ed1d..16472343a6 100644 --- a/hrms/hr/doctype/expense_claim/expense_claim.py +++ b/hrms/hr/doctype/expense_claim/expense_claim.py @@ -677,6 +677,7 @@ def get_advances(expense_claim: str | dict | Document, advance_id: str | None = if isinstance(expense_claim, str): expense_claim = frappe._dict(json.loads(expense_claim)) expense_claim_doc = frappe.get_doc(expense_claim) + frappe.has_permission("Employee", "read", expense_claim_doc.employee, throw=True) expense_claim_doc.advances = [] advance = frappe.qb.DocType("Employee Advance") @@ -714,6 +715,7 @@ def get_advances(expense_claim: str | dict | Document, advance_id: str | None = @frappe.whitelist() def get_expense_claim(employee_advance: str | dict) -> Document: + frappe.has_permission("Employee Advance", "read", employee_advance, throw=True) if isinstance(employee_advance, str): employee_advance = frappe.get_doc("Employee Advance", employee_advance) diff --git a/hrms/hr/doctype/expense_claim_detail/expense_claim_detail.json b/hrms/hr/doctype/expense_claim_detail/expense_claim_detail.json index 4faf15fe47..9078953f37 100644 --- a/hrms/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/hrms/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -60,8 +60,6 @@ "read_only": 1 }, { - "fetch_from": "expense_type.description", - "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Text Editor", "in_list_view": 1, @@ -155,7 +153,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-09-10 19:21:50.625260", + "modified": "2026-06-15 16:46:22.402968", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", diff --git a/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.py b/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.py index e7f3ca82c6..b5a297e2bb 100644 --- a/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.py +++ b/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.py @@ -310,6 +310,8 @@ def get_account_and_amount(ref_doctype: str, ref_document: str, company: str) -> if not ref_doctype or not ref_document: return None + frappe.has_permission(ref_doctype, "read", ref_document, throw=True) + if ref_doctype == "Salary Slip": salary_details = frappe.db.get_value( "Salary Slip", ref_document, ["payroll_entry", "net_pay"], as_dict=1 diff --git a/hrms/hr/doctype/interview/interview.py b/hrms/hr/doctype/interview/interview.py index 828f5d0fae..82565a2cff 100644 --- a/hrms/hr/doctype/interview/interview.py +++ b/hrms/hr/doctype/interview/interview.py @@ -156,6 +156,7 @@ def on_discard(self): @frappe.whitelist() def get_interviewers(interview_type: str) -> list[dict]: + frappe.has_permission("Interview Type", "read", interview_type, throw=True) return frappe.get_all("Interviewer", filters={"parent": interview_type}, fields=["user as interviewer"]) @@ -202,6 +203,7 @@ def get_feedback(interview: str) -> list[dict]: @frappe.whitelist() def get_skill_wise_average_rating(interview: str) -> list[dict]: + frappe.has_permission("Interview", "read", interview, throw=True) skill_assessment = frappe.qb.DocType("Skill Assessment") interview_feedback = frappe.qb.DocType("Interview Feedback") return ( @@ -340,6 +342,7 @@ def send_daily_feedback_reminder(): @frappe.whitelist() def get_expected_skill_set(interview_type: str): + frappe.has_permission("Interview Type", "read", interview_type, throw=True) return frappe.get_all( "Expected Skill Set", filters={"parent": interview_type}, fields=["skill"], order_by="idx" ) diff --git a/hrms/hr/doctype/interview_feedback/interview_feedback.py b/hrms/hr/doctype/interview_feedback/interview_feedback.py index 31220e18ee..7437350e52 100644 --- a/hrms/hr/doctype/interview_feedback/interview_feedback.py +++ b/hrms/hr/doctype/interview_feedback/interview_feedback.py @@ -102,4 +102,5 @@ def update_interview_average_rating(self): @frappe.whitelist() def get_applicable_interviewers(interview: str) -> list[str]: + frappe.has_permission("Interview", "read", interview, throw=True) return frappe.get_all("Interview Detail", filters={"parent": interview}, pluck="interviewer") diff --git a/hrms/hr/doctype/job_applicant/job_applicant.py b/hrms/hr/doctype/job_applicant/job_applicant.py index 01b1139ebd..33dc1299b1 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.py +++ b/hrms/hr/doctype/job_applicant/job_applicant.py @@ -114,6 +114,7 @@ def create_interview(job_applicant: str, interview_type: str) -> Document: @frappe.whitelist() def get_interview_details(job_applicant: str) -> dict: + frappe.has_permission("Job Applicant", "read", job_applicant, throw=True) interview_details = frappe.db.get_all( "Interview", filters={"job_applicant": job_applicant, "docstatus": ["!=", 2]}, diff --git a/hrms/hr/doctype/job_requisition/job_requisition.py b/hrms/hr/doctype/job_requisition/job_requisition.py index 4f9536da23..436f21f296 100644 --- a/hrms/hr/doctype/job_requisition/job_requisition.py +++ b/hrms/hr/doctype/job_requisition/job_requisition.py @@ -58,6 +58,7 @@ def check_duplicate_job_requisition(self): @frappe.whitelist() def associate_job_opening(self, job_opening: str) -> None: + frappe.has_permission("Job Opening", "write", job_opening, throw=True) frappe.db.set_value( "Job Opening", job_opening, {"job_requisition": self.name, "vacancies": self.no_of_positions} ) diff --git a/hrms/hr/doctype/leave_adjustment/leave_adjustment.py b/hrms/hr/doctype/leave_adjustment/leave_adjustment.py index e2a46dfddd..3585e1b40d 100644 --- a/hrms/hr/doctype/leave_adjustment/leave_adjustment.py +++ b/hrms/hr/doctype/leave_adjustment/leave_adjustment.py @@ -130,7 +130,7 @@ def get_leave_allocation_for_posting_date( """ Returns the leave allocation for the given employee, leave type and posting date. """ - return frappe.get_all( + return frappe.get_list( "Leave Allocation", { "employee": employee, @@ -151,7 +151,7 @@ def get_allocated_leave_types( """ Returns the leave types allocated to the given employee """ - return frappe.get_all( + return frappe.get_list( "Leave Allocation", { "employee": filters.get("employee"), diff --git a/hrms/hr/doctype/leave_application/leave_application.py b/hrms/hr/doctype/leave_application/leave_application.py index f8c3183383..c1ec44369a 100755 --- a/hrms/hr/doctype/leave_application/leave_application.py +++ b/hrms/hr/doctype/leave_application/leave_application.py @@ -968,6 +968,7 @@ def get_number_of_leave_days( ) -> float: """Returns number of leave days between 2 dates after considering half day and holidays (Based on the include_holiday setting in Leave Type)""" + validate_leave_access(employee) number_of_days = date_diff(to_date, from_date) + 1 if cint(half_day) == 1: @@ -1020,7 +1021,7 @@ def get_leave_details(employee: str, date: str | datetime.date, for_salary_slip: return { "leave_allocation": leave_allocation, - "leave_approver": get_leave_approver(employee), + "leave_approver": get_employee_leave_approver(employee), "lwps": lwp, } @@ -1338,6 +1339,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date): @frappe.whitelist() def get_holidays(employee: str, from_date: str | datetime.date, to_date: str | datetime.date) -> int: """get holidays between two dates for the given employee""" + validate_leave_access(employee) holidays = get_holiday_dates_between_range(employee, from_date, to_date) return len(holidays) @@ -1526,6 +1528,11 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): @frappe.whitelist() def get_leave_approver(employee: str) -> str: + validate_leave_access(employee) + return get_employee_leave_approver(employee) + + +def get_employee_leave_approver(employee: str) -> str: leave_approver, department = frappe.db.get_value("Employee", employee, ["leave_approver", "department"]) if not leave_approver and department: @@ -1549,13 +1556,13 @@ def get_leave_approver_and_mandatory(employee: str) -> dict: return { "is_mandatory": 1 if mandatory else 0, - "leave_approver": get_leave_approver(employee), + "leave_approver": get_employee_leave_approver(employee), } def validate_leave_access(employee): employee_user = frappe.db.get_value("Employee", employee, "user_id") - leave_approver = get_leave_approver(employee) + leave_approver = get_employee_leave_approver(employee) if frappe.session.user not in (employee_user, leave_approver) and ( not frappe.has_permission("Employee", "read", employee) diff --git a/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index a269b071d3..08a476fba1 100644 --- a/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -228,6 +228,8 @@ def expire_allocation(allocation: str | Document | frappe._dict, expiry_date: da allocation = json.loads(allocation) allocation = frappe.get_doc("Leave Allocation", allocation["name"]) + frappe.has_permission("Leave Allocation", "write", allocation.name, throw=True) + leaves = get_remaining_leaves(allocation) expiry_date = expiry_date if expiry_date else allocation.to_date diff --git a/hrms/hr/doctype/staffing_plan/staffing_plan.py b/hrms/hr/doctype/staffing_plan/staffing_plan.py index ceb337b96b..d7bc25edc9 100644 --- a/hrms/hr/doctype/staffing_plan/staffing_plan.py +++ b/hrms/hr/doctype/staffing_plan/staffing_plan.py @@ -242,6 +242,8 @@ def get_active_staffing_plan_details( from_date: str | datetime.date | None = None, to_date: str | datetime.date | None = None, ) -> list[dict] | None: + frappe.has_permission("Staffing Plan", "read", throw=True) + if from_date is None: from_date = getdate(nowdate()) if to_date is None: diff --git a/hrms/hr/doctype/training_result/training_result.py b/hrms/hr/doctype/training_result/training_result.py index b4685285ed..06a25ceb9a 100644 --- a/hrms/hr/doctype/training_result/training_result.py +++ b/hrms/hr/doctype/training_result/training_result.py @@ -47,4 +47,5 @@ def on_submit(self): @frappe.whitelist() def get_employees(training_event: str): + frappe.has_permission("Training Event", "read", training_event, throw=True) return frappe.get_doc("Training Event", training_event).employees diff --git a/hrms/hr/doctype/vehicle_log/test_vehicle_log.py b/hrms/hr/doctype/vehicle_log/test_vehicle_log.py index bba8e1a7ef..792efa3108 100644 --- a/hrms/hr/doctype/vehicle_log/test_vehicle_log.py +++ b/hrms/hr/doctype/vehicle_log/test_vehicle_log.py @@ -1,12 +1,19 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import json + import frappe +from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs from frappe.utils import cstr, flt, nowdate, random_string from erpnext.setup.doctype.employee.test_employee import make_employee -from hrms.hr.doctype.vehicle_log.vehicle_log import make_expense_claim +from hrms.hr.doctype.vehicle_log.vehicle_log import ( + get_draft_expense_claim_cancellation_actions, + get_draft_expense_claims, + make_expense_claim, +) from hrms.tests.utils import HRMSTestSuite @@ -47,7 +54,6 @@ def test_vehicle_log_fuel_expense(self): self.assertEqual(fuel_expense, 50 * 500) vehicle_log.cancel() - frappe.delete_doc("Expense Claim", expense_claim.name) frappe.delete_doc("Vehicle Log", vehicle_log.name) def test_vehicle_log_with_service_expenses(self): @@ -58,9 +64,201 @@ def test_vehicle_log_with_service_expenses(self): self.assertEqual(expenses, 27000) vehicle_log.cancel() - frappe.delete_doc("Expense Claim", expense_claim.name) frappe.delete_doc("Vehicle Log", vehicle_log.name) + def test_cancel_vehicle_log_unlinks_draft_expense_claim(self): + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) + currency, cost_center = frappe.db.get_value( + "Company", "_Test Company", ["default_currency", "cost_center"] + ) + expense_claim = frappe.get_doc( + { + "doctype": "Expense Claim", + "employee": self.employee_id, + "company": "_Test Company", + "currency": currency, + "exchange_rate": 1, + "approval_status": "Approved", + "payable_account": frappe.db.get_value("Company", "_Test Company", "default_payable_account"), + "vehicle_log": vehicle_log.name, + "expenses": [ + { + "expense_type": "Travel", + "default_account": "Travel Expenses - _TC", + "currency": currency, + "amount": 100, + "sanctioned_amount": 100, + "cost_center": cost_center, + } + ], + } + ).insert() + + self.assertIn(expense_claim.name, get_draft_expense_claims(vehicle_log.name)) + self.assertEqual( + get_draft_expense_claim_cancellation_actions(vehicle_log.name), + [{"name": expense_claim.name, "action": "unlink"}], + ) + + vehicle_log.reload() + vehicle_log.cancel() + expense_claim.reload() + + self.assertEqual(vehicle_log.docstatus, 2) + self.assertIsNone(expense_claim.vehicle_log) + self.assertNotIn(expense_claim.name, get_draft_expense_claims(vehicle_log.name)) + # non-Vehicle-Log expense rows remain intact + self.assertEqual(len(expense_claim.expenses), 1) + self.assertEqual(expense_claim.expenses[0].expense_type, "Travel") + + expense_claim.submit() + + expense_claim.cancel() + frappe.delete_doc("Vehicle Log", vehicle_log.name) + + def test_cancel_vehicle_log_deletes_claim_with_only_vehicle_log_expenses(self): + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) + currency, cost_center = frappe.db.get_value( + "Company", "_Test Company", ["default_currency", "cost_center"] + ) + + expense_claim = frappe.get_doc( + { + "doctype": "Expense Claim", + "employee": self.employee_id, + "company": "_Test Company", + "currency": currency, + "exchange_rate": 1, + "approval_status": "Approved", + "payable_account": frappe.db.get_value("Company", "_Test Company", "default_payable_account"), + "vehicle_log": vehicle_log.name, + "expenses": [ + { + "expense_date": nowdate(), + "expense_type": "Travel", + "default_account": "Travel Expenses - _TC", + "currency": currency, + "description": "Vehicle Expenses", + "amount": 25000, + "sanctioned_amount": 25000, + "cost_center": cost_center, + } + ], + } + ).insert() + + self.assertIn(expense_claim.name, get_draft_expense_claims(vehicle_log.name)) + self.assertEqual( + get_draft_expense_claim_cancellation_actions(vehicle_log.name), + [{"name": expense_claim.name, "action": "delete"}], + ) + + vehicle_log.reload() + vehicle_log.cancel() + + self.assertFalse(frappe.db.exists("Expense Claim", expense_claim.name)) + frappe.delete_doc("Vehicle Log", vehicle_log.name) + + def test_cancel_vehicle_log_removes_only_vehicle_log_rows_from_mixed_claim(self): + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) + currency, cost_center = frappe.db.get_value( + "Company", "_Test Company", ["default_currency", "cost_center"] + ) + + expense_claim = frappe.get_doc( + { + "doctype": "Expense Claim", + "employee": self.employee_id, + "company": "_Test Company", + "currency": currency, + "exchange_rate": 1, + "approval_status": "Approved", + "payable_account": frappe.db.get_value("Company", "_Test Company", "default_payable_account"), + "vehicle_log": vehicle_log.name, + "expenses": [ + { + "expense_date": nowdate(), + "expense_type": "Travel", + "default_account": "Travel Expenses - _TC", + "currency": currency, + "description": "Vehicle Expenses", + "amount": 25000, + "sanctioned_amount": 25000, + "cost_center": cost_center, + }, + { + "expense_date": nowdate(), + "expense_type": "Travel", + "default_account": "Travel Expenses - _TC", + "currency": currency, + "description": "Accident Repair", + "amount": 5000, + "sanctioned_amount": 5000, + "cost_center": cost_center, + }, + ], + } + ).insert() + + self.assertIn(expense_claim.name, get_draft_expense_claims(vehicle_log.name)) + self.assertEqual(len(expense_claim.expenses), 2) + self.assertEqual( + get_draft_expense_claim_cancellation_actions(vehicle_log.name), + [{"name": expense_claim.name, "action": "unlink"}], + ) + + vehicle_log.reload() + vehicle_log.cancel() + + expense_claim.reload() + self.assertEqual(len(expense_claim.expenses), 1) + self.assertEqual(expense_claim.expenses[0].description, "Accident Repair") + self.assertIsNone(expense_claim.vehicle_log) + + expense_claim.submit() + expense_claim.cancel() + frappe.delete_doc("Vehicle Log", vehicle_log.name) + + def test_cancel_vehicle_log_with_submitted_expense_claim_uses_linked_doc_cancellation(self): + vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) + currency, cost_center = frappe.db.get_value( + "Company", "_Test Company", ["default_currency", "cost_center"] + ) + expense_claim = frappe.get_doc( + { + "doctype": "Expense Claim", + "employee": self.employee_id, + "company": "_Test Company", + "currency": currency, + "exchange_rate": 1, + "approval_status": "Approved", + "payable_account": frappe.db.get_value("Company", "_Test Company", "default_payable_account"), + "vehicle_log": vehicle_log.name, + "expenses": [ + { + "expense_type": "Travel", + "default_account": "Travel Expenses - _TC", + "currency": currency, + "amount": 100, + "sanctioned_amount": 100, + "cost_center": cost_center, + } + ], + } + ).insert() + expense_claim.submit() + + linked_docs = get_submitted_linked_docs("Vehicle Log", vehicle_log.name)["docs"] + self.assertIn({"doctype": "Expense Claim", "name": expense_claim.name, "docstatus": 1}, linked_docs) + + cancel_all_linked_docs(json.dumps(linked_docs)) + expense_claim.reload() + self.assertEqual(expense_claim.docstatus, 2) + + vehicle_log.reload() + vehicle_log.cancel() + self.assertEqual(vehicle_log.docstatus, 2) + def get_vehicle(employee_id): license_plate = random_string(10).upper() diff --git a/hrms/hr/doctype/vehicle_log/vehicle_log.js b/hrms/hr/doctype/vehicle_log/vehicle_log.js index 1a544594fd..3c64ba4f7c 100644 --- a/hrms/hr/doctype/vehicle_log/vehicle_log.js +++ b/hrms/hr/doctype/vehicle_log/vehicle_log.js @@ -24,6 +24,35 @@ frappe.ui.form.on("Vehicle Log", { } }, + before_cancel: function (frm) { + return new Promise((resolve, reject) => { + frappe.call({ + method: "hrms.hr.doctype.vehicle_log.vehicle_log.get_draft_expense_claim_cancellation_actions", + args: { + vehicle_log: frm.doc.name, + }, + callback: function (r) { + const expense_claims = r.message || []; + if (!expense_claims.length) { + resolve(); + return; + } + + frappe.confirm( + get_expense_claim_cancellation_message(expense_claims, frm.doc.name), + () => resolve(), + () => reject(), + __("Yes"), + __("No"), + ); + }, + error: function (err) { + reject(err); + }, + }); + }); + }, + expense_claim: function (frm) { frappe.call({ method: "hrms.hr.doctype.vehicle_log.vehicle_log.make_expense_claim", @@ -37,3 +66,24 @@ frappe.ui.form.on("Vehicle Log", { }); }, }); + +function get_expense_claim_cancellation_message(expense_claims, vehicle_log) { + const expense_claim = expense_claims[0]; + const expense_claim_link = frappe.utils.get_form_link( + "Expense Claim", + expense_claim.name, + true, + ); + + if (expense_claim.action === "delete") { + return __( + "Cancelling Vehicle Log {0} will delete draft Expense Claim {1} because it only contains Vehicle Expenses.

Do you want to continue?", + [vehicle_log.bold(), expense_claim_link], + ); + } + + return __( + "Cancelling Vehicle Log {0} will update draft Expense Claim {1}.

Vehicle Expenses rows will be removed. Other expenses will remain.

Do you want to continue?", + [vehicle_log.bold(), expense_claim_link], + ); +} diff --git a/hrms/hr/doctype/vehicle_log/vehicle_log.py b/hrms/hr/doctype/vehicle_log/vehicle_log.py index 7b3ae5c6f7..21b24ce2ba 100644 --- a/hrms/hr/doctype/vehicle_log/vehicle_log.py +++ b/hrms/hr/doctype/vehicle_log/vehicle_log.py @@ -46,6 +46,20 @@ def validate(self): def on_submit(self): frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", self.odometer) + def before_cancel(self): + for expense_claim_name in _get_draft_expense_claims(self.name): + expense_claim = frappe.get_doc("Expense Claim", expense_claim_name) + + if _has_non_vehicle_log_expenses(expense_claim): + for row in list(expense_claim.expenses): + if row.description == _("Vehicle Expenses"): + expense_claim.remove(row) + + expense_claim.vehicle_log = None + expense_claim.save() + else: + expense_claim.delete() + def on_cancel(self): distance_travelled = self.odometer - self.last_odometer if distance_travelled > 0: @@ -57,6 +71,8 @@ def on_cancel(self): @frappe.whitelist() def make_expense_claim(docname: str) -> dict: + frappe.has_permission("Vehicle Log", "read", docname, throw=True) + expense_claim = frappe.db.exists("Expense Claim", {"vehicle_log": docname}) if expense_claim: frappe.throw(_("Expense Claim {0} already exists for the Vehicle Log").format(expense_claim)) @@ -77,3 +93,44 @@ def make_expense_claim(docname: str) -> dict: {"expense_date": vehicle_log.date, "description": _("Vehicle Expenses"), "amount": claim_amount}, ) return exp_claim.as_dict() + + +@frappe.whitelist() +def get_draft_expense_claims(vehicle_log: str) -> list[str]: + frappe.has_permission("Vehicle Log", doc=vehicle_log, throw=True) + + return _get_draft_expense_claims(vehicle_log) + + +@frappe.whitelist() +def get_draft_expense_claim_cancellation_actions(vehicle_log: str) -> list[dict[str, str]]: + frappe.has_permission("Vehicle Log", doc=vehicle_log, throw=True) + + return [ + { + "name": expense_claim.name, + "action": "unlink" if _has_non_vehicle_log_expenses(expense_claim) else "delete", + } + for expense_claim in _get_draft_expense_claim_docs(vehicle_log) + ] + + +def _get_draft_expense_claims(vehicle_log: str) -> list[str]: + return frappe.get_all( + "Expense Claim", + filters={"vehicle_log": vehicle_log, "docstatus": 0}, + pluck="name", + order_by="creation asc", + ) + + +def _get_draft_expense_claim_docs(vehicle_log: str) -> list[Document]: + return [ + frappe.get_doc("Expense Claim", expense_claim_name) + for expense_claim_name in _get_draft_expense_claims(vehicle_log) + ] + + +def _has_non_vehicle_log_expenses(expense_claim: Document) -> bool: + vehicle_log_description = _("Vehicle Expenses") + return any(row.description != vehicle_log_description for row in expense_claim.expenses) diff --git a/hrms/hr/page/organizational_chart/organizational_chart.py b/hrms/hr/page/organizational_chart/organizational_chart.py index 28cabcd29a..9eb7364476 100644 --- a/hrms/hr/page/organizational_chart/organizational_chart.py +++ b/hrms/hr/page/organizational_chart/organizational_chart.py @@ -16,7 +16,7 @@ def get_children(parent: str | None = None, company: str | None = None, exclude_ if exclude_node: filters.append(["name", "!=", exclude_node]) - employees = frappe.get_all( + employees = frappe.get_list( "Employee", fields=[ "employee_name as name", diff --git a/hrms/hr/page/team_updates/team_updates.py b/hrms/hr/page/team_updates/team_updates.py index 20d7153101..ae29b0f1dc 100644 --- a/hrms/hr/page/team_updates/team_updates.py +++ b/hrms/hr/page/team_updates/team_updates.py @@ -5,7 +5,7 @@ @frappe.whitelist() def get_data(start: int = 0): - # frappe.only_for('Employee', 'System Manager') + frappe.only_for("Employee", "System Manager") data = frappe.get_all( "Communication", fields=("content", "text_content", "sender", "creation"), diff --git a/hrms/overrides/employee_payment_entry.py b/hrms/overrides/employee_payment_entry.py index 87d21d3ba0..d22cb2069a 100644 --- a/hrms/overrides/employee_payment_entry.py +++ b/hrms/overrides/employee_payment_entry.py @@ -81,6 +81,7 @@ def get_payment_entry_for_employee( bank_amount: float | None = None, ): """Function to make Payment Entry for Employee Advance, Gratuity, Expense Claim, Leave Encashment""" + frappe.has_permission(dt, "read", dn, throw=True) doc = frappe.get_doc(dt, dn) party_account = get_party_account(doc) @@ -235,6 +236,7 @@ def get_payment_reference_details( party_type: str | None = None, party: str | None = None, ): + frappe.has_permission(reference_doctype, "read", reference_name, throw=True) if reference_doctype in ("Expense Claim", "Employee Advance", "Gratuity", "Leave Encashment"): return get_reference_details_for_employee(reference_doctype, reference_name, party_account_currency) else: @@ -251,6 +253,7 @@ def get_reference_details_for_employee( Returns payment reference details for employee related doctypes: Employee Advance, Expense Claim, Gratuity, Leave Encashment """ + frappe.has_permission(reference_doctype, "read", reference_name, throw=True) total_amount = outstanding_amount = exchange_rate = None ref_doc = frappe.get_doc(reference_doctype, reference_name) @@ -323,6 +326,7 @@ def set_exchange_rate_in_advance(doc: Document, method: None = None): if doc.references: for reference_doc in doc.references: if reference_doc.reference_doctype == "Employee Advance" and doc.target_exchange_rate: + frappe.has_permission("Employee Advance", "write", reference_doc.reference_name, throw=True) frappe.db.set_value( "Employee Advance", reference_doc.reference_name, diff --git a/hrms/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/hrms/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py index b6f2fe01c3..87caa854d2 100644 --- a/hrms/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py +++ b/hrms/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py @@ -192,6 +192,7 @@ def get_benefit_components( doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict ) -> list: """Fetch benefit components to choose from based on employee and date filters.""" + frappe.has_permission("Employee", "read", filters.get("employee"), throw=True) employee = filters.get("employee") date = filters.get("date") company = filters.get("company") diff --git a/hrms/payroll/doctype/payroll_entry/payroll_entry.py b/hrms/payroll/doctype/payroll_entry/payroll_entry.py index 10516f2fe2..73d3cb269d 100644 --- a/hrms/payroll/doctype/payroll_entry/payroll_entry.py +++ b/hrms/payroll/doctype/payroll_entry/payroll_entry.py @@ -1265,6 +1265,8 @@ def get_holidays_count(self, holiday_list: str, start_date: str, end_date: str) @frappe.whitelist() def create_overtime_slips(self) -> None: + self.check_permission("write") + from hrms.hr.doctype.overtime_slip.overtime_slip import ( create_overtime_slips_for_employees, filter_employees_for_overtime_slip_creation, @@ -1302,6 +1304,8 @@ def create_overtime_slips(self) -> None: @frappe.whitelist() def submit_overtime_slips(self) -> None: + self.check_permission("write") + from hrms.hr.doctype.overtime_slip.overtime_slip import ( submit_overtime_slips_for_employees, ) diff --git a/hrms/payroll/doctype/salary_component/salary_component.py b/hrms/payroll/doctype/salary_component/salary_component.py index 860d74f063..981fdc3402 100644 --- a/hrms/payroll/doctype/salary_component/salary_component.py +++ b/hrms/payroll/doctype/salary_component/salary_component.py @@ -165,6 +165,7 @@ def update_salary_structures( structures = self.get_structures_to_be_updated() for structure in structures: + frappe.has_permission("Salary Structure", "write", structure, throw=True) salary_structure = frappe.get_doc("Salary Structure", structure) # this is only used for versioning and we do not want # to make separate db calls by using load_doc_before_save diff --git a/hrms/payroll/doctype/salary_slip/salary_slip.py b/hrms/payroll/doctype/salary_slip/salary_slip.py index 73d6acf4ac..c3ea9a9b61 100644 --- a/hrms/payroll/doctype/salary_slip/salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/salary_slip.py @@ -2608,6 +2608,7 @@ def get_lwp_or_ppl_for_date_range(employee, start_date, end_date): @frappe.whitelist() def make_salary_slip_from_timesheet(source_name: str, target_doc: str | Document | None = None) -> Document: + frappe.has_permission("Timesheet", "read", source_name, throw=True) target = frappe.new_doc("Salary Slip") set_missing_values(source_name, target) target.run_method("get_emp_and_working_day_details") @@ -2670,6 +2671,9 @@ def enqueue_email_salary_slips(names: list | str) -> None: if isinstance(names, str): names = json.loads(names) + for name in names: + frappe.has_permission("Salary Slip", "read", name, throw=True) + frappe.enqueue("hrms.payroll.doctype.salary_slip.salary_slip.email_salary_slips", names=names) frappe.msgprint( _("Salary slip emails have been enqueued for sending. Check {0} for status.").format( diff --git a/hrms/payroll/doctype/salary_structure/salary_structure.py b/hrms/payroll/doctype/salary_structure/salary_structure.py index 39ef26d4c2..d8715f9d92 100644 --- a/hrms/payroll/doctype/salary_structure/salary_structure.py +++ b/hrms/payroll/doctype/salary_structure/salary_structure.py @@ -375,6 +375,9 @@ def make_salary_slip( for_preview: int = 0, lwp_days_corrected: float | None = None, ) -> str | Document: + if employee: + frappe.has_permission("Employee", "read", employee, throw=True) + return _make_salary_slip( source_name, target_doc=target_doc, 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 68fdaafc11..712d3799ee 100644 --- a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -452,6 +452,7 @@ def get_assigned_salary_structure(employee, on_date): @frappe.whitelist() def get_employee_currency(employee: str) -> str: + frappe.has_permission("Employee", "read", employee, throw=True) employee_currency = frappe.db.get_value("Salary Structure Assignment", {"employee": employee}, "currency") if not employee_currency: frappe.throw( diff --git a/hrms/payroll/doctype/salary_withholding/salary_withholding.py b/hrms/payroll/doctype/salary_withholding/salary_withholding.py index 133ac2b57b..6762099635 100644 --- a/hrms/payroll/doctype/salary_withholding/salary_withholding.py +++ b/hrms/payroll/doctype/salary_withholding/salary_withholding.py @@ -131,6 +131,7 @@ def on_discard(self): @frappe.whitelist() def get_payroll_frequency(employee: str, posting_date: str | date) -> str | None: + frappe.has_permission("Employee", "read", employee, throw=True) salary_structure = frappe.db.get_value( "Salary Structure Assignment", {