diff --git a/frontend/src/components/CheckInPanel.vue b/frontend/src/components/CheckInPanel.vue index 2282ae40a7..19cdcb83fa 100644 --- a/frontend/src/components/CheckInPanel.vue +++ b/frontend/src/components/CheckInPanel.vue @@ -69,7 +69,7 @@ - diff --git a/hrms/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/hrms/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index 7302e2ddd1..791e5295f0 100644 --- a/hrms/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/hrms/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -1,6 +1,7 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import datetime import frappe from frappe import _ @@ -78,7 +79,7 @@ def on_submit(self): comp_leave_valid_from = add_days(self.work_end_date, 1) leave_period = get_leave_period(comp_leave_valid_from, comp_leave_valid_from, company) if leave_period: - leave_allocation = self.get_existing_allocation_for_period(leave_period) + leave_allocation = self.get_existing_allocation(comp_leave_valid_from) if leave_allocation: leave_allocation.new_leaves_allocated += date_difference leave_allocation.validate() @@ -122,30 +123,21 @@ def on_cancel(self): leave_allocation, date_difference * -1, add_days(self.work_end_date, 1) ) - def get_existing_allocation_for_period(self, leave_period): - leave_allocation = frappe.db.sql( - """ - select name - from `tabLeave Allocation` - where employee=%(employee)s and leave_type=%(leave_type)s - and docstatus=1 - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, - { - "from_date": leave_period[0].from_date, - "to_date": leave_period[0].to_date, + def get_existing_allocation(self, comp_leave_valid_from: datetime.date) -> dict | None: + leave_allocation = frappe.db.get_all( + "Leave Allocation", + filters={ "employee": self.employee, "leave_type": self.leave_type, + "from_date": ("<=", comp_leave_valid_from), + "to_date": (">=", comp_leave_valid_from), + "docstatus": 1, }, - as_dict=1, + limit=1, ) if leave_allocation: return frappe.get_doc("Leave Allocation", leave_allocation[0].name) - else: - return False def create_leave_allocation(self, leave_period, date_difference): is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") diff --git a/hrms/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/hrms/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py index c05393a000..40b568ed34 100644 --- a/hrms/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py +++ b/hrms/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py @@ -3,15 +3,14 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, add_months, today +from frappe.utils import add_days, add_months, getdate, today from hrms.hr.doctype.attendance_request.test_attendance_request import get_employee +from hrms.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from hrms.hr.doctype.leave_application.leave_application import get_leave_balance_on from hrms.hr.doctype.leave_period.test_leave_period import create_leave_period from hrms.tests.test_utils import add_date_to_holiday_list -test_dependencies = ["Employee"] - class TestCompensatoryLeaveRequest(FrappeTestCase): def setUp(self): @@ -42,7 +41,7 @@ def test_leave_balance_on_submit(self): before + 1, ) - def test_leave_allocation_update_on_submit(self): + def test_allocation_update_on_submit(self): employee = get_employee() mark_attendance(employee, date=add_days(today(), -1)) compensatory_leave_request = get_compensatory_leave_request( @@ -70,6 +69,54 @@ def test_leave_allocation_update_on_submit(self): ) self.assertEqual(leaves_allocated, 2) + def test_allocation_update_on_submit_on_multiple_allocations(self): + """Tests whether the correct allocation is updated when there are multiple allocations in the same leave period""" + employee = get_employee() + today = getdate() + + first_alloc_start = add_months(today, -3) + first_alloc_end = add_days(today, -1) + second_alloc_start = today + second_alloc_end = add_months(today, 1) + + add_date_to_holiday_list(first_alloc_start, employee.holiday_list) + allocation_1 = create_leave_allocation( + leave_type="Compensatory Off", + employee=employee.name, + from_date=first_alloc_start, + to_date=first_alloc_end, + ) + allocation_1.new_leaves_allocated = 0 + allocation_1.submit() + + add_date_to_holiday_list(second_alloc_start, employee.holiday_list) + allocation_2 = create_leave_allocation( + leave_type="Compensatory Off", + employee=employee.name, + from_date=second_alloc_start, + to_date=second_alloc_end, + ) + allocation_2.new_leaves_allocated = 0 + allocation_2.submit() + + # adds leave balance in first allocation + mark_attendance(employee, date=first_alloc_start) + compensatory_leave_request = get_compensatory_leave_request( + employee.name, leave_date=first_alloc_start + ) + compensatory_leave_request.submit() + allocation_1.reload() + self.assertEqual(allocation_1.total_leaves_allocated, 1) + + # adds leave balance in second allocation + mark_attendance(employee, date=second_alloc_start) + compensatory_leave_request = get_compensatory_leave_request( + employee.name, leave_date=second_alloc_start + ) + compensatory_leave_request.submit() + allocation_2.reload() + self.assertEqual(allocation_2.total_leaves_allocated, 1) + def test_creation_of_leave_ledger_entry_on_submit(self): """check creation of leave ledger entry on submission of leave request""" employee = get_employee() diff --git a/hrms/hr/doctype/expense_claim/expense_claim.js b/hrms/hr/doctype/expense_claim/expense_claim.js index 0f826807bd..32c02611f2 100644 --- a/hrms/hr/doctype/expense_claim/expense_claim.js +++ b/hrms/hr/doctype/expense_claim/expense_claim.js @@ -64,6 +64,14 @@ frappe.ui.form.on("Expense Claim", { query: "erpnext.controllers.queries.employee_query", }; }); + + frm.set_query("department", function () { + return { + filters: { + company: frm.doc.company, + }, + }; + }); }, onload: function (frm) { diff --git a/hrms/hr/doctype/expense_claim/expense_claim.py b/hrms/hr/doctype/expense_claim/expense_claim.py index 04a02ba5d1..0717eed569 100644 --- a/hrms/hr/doctype/expense_claim/expense_claim.py +++ b/hrms/hr/doctype/expense_claim/expense_claim.py @@ -29,6 +29,10 @@ class ExpenseApproverIdentityError(frappe.ValidationError): pass +class MismatchError(frappe.ValidationError): + pass + + class ExpenseClaim(AccountsController, PWANotificationsMixin): def onload(self): self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value( @@ -47,6 +51,7 @@ def validate(self): self.set_expense_account(validate=True) self.calculate_taxes() self.set_status() + self.validate_company_and_department() if self.task and not self.project: self.project = frappe.db.get_value("Task", self.task, "project") @@ -83,6 +88,15 @@ def set_status(self, update=False): else: self.status = status + def validate_company_and_department(self): + if self.department: + company = frappe.db.get_value("Department", self.department, "company") + if company and self.company != company: + frappe.throw( + _("Department {0} does not belong to company: {1}").format(self.department, self.company), + exc=MismatchError, + ) + def on_update(self): share_doc_with_approver(self, self.expense_approver) self.publish_update() diff --git a/hrms/hr/doctype/expense_claim/test_expense_claim.py b/hrms/hr/doctype/expense_claim/test_expense_claim.py index 2d74922eae..8c12ad6afa 100644 --- a/hrms/hr/doctype/expense_claim/test_expense_claim.py +++ b/hrms/hr/doctype/expense_claim/test_expense_claim.py @@ -10,6 +10,7 @@ from erpnext.setup.doctype.employee.test_employee import make_employee from hrms.hr.doctype.expense_claim.expense_claim import ( + MismatchError, get_outstanding_amount_for_claim, make_bank_entry, make_expense_claim_for_delivery_trip, @@ -568,6 +569,13 @@ def test_repost(self): ) self.assertEqual(ledger_balance, expected_data) + def test_company_department_validation(self): + # validate company and department + expense_claim = frappe.new_doc("Expense Claim") + expense_claim.company = "_Test Company 3" + expense_claim.department = "Accounts - _TC2" + self.assertRaises(MismatchError, expense_claim.save) + def get_payable_account(company): return frappe.get_cached_value("Company", company, "default_payable_account") diff --git a/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.js b/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.js index 74e21f88bc..7e8e805dc4 100644 --- a/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.js +++ b/hrms/hr/doctype/full_and_final_statement/full_and_final_statement.js @@ -35,6 +35,10 @@ frappe.ui.form.on("Full and Final Statement", { filters["is_group"] = 0; } + if (frappe.model.is_submittable(fnf_doc.reference_document_type)) { + filters["docstatus"] = ["!=", 2]; + } + if (frappe.meta.has_field(fnf_doc.reference_document_type, "company")) { filters["company"] = frm.doc.company; } 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 2a465d1ed6..abf0889684 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 @@ -205,7 +205,7 @@ def create_journal_entry(self): "debit_in_account_currency": flt(data.amount, precision), "user_remark": data.remark, } - if data.reference_document_type == "Expense Claim": + if data.reference_document_type in ["Expense Claim", "Gratuity"]: account_dict["party_type"] = "Employee" account_dict["party"] = self.employee @@ -248,6 +248,15 @@ def create_journal_entry(self): ) return jv + def set_gratuity_status(self): + for payable in self.payables: + if payable.component != "Gratuity": + continue + gratuity = frappe.get_doc("Gratuity", payable.reference_document) + amount = payable.amount if self.docstatus == 1 and self.status == "Paid" else 0 + gratuity.db_set("paid_amount", amount) + gratuity.set_status(update=True) + @frappe.whitelist() def get_account_and_amount(ref_doctype, ref_document): @@ -310,3 +319,4 @@ def update_full_and_final_statement_status(doc, method=None): fnf = frappe.get_doc("Full and Final Statement", entry.reference_name) fnf.db_set("status", status) fnf.notify_update() + fnf.set_gratuity_status() diff --git a/hrms/hr/doctype/full_and_final_statement/test_full_and_final_statement.py b/hrms/hr/doctype/full_and_final_statement/test_full_and_final_statement.py index d1d0deeec3..a10b297253 100644 --- a/hrms/hr/doctype/full_and_final_statement/test_full_and_final_statement.py +++ b/hrms/hr/doctype/full_and_final_statement/test_full_and_final_statement.py @@ -34,7 +34,7 @@ def test_check_bootstraped_data_asset_movement_and_jv_creation(self): "Leave Encashment", ] - receivable_bootstraped_component = ["Employee Advance", "Loan"] + receivable_bootstraped_component = self.fnf.get_receivable_component() # checking payables and receivables bootstraped value self.assertEqual([payable.component for payable in self.fnf.payables], payables_bootstraped_component) diff --git a/hrms/hr/doctype/leave_control_panel/leave_control_panel.json b/hrms/hr/doctype/leave_control_panel/leave_control_panel.json index 9e96e61a83..4aeacfc519 100644 --- a/hrms/hr/doctype/leave_control_panel/leave_control_panel.json +++ b/hrms/hr/doctype/leave_control_panel/leave_control_panel.json @@ -186,7 +186,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2024-03-20 15:05:39.635388", + "modified": "2025-01-13 13:47:55.262534", "modified_by": "Administrator", "module": "HR", "name": "Leave Control Panel", @@ -199,8 +199,7 @@ "write": 1 } ], - "read_only": 1, - "sort_field": "modified", + "sort_field": "creation", "sort_order": "DESC", "states": [] } \ No newline at end of file diff --git a/hrms/hr/doctype/leave_encashment/leave_encashment.py b/hrms/hr/doctype/leave_encashment/leave_encashment.py index b32f9ebd67..66481f6bea 100644 --- a/hrms/hr/doctype/leave_encashment/leave_encashment.py +++ b/hrms/hr/doctype/leave_encashment/leave_encashment.py @@ -204,7 +204,9 @@ def create_leave_ledger_entry(self, submit=True): return to_date = leave_allocation.get("to_date") - if to_date < getdate(): + + can_expire = not frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") + if to_date < getdate() and can_expire: args = frappe._dict( leaves=self.encashment_days, from_date=to_date, to_date=to_date, is_carry_forward=0 ) 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 dd1718aef3..74d467730f 100644 --- a/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -4,13 +4,26 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import DATE_FORMAT, flt, get_link_to_form, getdate, today +from frappe.utils import DATE_FORMAT, flt, formatdate, get_link_to_form, getdate, today + + +class InvalidLeaveLedgerEntry(frappe.ValidationError): + pass class LeaveLedgerEntry(Document): def validate(self): if getdate(self.from_date) > getdate(self.to_date): - frappe.throw(_("To date needs to be before from date")) + frappe.throw( + _( + "Leave Ledger Entry's To date needs to be after From date. Currently, From Date is {0} and To Date is {1}" + ).format( + frappe.bold(formatdate(self.from_date)), + frappe.bold(formatdate(self.to_date)), + ), + exc=InvalidLeaveLedgerEntry, + title=_("Invalid Leave Ledger Entry"), + ) def on_cancel(self): # allow cancellation of expiry leaves diff --git a/hrms/hr/doctype/shift_assignment_tool/shift_assignment_tool.json b/hrms/hr/doctype/shift_assignment_tool/shift_assignment_tool.json index ace22bbcfc..f6fc00be05 100644 --- a/hrms/hr/doctype/shift_assignment_tool/shift_assignment_tool.json +++ b/hrms/hr/doctype/shift_assignment_tool/shift_assignment_tool.json @@ -209,7 +209,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2024-12-13 17:38:45.675004", + "modified": "2025-01-13 13:48:33.710186", "modified_by": "Administrator", "module": "HR", "name": "Shift Assignment Tool", @@ -225,7 +225,6 @@ "write": 1 } ], - "read_only": 1, "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/hrms/payroll/doctype/bulk_salary_structure_assignment/bulk_salary_structure_assignment.json b/hrms/payroll/doctype/bulk_salary_structure_assignment/bulk_salary_structure_assignment.json index d9eb073199..a0abf3760e 100644 --- a/hrms/payroll/doctype/bulk_salary_structure_assignment/bulk_salary_structure_assignment.json +++ b/hrms/payroll/doctype/bulk_salary_structure_assignment/bulk_salary_structure_assignment.json @@ -147,7 +147,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2024-07-09 19:33:40.135057", + "modified": "2025-01-13 13:48:46.095481", "modified_by": "Administrator", "module": "Payroll", "name": "Bulk Salary Structure Assignment", @@ -163,8 +163,7 @@ "write": 1 } ], - "read_only": 1, - "sort_field": "modified", + "sort_field": "creation", "sort_order": "DESC", "states": [] } \ No newline at end of file diff --git a/hrms/payroll/doctype/gratuity/gratuity.py b/hrms/payroll/doctype/gratuity/gratuity.py index de25701a0d..ca6f2b7a7c 100644 --- a/hrms/payroll/doctype/gratuity/gratuity.py +++ b/hrms/payroll/doctype/gratuity/gratuity.py @@ -44,7 +44,7 @@ def set_status(self, update=False): else: status = "Unpaid" - if update: + if update and self.status != status: self.db_set("status", status) else: self.status = status diff --git a/hrms/payroll/doctype/gratuity/test_gratuity.py b/hrms/payroll/doctype/gratuity/test_gratuity.py index ffb8285e3f..e16e5baa59 100644 --- a/hrms/payroll/doctype/gratuity/test_gratuity.py +++ b/hrms/payroll/doctype/gratuity/test_gratuity.py @@ -171,6 +171,53 @@ def test_gratuity_amount_consistent_irrespective_of_payment_days(self): ) self.assertEqual(gratuity.amount, 190000.0) + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_settle_gratuity_via_fnf_statement(self): + from hrms.hr.doctype.full_and_final_statement.test_full_and_final_statement import ( + create_full_and_final_statement, + ) + + create_salary_slip(self.employee) + setup_gratuity_rule("Rule Under Limited Contract (UAE)") + set_mode_of_payment_account() + + # create gratuity + gratuity = create_gratuity( + expense_account="Payment Account - _TC", mode_of_payment="Cash", employee=self.employee + ) + gratuity.reload() + + # create Full and Final Statement and add gratuity as Payables + fnf = create_full_and_final_statement(self.employee) + fnf.payables = [] + fnf.receivables = [] + fnf.append( + "payables", + { + "component": "Gratuity", + "reference_document_type": "Gratuity", + "reference_document": gratuity.name, + "amount": gratuity.amount, + "account": gratuity.payable_account, + "status": "Settled", + }, + ) + fnf.submit() + + jv = fnf.create_journal_entry() + jv.accounts[1].account = frappe.get_cached_value("Company", "_Test Company", "default_bank_account") + jv.cheque_no = "123456" + jv.cheque_date = getdate() + jv.save() + jv.submit() + + gratuity.reload() + self.assertEqual(gratuity.status, "Paid") + + jv.cancel() + gratuity.reload() + self.assertEqual(gratuity.status, "Unpaid") + def setup_gratuity_rule(name: str) -> dict: from hrms.regional.united_arab_emirates.setup import setup @@ -201,6 +248,7 @@ def create_gratuity(**args): gratuity.expense_account = args.expense_account or "Payment Account - _TC" gratuity.payable_account = args.payable_account or get_payable_account("_Test Company") gratuity.mode_of_payment = args.mode_of_payment or "Cash" + gratuity.cost_center = args.cost_center or "Main - _TC" gratuity.save() gratuity.submit() 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 00dc6a5eba..01dcf89017 100644 --- a/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/hrms/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -172,29 +172,7 @@ def are_opening_entries_required(self) -> bool: if not get_tax_component(self.salary_structure): return False - if self.has_emp_joined_after_payroll_period_start() and not self.has_existing_salary_slips(): - return True - else: - if not self.docstatus.is_draft() and ( - self.taxable_earnings_till_date or self.tax_deducted_till_date - ): - return True - return False - - def has_existing_salary_slips(self) -> bool: - return bool( - frappe.db.exists( - "Salary Slip", - {"employee": self.employee, "docstatus": 1}, - ) - ) - - def has_emp_joined_after_payroll_period_start(self) -> bool: - date_of_joining = getdate(frappe.db.get_value("Employee", self.employee, "date_of_joining")) - payroll_period = get_payroll_period(self.from_date, self.from_date, self.company) - if not payroll_period or date_of_joining > getdate(payroll_period.start_date): - return True - return False + return True def get_assigned_salary_structure(employee, on_date): diff --git a/hrms/www/jobs/index.py b/hrms/www/jobs/index.py index 54cf4e2910..f8f100f134 100644 --- a/hrms/www/jobs/index.py +++ b/hrms/www/jobs/index.py @@ -9,7 +9,10 @@ def get_context(context): context.no_cache = 1 - context.parents = [{"name": _("My Account"), "route": "/"}] + if frappe.session.user == "Guest": + context.parents = [{"name": _("Home"), "route": "/"}] + else: + context.parents = [{"name": _("My Account"), "route": "/me"}] context.body_class = "jobs-page" page_len = 20 filters, txt, sort, offset = get_filters_txt_sort_offset(page_len) diff --git a/roster/src/components/MonthViewTable.vue b/roster/src/components/MonthViewTable.vue index ca3158c486..8d9a6ca33e 100644 --- a/roster/src/components/MonthViewTable.vue +++ b/roster/src/components/MonthViewTable.vue @@ -112,11 +112,13 @@ v-if="events.data?.[employee.name]?.[day.date]?.holiday" class="blocked-cell" > - {{ - events.data[employee.name][day.date].weekly_off - ? "WO" - : events.data[employee.name][day.date].description - }} +