diff --git a/one_fm/accommodation/doctype/accommodation/accommodation.py b/one_fm/accommodation/doctype/accommodation/accommodation.py index 6fdde30f64..5d1a00dd6b 100644 --- a/one_fm/accommodation/doctype/accommodation/accommodation.py +++ b/one_fm/accommodation/doctype/accommodation/accommodation.py @@ -6,6 +6,7 @@ import frappe import functools from frappe.model.document import Document +from frappe import _ class Accommodation(Document): def autoname(self): @@ -59,3 +60,38 @@ def accommodation_contact_update(doc, method): link_name = doc.get_link_for('Accommodation') if link_name and doc.one_fm_doc_contact_field: frappe.db.set_value('Accommodation', link_name, doc.one_fm_doc_contact_field, doc.name) + + +def validate_contact(doc, method): + if doc.has_value_changed("first_name") or doc.has_value_changed("last_name"): + + base_name = f"{doc.first_name or ''} {doc.last_name or ''}".strip() + + if base_name and base_name != doc.name: + new_name = make_unique_name(base_name) + + if new_name != doc.name: + frappe.rename_doc("Contact", doc.name, new_name, force=True) + doc.name = new_name + doc.db_set("last_name", doc.last_name) + doc.db_set("first_name", doc.first_name) + + # Notify frontend to redirect to the new URL + frappe.msgprint( + _("The contact has been renamed. Redirecting to the new page..."), + indicator="green" + ) + frappe.local.response["location"] = frappe.utils.get_url(f"/app/contact/{new_name}") + + +def make_unique_name(base_name, count=0): + """ + Generates a unique name by appending a counter if necessary. + """ + + unique_name = f"{base_name}-{count}" if count > 0 else base_name + + if frappe.db.exists("Contact", unique_name): + return make_unique_name(base_name, count + 1) + + return unique_name diff --git a/one_fm/after_migrate/execute.py b/one_fm/after_migrate/execute.py index e4f7b8d661..fc3e18c978 100644 --- a/one_fm/after_migrate/execute.py +++ b/one_fm/after_migrate/execute.py @@ -425,4 +425,4 @@ def run_command(command, cwd=None, shell=True): except subprocess.CalledProcessError as e: print(f"An error occurred while running the command: {e}") print(f"Output: {e.stdout}") - print(f"Error: {e.stderr}") \ No newline at end of file + print(f"Error: {e.stderr}") diff --git a/one_fm/api/v1/leave_application.py b/one_fm/api/v1/leave_application.py index 8b23fbd1fa..7b9a41ce99 100644 --- a/one_fm/api/v1/leave_application.py +++ b/one_fm/api/v1/leave_application.py @@ -376,6 +376,11 @@ def proof_document_required_for_leave_type(leave_type): def leave_approver_action(leave_id: str,status: str) -> dict: try: doc = frappe.get_doc("Leave Application",{"name":leave_id}) + has_leave_approver_role = "Leave Approver" in frappe.get_roles(frappe.session.user) + + if not has_leave_approver_role: + return response("error", 403, {}, "You are not allowed to approve leave applications") + if doc: if not doc.leave_approver in [frappe.session.user, 'administrator']: return response("error", 401, {}, "Unauthorised.") diff --git a/one_fm/fixtures/workflow.json b/one_fm/fixtures/workflow.json index 6b1ccdcf28..45b84d0dbf 100755 --- a/one_fm/fixtures/workflow.json +++ b/one_fm/fixtures/workflow.json @@ -1474,7 +1474,7 @@ "workflow_builder_id": null }, { - "allow_edit": "Purchase Manager", + "allow_edit": "Procurement Manager", "avoid_status_override": 0, "doc_status": "0", "is_optional_state": 0, @@ -1489,7 +1489,7 @@ "workflow_builder_id": null }, { - "allow_edit": "Finance PO Approver", + "allow_edit": "Procurement Manager", "avoid_status_override": 0, "doc_status": "0", "is_optional_state": 0, @@ -1504,7 +1504,7 @@ "workflow_builder_id": null }, { - "allow_edit": "Purchase User", + "allow_edit": "Procurement Manager", "avoid_status_override": 0, "doc_status": "0", "is_optional_state": 0, @@ -1519,7 +1519,7 @@ "workflow_builder_id": null }, { - "allow_edit": "Purchase User", + "allow_edit": "Procurement Manager", "avoid_status_override": 0, "doc_status": "1", "is_optional_state": 0, diff --git a/one_fm/hiring/doctype/hiring_settings/hiring_settings.json b/one_fm/hiring/doctype/hiring_settings/hiring_settings.json index 0568d49a0e..2d1230c649 100644 --- a/one_fm/hiring/doctype/hiring_settings/hiring_settings.json +++ b/one_fm/hiring/doctype/hiring_settings/hiring_settings.json @@ -16,6 +16,12 @@ "performance_profile_resource", "performance_profile_guid", "job_offer_section", + "auto_email_job_offer", + "job_offer_workflow_state", + "auto_email_hiring_method", + "job_offer_print_format", + "job_offer_email_template", + "column_break_vjwl", "notify_finance_department_for_job_offer_salary_advance", "default_terms_and_conditions", "job_applicaion_section", @@ -143,11 +149,57 @@ "fieldtype": "Link", "label": "Default Terms and Conditions", "options": "Terms and Conditions" + }, + { + "default": "0", + "description": "If Checked, then the system will Email Job Offer Automatically on Workflow State and Hiring Method", + "fieldname": "auto_email_job_offer", + "fieldtype": "Check", + "label": "Auto Email Job Offer" + }, + { + "fieldname": "column_break_vjwl", + "fieldtype": "Column Break" + }, + { + "depends_on": "auto_email_job_offer", + "description": "Email Job Offer based on the Hiring Method selected here", + "fieldname": "auto_email_hiring_method", + "fieldtype": "Select", + "label": "Auto Email Hiring Method", + "mandatory_depends_on": "auto_email_job_offer", + "options": "\nBulk Recruitment\nA la carte Recruitment\nAll Recruitment" + }, + { + "depends_on": "auto_email_job_offer", + "description": "Email Job Offer on the selected workflow state", + "fieldname": "job_offer_workflow_state", + "fieldtype": "Link", + "label": "Auto Email Job Offer Workflow State", + "mandatory_depends_on": "auto_email_job_offer", + "options": "Workflow State" + }, + { + "depends_on": "auto_email_job_offer", + "description": "Selected Email Template will be used in Job Offer Auto Email. If not selected any default message will send to the candidate.", + "fieldname": "job_offer_email_template", + "fieldtype": "Link", + "label": "Job Offer Email Template", + "options": "Email Template" + }, + { + "depends_on": "auto_email_job_offer", + "description": "Selected Print Format will use to attach in the email", + "fieldname": "job_offer_print_format", + "fieldtype": "Link", + "label": "Job Offer Print Format", + "mandatory_depends_on": "auto_email_job_offer", + "options": "Print Format" } ], "issingle": 1, "links": [], - "modified": "2024-02-27 08:29:05.173163", + "modified": "2024-12-30 17:44:45.951842", "modified_by": "Administrator", "module": "Hiring", "name": "Hiring Settings", diff --git a/one_fm/hiring/doctype/hiring_settings/hiring_settings.py b/one_fm/hiring/doctype/hiring_settings/hiring_settings.py index c2b45f1cdf..6d1303c7ad 100644 --- a/one_fm/hiring/doctype/hiring_settings/hiring_settings.py +++ b/one_fm/hiring/doctype/hiring_settings/hiring_settings.py @@ -3,8 +3,25 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document class HiringSettings(Document): pass + + +def get_job_offer_auto_email_settings(): + """ + Retrieves the job offer auto-email settings from the 'Hiring Settings' doctype. + + The function fetches specific fields related to automatic job offer emails, + such as whether auto-emailing is enabled, the workflow state to trigger the email, + the hiring method, and the email template to use. + + Returns: + dict: A dictionary containing the values of the specified fields from the 'Hiring Settings' doctype. + """ + fields = [ + 'auto_email_job_offer', 'job_offer_workflow_state', 'auto_email_hiring_method', 'job_offer_email_template' + ] + return frappe.db.get_value('Hiring Settings', None, fields, as_dict=1) diff --git a/one_fm/hooks.py b/one_fm/hooks.py index 5431bb2f00..23a70f0207 100644 --- a/one_fm/hooks.py +++ b/one_fm/hooks.py @@ -117,7 +117,8 @@ "Task": "public/js/doctype_js/task.js", "HD Ticket": "public/js/doctype_js/hd_ticket.js", "Appraisal": "public/js/doctype_js/appraisal.js", - "Employee Performance Feedback":"public/js/doctype_js/employee_performance_feedback.js" + "Employee Performance Feedback":"public/js/doctype_js/employee_performance_feedback.js", + "Contact": "public/js/doctype_js/contact.js" } doctype_list_js = { "Job Applicant" : "public/js/doctype_js/job_applicant_list.js", @@ -249,7 +250,7 @@ "after_insert":[ "one_fm.overrides.hd_ticket.send_google_chat_notification", "one_fm.overrides.hd_ticket.notify_ticket_raiser_of_receipt" - ], + ], "on_change": "one_fm.overrides.hd_ticket.notify_issue_raiser_about_priority", "on_update": "one_fm.overrides.hd_ticket.apply_ticket_escalation" }, @@ -262,7 +263,11 @@ "Job Applicant": { "validate": "one_fm.utils.validate_job_applicant", "onload": "one_fm.utils.validate_pam_file_number_and_pam_designation", - "on_update": "one_fm.one_fm.utils.send_notification_to_grd_or_recruiter" + "on_update": [ + "one_fm.one_fm.utils.send_notification_to_grd_or_recruiter", + "one_fm.utils.on_update_job_applicant" + ] + }, "Warehouse": { "autoname": "one_fm.utils.warehouse_naming_series", @@ -306,7 +311,8 @@ ] }, "Contact": { - "on_update": "one_fm.accommodation.doctype.accommodation.accommodation.accommodation_contact_update" + "on_update": "one_fm.accommodation.doctype.accommodation.accommodation.accommodation_contact_update", + "validate": "one_fm.accommodation.doctype.accommodation.accommodation.validate_contact", }, "Project": { "validate": [ @@ -376,7 +382,7 @@ "after_insert": [ "one_fm.utils.assign_issue", "one_fm.api.doc_methods.issue.notify_issue_raiser" - + ], "on_update": "one_fm.utils.notify_on_close", }, @@ -508,7 +514,9 @@ "Payroll Entry": "one_fm.overrides.payroll_entry.PayrollEntryOverride", "Salary Slip": "one_fm.overrides.salary_slip.SalarySlipOverride", "Interview Feedback": "one_fm.overrides.interview_feedback.InterviewFeedbackOverride", - + "Leave Allocation": "one_fm.overrides.leave_allocation.LeaveAllocationOverride", + "Interview": "one_fm.overrides.interview.InterviewOverride", + # "User": "one_fm.overrides.user.UserOverride" } diff --git a/one_fm/one_fm/custom/designation_skill.json b/one_fm/one_fm/custom/designation_skill.json index 5e78b38dcf..614e7bf8e6 100644 --- a/one_fm/one_fm/custom/designation_skill.json +++ b/one_fm/one_fm/custom/designation_skill.json @@ -47,9 +47,6 @@ "non_negative": 0, "options": "", "owner": "Administrator", - "parent": null, - "parentfield": null, - "parenttype": null, "permlevel": 0, "precision": "", "print_hide": 0, diff --git a/one_fm/one_fm/custom/job_applicant.json b/one_fm/one_fm/custom/job_applicant.json index d85634267e..05174c6e00 100644 --- a/one_fm/one_fm/custom/job_applicant.json +++ b/one_fm/one_fm/custom/job_applicant.json @@ -9734,14 +9734,14 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "one_fm_applicant_status", + "insert_after": "mark_as_shortlisted_first", "is_system_generated": 0, "is_virtual": 0, "label": "Document Verification", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2020-05-18 22:55:55.326656", + "modified": "2025-01-13 18:39:53.132754", "modified_by": "Administrator", "module": null, "name": "Job Applicant-one_fm_document_verification", @@ -11747,6 +11747,69 @@ "translatable": 1, "unique": 0, "width": null + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2025-01-13 18:31:36.654134", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "dt": "Job Applicant", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "mark_as_shortlisted_first", + "fieldtype": "Check", + "hidden": 1, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 17, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "one_fm_applicant_status", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Mark as Shortlisted First", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2025-01-13 18:39:53.114243", + "modified_by": "Administrator", + "module": null, + "name": "Job Applicant-custom_mark_as_shortlisted_first", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null } ], "custom_perms": [], @@ -11777,7 +11840,7 @@ "property": "field_order", "property_type": "Data", "row_name": null, - "value": "[\"interview_rounds_sb\", \"interview_rounds\", \"details_section\", \"applicant_name\", \"email_id\", \"one_fm_application_id\", \"phone_number\", \"country\", \"column_break_3\", \"job_title\", \"one_fm_designation\", \"applicant_lead\", \"designation\", \"status\", \"one_fm_reason_for_rejection\", \"one_fm_applicant_status\", \"one_fm_document_verification\", \"source_and_rating_section\", \"source\", \"source_name\", \"one_fm_erf_application_details_section\", \"one_fm_erf\", \"project\", \"department\", \"one_fm_sourcing_team\", \"one_fm_agency\", \"one_fm_is_agency_applying\", \"one_fm_job_applicant_cb_1\", \"one_fm_source_of_hire\", \"one_fm_hiring_method\", \"interview_round\", \"bulk_interview\", \"employment_type\", \"attendance_by_timesheet\", \"one_fm_applicant_personal_details_sb\", \"one_fm_first_name\", \"one_fm_second_name\", \"one_fm_third_name\", \"one_fm_forth_name\", \"one_fm_last_name\", \"one_fm_first_name_in_arabic\", \"one_fm_second_name_in_arabic\", \"one_fm_third_name_in_arabic\", \"one_fm_forth_name_in_arabic\", \"one_fm_last_name_in_arabic\", \"one_fm_height\", \"one_fm_i_am_currently_working\", \"one_fm_applicant_demographics_cb\", \"one_fm_gender\", \"one_fm_religion\", \"one_fm_date_of_birth\", \"one_fm_place_of_birth\", \"one_fm_marital_status\", \"one_fm_nationality\", \"one_fm_date_of_entry\", \"employee_referral\", \"column_break_13\", \"applicant_rating\", \"section_break_6\", \"notes\", \"cover_letter\", \"resume_attachment\", \"children_details_section\", \"one_fm_number_of_kids\", \"one_fm_kids_details\", \"day_off_details\", \"day_off_category\", \"column_break_66\", \"number_of_days_off\", \"one_fm_work_details_section\", \"one_fm_rotation_shift\", \"one_fm_night_shift\", \"one_fm_work_details_cb\", \"one_fm_type_of_travel\", \"one_fm_type_of_driving_license\", \"one_fm_uniform_measurements\", \"one_fm_is_uniform_needed_for_this_job\", \"one_fm_shoulder_width\", \"one_fm_waist_size\", \"one_fm_shoe_size\", \"one_fm_basic_skill_section\", \"one_fm_designation_skill\", \"one_fm_documents_required_section\", \"one_fm_documents_required\", \"one_fm_is_easy_apply\", \"previous_work_details\", \"one_fm_work_permit_number\", \"one_fm_duration_of_work_permit\", \"one_fm_previous_designation\", \"column_break_51\", \"one_fm_work_permit_salary\", \"one_fm_last_working_date\", \"resume_link\", \"section_break_16\", \"currency\", \"column_break_18\", \"lower_range\", \"upper_range\", \"one_fm_contact_details_section\", \"one_fm_email_id\", \"one_fm_country_code\", \"one_fm_contact_number\", \"one_fm_country_code_second\", \"one_fm_secondary_contact_number\", \"one_fm_contact_cb\", \"one_fm_language_section\", \"one_fm_languages\", \"country_and_nationality_section\", \"nationality_no\", \"nationality_subject\", \"nationality_cb\", \"date_of_naturalization\", \"one_fm_passport_section\", \"one_fm_passport_number\", \"one_fm_passport_holder_of\", \"one_fm_passport_issued\", \"one_fm_passport_expire\", \"one_fm_passport_cb\", \"one_fm_passport_type\", \"one_fm_centralized_number\", \"one_fm_visa_and_residency_section\", \"one_fm_have_a_valid_visa_in_kuwait\", \"one_fm_visa_type\", \"one_fm_cid_number\", \"one_fm_cid_expire\", \"one_fm_in_kuwait_at_present\", \"one_fm_visa_cb\", \"one_fm_current_employment_section_\", \"one_fm_current_employer\", \"one_fm_current_employer_website_link\", \"one_fm_employment_start_date\", \"one_fm_employment_end_date\", \"one_fm_current_employment_cb\", \"one_fm_current_job_title\", \"one_fm_current_salary\", \"one_fm_notice_period_in_days\", \"one_fm_educational_qualification_section\", \"one_fm_educational_qualification\", \"other_education\", \"one_fm_education_specialization\", \"one_fm_educational_qualification_cb\", \"one_fm_university\", \"one_fm_country_of_employment\", \"section_break_88\", \"one_fm_are_you_currently_studying\", \"one_fm_current_educational_institution\", \"column_break_91\", \"one_fm_place_of_study\", \"one_fm_entry_date_of_current_educational_institution\", \"section_break_66\", \"one_fm_applicant_is_overseas_or_local\", \"one_fm_country_of_overseas\", \"one_fm_is_transferable\", \"custom_transfer_reminder_date\", \"column_break_72\", \"one_fm_applicant_password\", \"authorized_signatory\", \"one_fm_old_number\", \"one_fm_old_designation\", \"one_fm_erf_pam_file_number\", \"one_fm_erf_pam_designation\", \"send_changes_to_supervisor\", \"accept_changes\", \"reject_changes\", \"suggestions\", \"save_me\", \"no_internal_issues\", \"one_fm_has_issue\", \"one_fm_type_of_issues\", \"column_break_149\", \"one_fm_change_pam_file_number\", \"pam_number_button\", \"pam_designation_button\", \"one_fm_change_pam_designation\", \"column_break_152\", \"one_fm_pam_file_number\", \"one_fm_pam_designation\", \"column_break_154\", \"one_fm_file_number\", \"one_fm_notify_recruiter\", \"previous_company_details\", \"one_fm_previous_company_trade_name_in_arabic\", \"one_fm__previous_company_authorized_signatory_name_arabic\", \"one_fm_previous_company_issuer_number\", \"column_break_142\", \"one_fm_previous_company_pam_file_number\", \"one_fm_government_project\", \"authorized_signatory_section\", \"one_fm_pam_authorized_signatory\", \"one_fm_signatory_name\", \"one_fm_grd_operator\", \"scans\", \"passport_data_page\", \"civil_id_front\", \"civil_id_back\", \"magic_link_details\", \"career_history_ml\", \"career_history_ml_url\", \"career_history_ml_expired\", \"column_break_avxor\", \"applicant_doc_ml\", \"applicant_doc_ml_url\", \"applicant_doc_ml_expired\"]" + "value": "[\"interview_rounds_sb\", \"interview_rounds\", \"details_section\", \"applicant_name\", \"email_id\", \"one_fm_application_id\", \"phone_number\", \"country\", \"column_break_3\", \"job_title\", \"one_fm_designation\", \"applicant_lead\", \"designation\", \"status\", \"one_fm_reason_for_rejection\", \"one_fm_applicant_status\", \"mark_as_shortlisted_first\", \"one_fm_document_verification\", \"source_and_rating_section\", \"source\", \"source_name\", \"one_fm_erf_application_details_section\", \"one_fm_erf\", \"project\", \"department\", \"one_fm_sourcing_team\", \"one_fm_agency\", \"one_fm_is_agency_applying\", \"one_fm_job_applicant_cb_1\", \"one_fm_source_of_hire\", \"one_fm_hiring_method\", \"interview_round\", \"bulk_interview\", \"employment_type\", \"attendance_by_timesheet\", \"one_fm_applicant_personal_details_sb\", \"one_fm_first_name\", \"one_fm_second_name\", \"one_fm_third_name\", \"one_fm_forth_name\", \"one_fm_last_name\", \"one_fm_first_name_in_arabic\", \"one_fm_second_name_in_arabic\", \"one_fm_third_name_in_arabic\", \"one_fm_forth_name_in_arabic\", \"one_fm_last_name_in_arabic\", \"one_fm_height\", \"one_fm_i_am_currently_working\", \"one_fm_applicant_demographics_cb\", \"one_fm_gender\", \"one_fm_religion\", \"one_fm_date_of_birth\", \"one_fm_place_of_birth\", \"one_fm_marital_status\", \"one_fm_nationality\", \"one_fm_date_of_entry\", \"employee_referral\", \"column_break_13\", \"applicant_rating\", \"section_break_6\", \"notes\", \"cover_letter\", \"resume_attachment\", \"children_details_section\", \"one_fm_number_of_kids\", \"one_fm_kids_details\", \"day_off_details\", \"day_off_category\", \"column_break_66\", \"number_of_days_off\", \"one_fm_work_details_section\", \"one_fm_rotation_shift\", \"one_fm_night_shift\", \"one_fm_work_details_cb\", \"one_fm_type_of_travel\", \"one_fm_type_of_driving_license\", \"one_fm_uniform_measurements\", \"one_fm_is_uniform_needed_for_this_job\", \"one_fm_shoulder_width\", \"one_fm_waist_size\", \"one_fm_shoe_size\", \"one_fm_basic_skill_section\", \"one_fm_designation_skill\", \"one_fm_documents_required_section\", \"one_fm_documents_required\", \"one_fm_is_easy_apply\", \"previous_work_details\", \"one_fm_work_permit_number\", \"one_fm_duration_of_work_permit\", \"one_fm_previous_designation\", \"column_break_51\", \"one_fm_work_permit_salary\", \"one_fm_last_working_date\", \"resume_link\", \"section_break_16\", \"currency\", \"column_break_18\", \"lower_range\", \"upper_range\", \"one_fm_contact_details_section\", \"one_fm_email_id\", \"one_fm_country_code\", \"one_fm_contact_number\", \"one_fm_country_code_second\", \"one_fm_secondary_contact_number\", \"one_fm_contact_cb\", \"one_fm_language_section\", \"one_fm_languages\", \"country_and_nationality_section\", \"nationality_no\", \"nationality_subject\", \"nationality_cb\", \"date_of_naturalization\", \"one_fm_passport_section\", \"one_fm_passport_number\", \"one_fm_passport_holder_of\", \"one_fm_passport_issued\", \"one_fm_passport_expire\", \"one_fm_passport_cb\", \"one_fm_passport_type\", \"one_fm_centralized_number\", \"one_fm_visa_and_residency_section\", \"one_fm_have_a_valid_visa_in_kuwait\", \"one_fm_visa_type\", \"one_fm_cid_number\", \"one_fm_cid_expire\", \"one_fm_in_kuwait_at_present\", \"one_fm_visa_cb\", \"one_fm_current_employment_section_\", \"one_fm_current_employer\", \"one_fm_current_employer_website_link\", \"one_fm_employment_start_date\", \"one_fm_employment_end_date\", \"one_fm_current_employment_cb\", \"one_fm_current_job_title\", \"one_fm_current_salary\", \"one_fm_notice_period_in_days\", \"one_fm_educational_qualification_section\", \"one_fm_educational_qualification\", \"other_education\", \"one_fm_education_specialization\", \"one_fm_educational_qualification_cb\", \"one_fm_university\", \"one_fm_country_of_employment\", \"section_break_88\", \"one_fm_are_you_currently_studying\", \"one_fm_current_educational_institution\", \"column_break_91\", \"one_fm_place_of_study\", \"one_fm_entry_date_of_current_educational_institution\", \"section_break_66\", \"one_fm_applicant_is_overseas_or_local\", \"one_fm_country_of_overseas\", \"one_fm_is_transferable\", \"custom_transfer_reminder_date\", \"column_break_72\", \"one_fm_applicant_password\", \"authorized_signatory\", \"one_fm_old_number\", \"one_fm_old_designation\", \"one_fm_erf_pam_file_number\", \"one_fm_erf_pam_designation\", \"send_changes_to_supervisor\", \"accept_changes\", \"reject_changes\", \"suggestions\", \"save_me\", \"no_internal_issues\", \"one_fm_has_issue\", \"one_fm_type_of_issues\", \"column_break_149\", \"one_fm_change_pam_file_number\", \"pam_number_button\", \"pam_designation_button\", \"one_fm_change_pam_designation\", \"column_break_152\", \"one_fm_pam_file_number\", \"one_fm_pam_designation\", \"column_break_154\", \"one_fm_file_number\", \"one_fm_notify_recruiter\", \"previous_company_details\", \"one_fm_previous_company_trade_name_in_arabic\", \"one_fm__previous_company_authorized_signatory_name_arabic\", \"one_fm_previous_company_issuer_number\", \"column_break_142\", \"one_fm_previous_company_pam_file_number\", \"one_fm_government_project\", \"authorized_signatory_section\", \"one_fm_pam_authorized_signatory\", \"one_fm_signatory_name\", \"one_fm_grd_operator\", \"scans\", \"passport_data_page\", \"civil_id_front\", \"civil_id_back\", \"magic_link_details\", \"career_history_ml\", \"career_history_ml_url\", \"career_history_ml_expired\", \"column_break_avxor\", \"applicant_doc_ml\", \"applicant_doc_ml_url\", \"applicant_doc_ml_expired\"]" }, { "_assign": null, @@ -11859,4 +11922,4 @@ } ], "sync_on_migrate": 1 -} \ No newline at end of file +} diff --git a/one_fm/one_fm/custom/task.json b/one_fm/one_fm/custom/task.json index 4b927464b1..6cd42ab5a8 100644 --- a/one_fm/one_fm/custom/task.json +++ b/one_fm/one_fm/custom/task.json @@ -1,5 +1,71 @@ { "custom_fields": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2024-12-19 14:20:42.358729", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "dt": "Task", + "fetch_from": "project.project_type", + "fetch_if_empty": 0, + "fieldname": "custom_project_type", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 4, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 1, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 1, + "insert_after": "project", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Project Type", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2024-12-19 14:20:42.358729", + "modified_by": "Administrator", + "module": null, + "name": "Task-custom_project_type", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "parent": null, + "parentfield": null, + "parenttype": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 1, + "unique": 0, + "width": null + }, { "_assign": null, "_comments": null, @@ -25,7 +91,7 @@ "hide_border": 0, "hide_days": 0, "hide_seconds": 0, - "idx": 19, + "idx": 20, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, @@ -91,7 +157,7 @@ "hide_border": 0, "hide_days": 0, "hide_seconds": 0, - "idx": 8, + "idx": 9, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, @@ -157,7 +223,7 @@ "hide_border": 0, "hide_days": 0, "hide_seconds": 0, - "idx": 7, + "idx": 8, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, @@ -223,7 +289,7 @@ "hide_border": 0, "hide_days": 0, "hide_seconds": 0, - "idx": 6, + "idx": 7, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, @@ -355,7 +421,7 @@ "hide_border": 0, "hide_days": 0, "hide_seconds": 0, - "idx": 53, + "idx": 54, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, @@ -538,7 +604,7 @@ "_comments": null, "_liked_by": null, "_user_tags": null, - "creation": "2024-06-23 13:48:05.138894", + "creation": "2024-12-19 16:35:26.949454", "default_value": null, "doc_type": "Task", "docstatus": 0, @@ -546,7 +612,7 @@ "field_name": null, "idx": 0, "is_system_generated": 0, - "modified": "2024-06-23 13:48:05.138894", + "modified": "2024-12-19 16:35:26.949454", "modified_by": "Administrator", "module": null, "name": "Task-main-field_order", @@ -557,7 +623,7 @@ "property": "field_order", "property_type": "Data", "row_name": null, - "value": "[\"github_sync_id\", \"subject\", \"project\", \"issue\", \"type\", \"is_routine_task\", \"routine_erp_document\", \"routine_erp_docname\", \"color\", \"is_group\", \"is_template\", \"column_break0\", \"status\", \"priority\", \"task_weight\", \"parent_task\", \"completed_by\", \"completed_on\", \"custom_assigned_to\", \"sb_timeline\", \"exp_start_date\", \"expected_time\", \"start\", \"column_break_11\", \"exp_end_date\", \"progress\", \"duration\", \"is_milestone\", \"sb_details\", \"description\", \"sb_depends_on\", \"depends_on\", \"depends_on_tasks\", \"sb_actual\", \"act_start_date\", \"actual_time\", \"column_break_15\", \"act_end_date\", \"sb_costing\", \"total_costing_amount\", \"total_expense_claim\", \"column_break_20\", \"total_billing_amount\", \"sb_more_info\", \"review_date\", \"closing_date\", \"column_break_22\", \"department\", \"company\", \"lft\", \"rgt\", \"old_parent\", \"email_sender\", \"auto_repeat\", \"template_task\"]" + "value": "[\"github_sync_id\", \"subject\", \"project\", \"custom_project_type\", \"issue\", \"type\", \"is_routine_task\", \"routine_erp_document\", \"routine_erp_docname\", \"color\", \"is_group\", \"is_template\", \"column_break0\", \"status\", \"priority\", \"task_weight\", \"parent_task\", \"completed_by\", \"completed_on\", \"custom_assigned_to\", \"sb_timeline\", \"exp_start_date\", \"expected_time\", \"start\", \"column_break_11\", \"exp_end_date\", \"progress\", \"duration\", \"is_milestone\", \"sb_details\", \"description\", \"sb_depends_on\", \"depends_on\", \"depends_on_tasks\", \"sb_actual\", \"act_start_date\", \"actual_time\", \"column_break_15\", \"act_end_date\", \"sb_costing\", \"total_costing_amount\", \"total_expense_claim\", \"column_break_20\", \"total_billing_amount\", \"sb_more_info\", \"review_date\", \"closing_date\", \"column_break_22\", \"department\", \"company\", \"lft\", \"rgt\", \"old_parent\", \"email_sender\", \"auto_repeat\", \"template_task\"]" }, { "_assign": null, @@ -572,7 +638,7 @@ "field_name": "type", "idx": 0, "is_system_generated": 0, - "modified": "2024-06-23 13:34:07.847200", + "modified": "2024-12-19 16:32:24.752271", "modified_by": "Administrator", "module": null, "name": "Task-type-allow_in_quick_entry", @@ -598,7 +664,7 @@ "field_name": null, "idx": 0, "is_system_generated": 0, - "modified": "2024-06-23 13:34:07.826700", + "modified": "2024-12-19 16:32:24.743202", "modified_by": "Administrator", "module": null, "name": "Task-main-allow_auto_repeat", @@ -624,7 +690,7 @@ "field_name": null, "idx": 0, "is_system_generated": 0, - "modified": "2024-06-23 13:34:07.804254", + "modified": "2024-12-19 16:32:24.733425", "modified_by": "Administrator", "module": null, "name": "Task-main-subject_field", @@ -650,7 +716,7 @@ "field_name": null, "idx": 0, "is_system_generated": 0, - "modified": "2024-06-23 13:34:07.779335", + "modified": "2024-12-19 16:32:24.724107", "modified_by": "Administrator", "module": null, "name": "Task-main-email_append_to", @@ -676,7 +742,7 @@ "field_name": null, "idx": 0, "is_system_generated": 0, - "modified": "2024-06-23 13:34:07.758608", + "modified": "2024-12-19 16:32:24.714153", "modified_by": "Administrator", "module": null, "name": "Task-main-sender_field", @@ -691,4 +757,4 @@ } ], "sync_on_migrate": 1 -} +} \ No newline at end of file diff --git a/one_fm/one_fm/doctype/erf/erf.py b/one_fm/one_fm/doctype/erf/erf.py index 7279a3a1dc..32de581f28 100755 --- a/one_fm/one_fm/doctype/erf/erf.py +++ b/one_fm/one_fm/doctype/erf/erf.py @@ -228,6 +228,8 @@ def on_update_after_submit(self): self.fix_erf_employee_indexing() self.validate_total_required_candidates() if self.workflow_state == "Accepted": + if not self.base or not self.salary_structure: + frappe.throw(_('The Base field and Salary Structure field must be filled!')) # self.validate_submit_to_hr() # if not self.hiring_method: # frappe.throw(_("Please set Hiring Method in HR section")) diff --git a/one_fm/one_fm/doctype/roster_employee_actions/roster_employee_actions.py b/one_fm/one_fm/doctype/roster_employee_actions/roster_employee_actions.py index c88744d689..b691e9de46 100644 --- a/one_fm/one_fm/doctype/roster_employee_actions/roster_employee_actions.py +++ b/one_fm/one_fm/doctype/roster_employee_actions/roster_employee_actions.py @@ -151,7 +151,8 @@ def get_shift_working_active_employees(start_date, end_date): active_employees = frappe.db.sql(""" select - employee + employee, + relieving_date from `tabEmployee` where @@ -159,11 +160,11 @@ def get_shift_working_active_employees(start_date, end_date): and shift_working = 1 """, as_dict=1) - return [ (employee.employee, (start_date + timedelta(days=x)).strftime('%Y-%m-%d')) for employee in active_employees for x in range((end_date - start_date).days + 1) + if employee.relieving_date is None or (start_date + timedelta(days=x)) < employee.relieving_date ] def get_rostered_employees(start_date, end_date): diff --git a/one_fm/one_fm/report/absentees_per_supervisor/__init__.py b/one_fm/one_fm/report/absentees_per_supervisor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.js b/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.js new file mode 100644 index 0000000000..bf87335326 --- /dev/null +++ b/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.js @@ -0,0 +1,28 @@ +// Copyright (c) 2025, omar jaber and contributors +// For license information, please see license.txt + +frappe.query_reports["Absentees Per Supervisor"] = { + "filters": [ + { + "fieldname": "start_date", + "label": __("Start Date"), + "fieldtype": "Date", + 'default':new Date(new Date().getFullYear(), new Date().getMonth(), 1), + "reqd": 1, + }, + { + "fieldname": "end_date", + "label": __("End Date"), + "fieldtype": "Date", + 'default':frappe.datetime.add_days(frappe.datetime.get_today(), -1), + "reqd": 1, + }, + { + "fieldname":"roster_type", + "label": __("Roster Type"), + "fieldtype": "Select", + "options": "\nBasic\nOver-Time", + "reqd": 0 + }, + ] +}; diff --git a/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.json b/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.json new file mode 100644 index 0000000000..788527248a --- /dev/null +++ b/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.json @@ -0,0 +1,35 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2025-01-09 09:20:25.169573", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "ONEFM", + "letterhead": null, + "modified": "2025-01-12 10:29:44.169380", + "modified_by": "Administrator", + "module": "One Fm", + "name": "Absentees Per Supervisor", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Attendance", + "reference_report": "Absentees Per Supervisor", + "report_name": "Absentees Per Supervisor", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Attendance Manager" + }, + { + "role": "Operations Manager" + } + ], + "timeout": 0 +} \ No newline at end of file diff --git a/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.py b/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.py new file mode 100644 index 0000000000..bdb8b02684 --- /dev/null +++ b/one_fm/one_fm/report/absentees_per_supervisor/absentees_per_supervisor.py @@ -0,0 +1,72 @@ +# Copyright (c) 2013, omar jaber and contributors +# For license information, please see license.txt +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import formatdate, getdate, flt, add_days +from datetime import datetime +import datetime +# import operator +import re +from datetime import date +from dateutil.relativedelta import relativedelta + +def execute(filters=None): + columns, data = get_columns(), get_data(filters) + return columns, data + +def get_columns(): + return [ + _("Operations Shift") + ":Link/Operations Shift:250", + _("Shift Supervisor Name") + ":Link/Operations Shift:250", + _("Operations Site") + ":Link/Operations Site:300", + _("Site Supervisor Name") + ":Data:250", + _("Number of Absentees") + ":Int:150", + ] + +def get_data(filters=None): + if not filters.get("start_date") or not filters.get("end_date"): + frappe.throw(_("Please specify Start Date and End Date")) + + grouped_data = get_absent_employees(filters.start_date,filters.end_date,filters.roster_type) + + final_data = [] + for shift, count in grouped_data: + if shift: + shift_doc = frappe.get_doc("Operations Shift", shift) + first_shift_supervisor = None + if shift_doc.shift_supervisor: + first_shift_supervisor = shift_doc.shift_supervisor[0].supervisor_name + + site_name = shift_doc.site + site_doc = frappe.get_doc("Operations Site", site_name) if site_name and frappe.db.exists("Operations Site", site_name) else None + final_data.append([ + shift_doc.name, # Operations Shift Name + first_shift_supervisor or "N/A", # Shift Supervisor Name + site_name or "N/A", # Operations Site Name + site_doc.account_supervisor_name if site_doc else "N/A", # Site Supervisor Name + count, # Number of Absentees + ]) + return final_data + + + +def get_absent_employees(start_date, end_date,roster_type): + attendance_records = frappe.get_all( + "Attendance", + filters={ + "attendance_date": ["between", [start_date, end_date]], + "status": "Absent", + "roster_type":roster_type, + }, + fields=["employee", "operations_shift", "attendance_date"]) + grouped_data = {} + for record in attendance_records: + shift = record.get("operations_shift") + if shift not in grouped_data: + grouped_data[shift] = 0 + grouped_data[shift] += 1 + sorted_shifts = sorted(grouped_data.items(), key=lambda x: x[1], reverse=True) + + return sorted_shifts + diff --git a/one_fm/overrides/interview.py b/one_fm/overrides/interview.py index 812a6d0c7f..5b9555243e 100644 --- a/one_fm/overrides/interview.py +++ b/one_fm/overrides/interview.py @@ -1,6 +1,9 @@ import frappe from frappe import _ +import json from frappe.utils import get_link_to_form +from hrms.hr.doctype.interview.interview import Interview +from one_fm.templates.pages.applicant_docs import send_applicant_doc_magic_link def validate_interview_overlap(self): interviewers = [entry.interviewer for entry in self.interview_details] or [""] @@ -43,3 +46,65 @@ def update_interview_rounds_in_job_applicant(doc, method): frappe.db.set_value('Job Applicant Interview Round', doc.interview_round_child_ref, 'interview', doc.name) if not doc.interview_details: doc.append('interview_details', {'interviewer': frappe.session.user}) + + +class InterviewOverride(Interview): + + def show_job_applicant_update_dialog(self): + job_applicant_status = self.get_job_applicant_status() + if not job_applicant_status: + return + job_application_name = frappe.db.get_value("Job Applicant", self.job_applicant, "applicant_name") + + if job_applicant_status == "Accepted": + frappe.publish_realtime( + event = 'show_job_applicant_update_dialog', + message = { + 'job_application_name': job_application_name, + 'job_applicant_status': job_applicant_status, + 'job_applicant': self.job_applicant + }, + user = frappe.session.user + ) + elif job_applicant_status == "Rejected": + frappe.msgprint( + _("Do you want to update the Job Applicant {0} as {1} based on this interview result?").format( + frappe.bold(job_application_name), frappe.bold(job_applicant_status) + ), + title=_("Update Job Applicant"), + primary_action={ + "label": _("Mark as {0}").format(job_applicant_status), + "server_action": "one_fm.overrides.interview.update_job_applicant_status", + "args": {"job_applicant": self.job_applicant, "status": job_applicant_status}, + }, + ) + + +@frappe.whitelist() +def update_job_applicant_status(args): + try: + if isinstance(args, str): + args = json.loads(args) + if not args.get("job_applicant"): + frappe.throw(_("Please specify the job applicant to be updated.")) + job_applicant = frappe.get_doc("Job Applicant", args["job_applicant"]) + if args["status"] == "Shortlisted": + job_applicant.one_fm_applicant_status = args["status"] + else: + job_applicant.status = args["status"] + job_applicant.save() + + if job_applicant.status == "Accepted": + send_applicant_doc_magic_link(job_applicant.name, job_applicant.applicant_name, job_applicant.one_fm_designation) + frappe.msgprint( + _("Updated the Job Applicant status to {0}").format(job_applicant.status), + alert=True, + indicator="green", + ) + except Exception: + job_applicant.log_error("Failed to update Job Applicant status") + frappe.msgprint( + _("Failed to update the Job Applicant status"), + alert=True, + indicator="red", + ) diff --git a/one_fm/overrides/job_applicant.py b/one_fm/overrides/job_applicant.py index 17ad3a3517..517243ab1a 100644 --- a/one_fm/overrides/job_applicant.py +++ b/one_fm/overrides/job_applicant.py @@ -21,6 +21,8 @@ def validate(self): super(JobApplicantOverride, self).validate() self.validate_transfer_reminder_date() self.convert_name_to_title_case() + if self.status == 'Open' and self.one_fm_applicant_status == 'Shortlisted': + self.mark_as_shortlisted_first = True def set_hiring_method(self): ''' diff --git a/one_fm/overrides/job_offer.py b/one_fm/overrides/job_offer.py index afc2cae6a0..9f4007f28a 100644 --- a/one_fm/overrides/job_offer.py +++ b/one_fm/overrides/job_offer.py @@ -10,6 +10,8 @@ from one_fm.api.notification import create_notification_log from frappe.desk.form.assign_to import add as add_assignment, DuplicateToDoError, close_all_assignments from one_fm.qr_code_generator import get_qr_code +from one_fm.hiring.doctype.hiring_settings.hiring_settings import get_job_offer_auto_email_settings +from frappe.model.workflow import apply_workflow class JobOfferOverride(JobOffer): @@ -124,11 +126,13 @@ def on_update_after_submit(self): def validate_job_offer_mandatory_fields(self): if self.workflow_state == 'Submit for Candidate Response': + applicant_details = frappe.db.get_value('Job Applicant', self.job_applicant, ['one_fm_hiring_method'], as_dict=1) mandatory_field_required = False - fields = ['Project', 'Base', 'Salary Structure'] if not self.attendance_by_timesheet else ['Base', 'Salary Structure'] + fields = ['Base', 'Salary Structure'] if not self.shift_working and not self.reports_to: fields.append("Reports to") - if not self.attendance_by_timesheet: + if not self.attendance_by_timesheet and applicant_details.one_fm_hiring_method != 'Bulk Recruitment': + fields.append('Project') fields.append('Operations Shift') if self.shift_working: fields.append('Operations Site') @@ -144,6 +148,76 @@ def validate_job_offer_mandatory_fields(self): if mandatory_field_required: frappe.throw(msg + '') + def on_update(self): + self.submit_job_offer_to_candidate() + self.auto_email_job_offer() + + def submit_job_offer_to_candidate(self): + if self.workflow_state == 'Open': + applicant_details = frappe.db.get_value( + 'Job Applicant', + self.job_applicant, + ['one_fm_hiring_method', 'one_fm_applicant_status', 'mark_as_shortlisted_first'], + as_dict=1 + ) + if applicant_details.one_fm_hiring_method == 'Bulk Recruitment' and applicant_details.one_fm_applicant_status == 'Selected' and not applicant_details.mark_as_shortlisted_first: + if frappe.session.user == 'Guest': + frappe.set_user('Administrator') + apply_workflow(self, 'Submit for Candidate Response') + + def auto_email_job_offer(self): + """ + Automatically sends a job offer email if specific conditions are met. + + Conditions: Auto email job offer setting is enabled and The workflow state of the job offer matches + the configured state and the hiring method matches the configured method or is set to 'All Recruitment'. + + Returns: + None + """ + auto_email_settings = get_job_offer_auto_email_settings() + if not auto_email_settings.auto_email_job_offer: + return + if self.workflow_state != auto_email_settings.job_offer_workflow_state: + return + hiring_method = frappe.db.get_value('Job Applicant', self.job_applicant, 'one_fm_hiring_method') + if auto_email_settings.auto_email_hiring_method not in [hiring_method, 'All Recruitment']: + return + + message = self.get_message_for_job_offer_email(auto_email_settings.job_offer_email_template) + attachment = frappe.attach_print( + 'Job Offer', self.name, file_name=self.name, print_format=auto_email_settings.job_offer_print_format + ) + email_args = { + "recipients": [self.applicant_email], + "message": message, + "subject": 'Job Offer: {0} [{1}]'.format(self.applicant_name, self.job_applicant), + "attachments": [attachment], + "reference_doctype": 'Job Offer', + "reference_name": self.name + } + frappe.sendmail(**email_args) + + def get_message_for_job_offer_email(self, email_template=None): + """ + Generates the message body for a job offer email. + + If an email template is provided, it retrieves the HTML content of the template + and renders it with context from the current Job Offer document. + If no template is provided, it returns a default message. + + Args: + email_template (str, optional): The name of the Email Template to use. Defaults to None. + + Returns: + str: The rendered email message or a default message if no template is specified. + """ + if email_template: + response_html = frappe.get_value('Email Template', email_template, 'response_html') + return frappe.render_template(response_html, self.as_dict()) + else: + return _("Please find the Job Offer attached and revert back with sign on the Job Offer") + def job_offer_validate_attendance_by_timesheet(self): if self.attendance_by_timesheet: self.shift_working = False @@ -158,7 +232,7 @@ def before_cancel(self): def reset_status_on_amend(self): if self.amended_from and self.status == "Rejected": - self.status = "Awaiting Response" + self.status = "Awaiting Response" def assign_to_onboarding_officer(self): try: diff --git a/one_fm/overrides/leave_allocation.py b/one_fm/overrides/leave_allocation.py new file mode 100644 index 0000000000..d251e54905 --- /dev/null +++ b/one_fm/overrides/leave_allocation.py @@ -0,0 +1,16 @@ +import frappe + + +from hrms.hr.doctype.leave_allocation.leave_allocation import LeaveAllocation + +class LeaveAllocationOverride(LeaveAllocation): + + + def validate(self): + super(LeaveAllocationOverride, self).validate() + + + def validate_leave_days_and_dates(self): + self.validate_back_dated_allocation() + self.validate_total_leaves_allocated() + # self.validate_leave_allocation_days() \ No newline at end of file diff --git a/one_fm/patches.txt b/one_fm/patches.txt index 4fce3a4b51..364080f93b 100644 --- a/one_fm/patches.txt +++ b/one_fm/patches.txt @@ -95,3 +95,4 @@ one_fm.patches.v15_0.disable_wrong_shift_request one_fm.patches.v15_0.update_job_offer_00057_erf one_fm.patches.v15_0.update_purchase_order_according_to_new_workflow one_fm.patches.v15_0.update_erf_for_job_offer +one_fm.patches.v15_0.populate_project_type_task diff --git a/one_fm/patches/v15_0/populate_project_type_task.py b/one_fm/patches/v15_0/populate_project_type_task.py new file mode 100644 index 0000000000..78bf3a8adc --- /dev/null +++ b/one_fm/patches/v15_0/populate_project_type_task.py @@ -0,0 +1,23 @@ +import frappe + + +def execute(): + try: + qs = frappe.db.sql(""" + SELECT + t.name AS task_name, + p.project_type AS project_type + FROM + `tabTask` AS t + JOIN + `tabProject` AS p + ON + t.project = p.name + WHERE + t.project IS NOT NULL + """, as_dict=True) + if qs: + for obj in qs: + frappe.db.set_value('Task', obj.get("task_name"), 'custom_project_type', obj.get("project_type")) + except Exception as e: + print(str(e)) \ No newline at end of file diff --git a/one_fm/public/js/doctype_js/contact.js b/one_fm/public/js/doctype_js/contact.js new file mode 100644 index 0000000000..99f4735275 --- /dev/null +++ b/one_fm/public/js/doctype_js/contact.js @@ -0,0 +1,15 @@ +frappe.ui.form.on('Contact', { + after_save: function (frm) { + + // Construct the new name based on the form data + let new_name = (frm.doc.first_name + " " + frm.doc.last_name).trim(); + console.log("Constructed New Name:", new_name); + + // Check if the new name is different from the current doc name + if (new_name !== frm.doc.name) { + console.log("Name has changed. Redirecting to the updated URL..."); + let new_url = frappe.urllib.get_base_url() + '/app/contact/' + encodeURIComponent(new_name); + window.location.href = new_url; + } + } +}); diff --git a/one_fm/public/js/doctype_js/job_applicant.js b/one_fm/public/js/doctype_js/job_applicant.js index 14e1f5e223..a373400ac3 100644 --- a/one_fm/public/js/doctype_js/job_applicant.js +++ b/one_fm/public/js/doctype_js/job_applicant.js @@ -8,7 +8,76 @@ frappe.ui.form.on('Job Applicant', { } }; }); + if (!frm.__is_realtime_listener_added) { + frappe.realtime.on('show_job_applicant_update_dialog', function(data) { + var job_application_name = data.job_application_name; + var job_applicant_status = data.job_applicant_status; + var job_applicant = data.job_applicant; + + // Create the dialog with buttons + var dialog = new frappe.ui.Dialog({ + title: __("Update Job Applicant"), + fields: [ + { + fieldname: "info", + fieldtype: "HTML", + options: `

Update the Job Applicant ${job_application_name} status based on this interview result.

`, + }, + ], + // Explicitly prevent any default action for buttons + no_submit_on_enter: true, + }); + + // Add primary button action + dialog.set_primary_action(`Mark as ${job_applicant_status}`, function() { + frappe.call({ + method: "one_fm.overrides.interview.update_job_applicant_status", + args: { + args: { + job_applicant: job_applicant, + status: job_applicant_status + } + }, + callback: function() { + frappe.show_alert({ + message: (`Job Applicant marked as ${job_applicant_status}`), + indicator: "green" + }); + }, + }); + dialog.hide(); + }); + + // Add secondary button action for "Mark as Shortlisted" + dialog.set_secondary_action(function() { + frappe.call({ + method: "one_fm.overrides.interview.update_job_applicant_status", + args: { + args: { + job_applicant: job_applicant, + status: "Shortlisted" + } + }, + callback: function() { + frappe.show_alert({ + message: ("Job Applicant marked as Shortlisted"), + indicator: "green" + }); + }, + }); + dialog.hide(); + }); + dialog.set_secondary_action_label("Mark as Shortlisted"); + + // Show the dialog + dialog.show(); + }); + + // Mark listener as added + frm.__is_realtime_listener_added = true; + } }, + refresh(frm) { // Changes the buttons for `PAM File Number` and `PAM Desigantion` once operator wants to changethe data of any // if(frm.doc.pam_number_button == 0 || frm.is_new()){ @@ -59,6 +128,22 @@ frappe.ui.form.on('Job Applicant', { },'Action'); if(frm.doc.one_fm_applicant_status != 'Selected' && frm.doc.status != 'Rejected'){ + if(frm.doc.status != 'Accepted'){ + frm.add_custom_button(__('Accept Applicant'), function() { + if(frm.doc.day_off_category && frm.doc.number_of_days_off && frm.doc.number_of_days_off > 0){ + frappe.confirm('Are you sure you want to set Final Status as Accepted for this applicant?', + () => { + // action to perform if Yes is selected + change_applicant_status(frm, 'status', 'Accepted'); + }, () => { + // action to perform if No is selected + }) + } + else{ + frappe.throw(__("Please Update Day off Details to Proceed !!")); + } + },"Action"); + } frm.add_custom_button(__('Select Applicant'), function() { if(frm.doc.day_off_category && frm.doc.number_of_days_off && frm.doc.number_of_days_off > 0){ frappe.confirm('Are you sure you want to select this applicant?', diff --git a/one_fm/utils.py b/one_fm/utils.py index 329518220f..5be132f5ce 100755 --- a/one_fm/utils.py +++ b/one_fm/utils.py @@ -1349,7 +1349,7 @@ def validate_item(doc, method): if doc.subitem_group == "Service": doc.is_stock_item = 0 - + doc.description = final_description doc.change_request = False item_approval_workflow_notification(doc) @@ -1505,8 +1505,6 @@ def validate_job_applicant(doc, method): set_required_documents(doc, method) if frappe.session.user != 'Guest' and not doc.one_fm_is_easy_apply: validate_mandatory_childs(doc) - if doc.one_fm_applicant_status in ["Shortlisted", "Selected"] and doc.status not in ["Rejected"]: - create_job_offer_from_job_applicant(doc.name) if doc.one_fm_number_of_kids and doc.one_fm_number_of_kids > 0: """This part is comparing the number of children with the listed children details in the table and ask user to add all childrens""" if doc.one_fm_number_of_kids != len(doc.one_fm_kids_details): @@ -1694,6 +1692,10 @@ def set_job_applicant_status(doc, method): status = 'Verified - With Exception' doc.one_fm_document_verification = status +def on_update_job_applicant(doc, method): + if doc.one_fm_applicant_status in ["Selected"] and doc.status not in ["Rejected"]: + create_job_offer_from_job_applicant(doc.name) + def create_job_offer_from_job_applicant(job_applicant): if not frappe.db.exists('Job Offer', {'job_applicant': job_applicant, 'docstatus': ['<', 2]}): job_app = frappe.get_doc('Job Applicant', job_applicant) @@ -1740,26 +1742,19 @@ def set_salary_details(job_offer, erf): job_offer.one_fm_job_offer_total_salary = total_amount def set_other_benefits_to_terms(job_offer, erf, job_app): - # if erf.other_benefits: - # for benefit in erf.other_benefits: - # terms = job_offer.append('offer_terms') - # terms.offer_term = benefit.benefit - # terms.value = 'Company Provided' - options = [{'provide_mobile_with_line':'Mobile with Line'}, {'provide_health_insurance':'Health Insurance'}, - {'provide_company_insurance': 'Company Insurance'}, {'provide_laptop_by_company': 'Personal Laptop'}, - {'provide_vehicle_by_company': 'Personal Vehicle'}] + options = { + 'provide_mobile_with_line':'Mobile with Line', 'provide_health_insurance':'Health Insurance', + 'provide_company_insurance': 'Company Insurance', 'provide_laptop_by_company': 'Personal Laptop', + 'provide_vehicle_by_company': 'Personal Vehicle', 'provide_accommodation_by_company': 'Accommodation', + 'provide_transportation_by_company': 'Transportation' + } + for option in options: if erf.get(option): terms = job_offer.append('offer_terms') terms.offer_term = options[option] terms.value = 'Company Provided' - terms_list = ['Kuwait Visa processing Fees', 'Kuwait Residency Fees', 'Kuwait insurance Fees'] - for term in terms_list: - terms = job_offer.append('offer_terms') - terms.offer_term = term - terms.value = 'Borne By The Company' - hours = erf.shift_hours if erf.shift_hours else 9 vacation_days = erf.vacation_days if erf.vacation_days else 30 terms = job_offer.append('offer_terms') @@ -2741,7 +2736,7 @@ def send_workflow_action_email(doc, recipients): def queue_send_workflow_action_email(doc, recipients): if recipients and (type(recipients)!=list): recipients = [recipients] - + workflow = get_workflow_name(doc.get("doctype")) next_possible_transitions = get_next_possible_transitions( workflow, get_doc_workflow_state(doc), doc @@ -3118,7 +3113,7 @@ def get_approver(employee, date=False): if not line_manager and employee_data.shift: line_manager = get_shift_supervisor(employee_data.shift, date) - + if not line_manager: frappe.msgprint( _("Please ensure that the Reports To or Operations Site Supervisor is set for {0}, Since the employee is not shift working".format(employee_data.employee_name)), @@ -3668,5 +3663,3 @@ def background_enqueue_run(report_name, filters=None, user=None): "name": track_instance.name, "redirect_url": get_url_to_form("Prepared Report", track_instance.name) } - - \ No newline at end of file diff --git a/one_fm/www/job_applicant_magic_link/index.py b/one_fm/www/job_applicant_magic_link/index.py index 94cc859b1f..7fa0cd8aa4 100644 --- a/one_fm/www/job_applicant_magic_link/index.py +++ b/one_fm/www/job_applicant_magic_link/index.py @@ -32,7 +32,7 @@ def get_magic_link(): job_applicant = frappe.get_doc('Job Applicant', magic_link.reference_docname) # get other required data like nationality, gender, ... civil_id_required = True if job_applicant.one_fm_nationality=='Kuwaiti' else False - civil_id_required = True if job_applicant.one_fm_have_a_valid_visa_in_kuwait else False + civil_id_required = True if job_applicant.one_fm_have_a_valid_visa_in_kuwait else False result['civil_id_required'] = civil_id_required nationalities = frappe.get_all("Nationality", fields=["name as nationality", "nationality_arabic", "country"]) nationalities_dict = {} @@ -44,7 +44,7 @@ def get_magic_link(): for i in countries: countries_dict[i.name] = i if nationalities_dict.get(i.name): - countries_dict[i.name] = {**countries_dict[i.name], **nationalities_dict.get(i.name)} + countries_dict[i.name] = {**countries_dict[i.name], **nationalities_dict.get(i.name)} result['job_applicant'] = job_applicant result['nationalities'] = nationalities_dict @@ -110,10 +110,10 @@ def upload_image(): # append to File doctype file_url = "/files/user/magic_link/"+filename filedoc = frappe.get_doc({ - "doctype":"File", + "doctype":"File", "is_private":0, - "file_url":"/files/user/magic_link/"+filename, - "attached_to_doctype":frappe.form_dict.reference_doctype, + "file_url":"/files/user/magic_link/"+filename, + "attached_to_doctype":frappe.form_dict.reference_doctype, "attached_to_name":frappe.form_dict.reference_docname }).insert(ignore_permissions=True) filedoc.db_set('file_url', file_url) @@ -157,7 +157,7 @@ def upload_image(): if country_code: result_dict.country = country_code # 'passport_file':filedoc.as_dict() - + for k, v in dict(result_dict).items(): if(k=="country") and v: frappe.db.set_value(frappe.form_dict.reference_doctype, frappe.form_dict.reference_docname, k, country_code) @@ -193,7 +193,7 @@ def upload_image(): except Exception as e: frappe.log_error(frappe.get_traceback(), "Mindee-Passport") errors.append("We could not process your passport document.") - + # civil id front if frappe.form_dict.civil_id_front: data_content = frappe._dict(frappe.form_dict.civil_id_front) @@ -207,10 +207,10 @@ def upload_image(): # append to File doctype file_url = "/files/user/magic_link/"+filename filedoc = frappe.get_doc({ - "doctype":"File", + "doctype":"File", "is_private":0, - "file_url":"/files/user/magic_link/"+filename, - "attached_to_doctype":frappe.form_dict.reference_doctype, + "file_url":"/files/user/magic_link/"+filename, + "attached_to_doctype":frappe.form_dict.reference_doctype, "attached_to_name":frappe.form_dict.reference_docname }).insert(ignore_permissions=True) filedoc.db_set('file_url', file_url) @@ -286,10 +286,10 @@ def upload_image(): # append to File doctype file_url = "/files/user/magic_link/"+filename filedoc = frappe.get_doc({ - "doctype":"File", + "doctype":"File", "is_private":0, - "file_url":"/files/user/magic_link/"+filename, - "attached_to_doctype":frappe.form_dict.reference_doctype, + "file_url":"/files/user/magic_link/"+filename, + "attached_to_doctype":frappe.form_dict.reference_doctype, "attached_to_name":frappe.form_dict.reference_docname }).insert(ignore_permissions=True) filedoc.db_set('file_url', file_url) @@ -307,7 +307,7 @@ def upload_image(): return frappe._dict({**response_data, **{'errors':errors}}) - + def is_date(string, fuzzy=False): """ Return whether the string can be interpreted as a date. @@ -321,7 +321,7 @@ def is_date(string, fuzzy=False): except ValueError: return False - + def delete_existing_files(reference_doctype,reference_docname, file_url_filter, newly_created_docname): bench_path = frappe.utils.get_bench_path() try: @@ -368,6 +368,13 @@ def update_job_applicant(): def submit_job_applicant(job_applicant): try: set_expire_magic_link('Job Applicant', job_applicant, 'Job Applicant') + select_applicant_on_magic_link_submit(job_applicant) return {'msg':'Your application has been successfully submitted, we will be intouch soonest.'} except Exception as e: - return {'error':e} \ No newline at end of file + return {'error':e} + +def select_applicant_on_magic_link_submit(job_applicant_id): + job_applicant = frappe.get_doc('Job Applicant', job_applicant_id) + if job_applicant.status == 'Accepted' and job_applicant.one_fm_hiring_method == 'Bulk Recruitment': + job_applicant.one_fm_applicant_status = 'Selected' + job_applicant.save(ignore_permissions=True)