Skip to content

Commit

Permalink
Merge pull request #1826 from frappe/version-15-hotfix
Browse files Browse the repository at this point in the history
chore: release v15
  • Loading branch information
ruchamahabal authored May 30, 2024
2 parents 0f39cd4 + 29ebc47 commit f3684c1
Show file tree
Hide file tree
Showing 15 changed files with 477 additions and 314 deletions.
111 changes: 83 additions & 28 deletions frontend/src/components/CheckInPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<div class="flex flex-col bg-white rounded w-full py-6 px-4 border-none">
<h2 class="text-lg font-bold text-gray-900">Hey, {{ employee?.data?.first_name }} 👋</h2>

<template v-if="allowCheckinFromMobile.data">
<template v-if="HRSettings.doc?.allow_employee_checkin_from_mobile_app">
<div class="font-medium text-sm text-gray-500 mt-1.5" v-if="lastLog">
Last {{ lastLogType }} was at {{ lastLogTime }}
</div>
<Button
class="mt-4 mb-1 drop-shadow-sm py-5 text-base"
id="open-checkin-modal"
@click="checkinTimestamp = dayjs().format('YYYY-MM-DD HH:mm:ss')"
@click="handleEmployeeCheckin"
>
<template #prefix>
<FeatherIcon
Expand All @@ -19,51 +19,72 @@
</template>
{{ nextAction.label }}
</Button>

<ion-modal
ref="modal"
trigger="open-checkin-modal"
:initial-breakpoint="1"
:breakpoints="[0, 1]"
>
<div class="h-40 w-full flex flex-col items-center justify-center gap-5 p-4 mb-5">
<div class="flex flex-col gap-1.5 items-center justify-center">
<div class="font-bold text-xl">
{{ dayjs(checkinTimestamp).format("hh:mm:ss a") }}
</div>
<div class="font-medium text-gray-500 text-sm">
{{ dayjs().format("D MMM, YYYY") }}
</div>
</div>
<Button
variant="solid"
class="w-full py-5 text-sm"
@click="submitLog(nextAction.action)"
>
Confirm {{ nextAction.label }}
</Button>
</div>
</ion-modal>
</template>

<div v-else class="font-medium text-sm text-gray-500 mt-1.5">
{{ dayjs().format("ddd, D MMMM, YYYY") }}
</div>
</div>

<ion-modal
v-if="HRSettings.doc?.allow_employee_checkin_from_mobile_app"
ref="modal"
trigger="open-checkin-modal"
:initial-breakpoint="1"
:breakpoints="[0, 1]"
>
<div class="h-120 w-full flex flex-col items-center justify-center gap-5 p-4 mb-5">
<div class="flex flex-col gap-1.5 mt-2 items-center justify-center">
<div class="font-bold text-xl">
{{ dayjs(checkinTimestamp).format("hh:mm:ss a") }}
</div>
<div class="font-medium text-gray-500 text-sm">
{{ dayjs().format("D MMM, YYYY") }}
</div>
</div>

<template v-if="HRSettings.doc?.allow_geolocation_tracking">
<span v-if="locationStatus" class="font-medium text-gray-500 text-sm">
{{ locationStatus }}
</span>

<div class="rounded border-4 translate-z-0 block overflow-hidden w-full h-170">
<iframe
width="100%"
height="170"
frameborder="0"
scrolling="no"
marginheight="0"
marginwidth="0"
style="border: 0"
:src="`https://maps.google.com/maps?q=${latitude},${longitude}&hl=en&z=15&amp;output=embed`"
>
</iframe>
</div>
</template>

<Button variant="solid" class="w-full py-5 text-sm" @click="submitLog(nextAction.action)">
Confirm {{ nextAction.label }}
</Button>
</div>
</ion-modal>
</template>

<script setup>
import { createListResource, toast, FeatherIcon } from "frappe-ui"
import { computed, inject, ref, onMounted, onBeforeUnmount } from "vue"
import { IonModal, modalController } from "@ionic/vue"
import { allowCheckinFromMobile } from "@/data/settings"
import { HRSettings } from "@/data/HRSettings"
const DOCTYPE = "Employee Checkin"
const socket = inject("$socket")
const employee = inject("$employee")
const dayjs = inject("$dayjs")
const checkinTimestamp = ref(null)
const latitude = ref(0)
const longitude = ref(0)
const locationStatus = ref("")
const checkins = createListResource({
doctype: DOCTYPE,
Expand Down Expand Up @@ -102,6 +123,38 @@ const lastLogTime = computed(() => {
return `${formattedTime} on ${dayjs(timestamp).format("D MMM, YYYY")}`
})
function handleLocationSuccess(position) {
latitude.value = position.coords.latitude
longitude.value = position.coords.longitude
locationStatus.value = `
Latitude: ${Number(latitude.value).toFixed(5)}°,
Longitude: ${Number(longitude.value).toFixed(5)}°
`
}
function handleLocationError(error) {
locationStatus.value = "Unable to retrieve your location"
if (error) locationStatus.value += `: ERROR(${error.code}): ${error.message}`
}
const fetchLocation = () => {
if (!navigator.geolocation) {
locationStatus.value = "Geolocation is not supported by your current browser"
} else {
locationStatus.value = "Locating..."
navigator.geolocation.getCurrentPosition(handleLocationSuccess, handleLocationError)
}
}
const handleEmployeeCheckin = () => {
checkinTimestamp.value = dayjs().format("YYYY-MM-DD HH:mm:ss")
if (HRSettings.doc?.allow_geolocation_tracking) {
fetchLocation()
}
}
const submitLog = (logType) => {
const action = logType === "IN" ? "Check-in" : "Check-out"
Expand All @@ -110,6 +163,8 @@ const submitLog = (logType) => {
employee: employee.data.name,
log_type: logType,
time: checkinTimestamp.value,
latitude: latitude.value,
longitude: longitude.value,
},
{
onSuccess() {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/data/HRSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createDocumentResource } from "frappe-ui"

export const HRSettings = createDocumentResource({
doctype: "HR Settings",
name: "HR Settings",
auto: true,
})
6 changes: 0 additions & 6 deletions frontend/src/data/settings.js

This file was deleted.

6 changes: 0 additions & 6 deletions hrms/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from frappe.model.workflow import get_workflow_name
from frappe.query_builder import Order
from frappe.utils import getdate
from frappe.utils.data import cint

SUPPORTED_FIELD_TYPES = [
"Link",
Expand Down Expand Up @@ -627,8 +626,3 @@ def get_workflow_state_field(doctype: str) -> str | None:
def get_allowed_states_for_workflow(workflow: dict, user_id: str) -> list[str]:
user_roles = frappe.get_roles(user_id)
return [transition.state for transition in workflow.transitions if transition.allowed in user_roles]


@frappe.whitelist()
def is_employee_checkin_allowed():
return cint(frappe.db.get_single_value("HR Settings", "allow_employee_checkin_from_mobile_app"))
23 changes: 22 additions & 1 deletion hrms/hr/doctype/appraisal/appraisal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from hrms.hr.doctype.appraisal_cycle.appraisal_cycle import validate_active_appraisal_cycle
from hrms.hr.utils import validate_active_employee
from hrms.payroll.utils import sanitize_expression


class Appraisal(Document):
Expand Down Expand Up @@ -179,7 +180,27 @@ def calculate_avg_feedback_score(self, update=False):
self.db_update()

def calculate_final_score(self):
final_score = (flt(self.total_score) + flt(self.avg_feedback_score) + flt(self.self_score)) / 3
final_score = 0
appraisal_cycle_doc = frappe.get_cached_doc("Appraisal Cycle", self.appraisal_cycle)

formula = appraisal_cycle_doc.final_score_formula
based_on_formula = appraisal_cycle_doc.calculate_final_score_based_on_formula

if based_on_formula:
employee_doc = frappe.get_cached_doc("Employee", self.employee)
data = {
"goal_score": flt(self.total_score),
"average_feedback_score": flt(self.avg_feedback_score),
"self_appraisal_score": flt(self.self_score),
}
data.update(appraisal_cycle_doc.as_dict())
data.update(employee_doc.as_dict())
data.update(self.as_dict())

sanitized_formula = sanitize_expression(formula)
final_score = frappe.safe_eval(sanitized_formula, data)
else:
final_score = (flt(self.total_score) + flt(self.avg_feedback_score) + flt(self.self_score)) / 3

self.final_score = flt(final_score, self.precision("final_score"))

Expand Down
22 changes: 21 additions & 1 deletion hrms/hr/doctype/appraisal/test_appraisal.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,26 @@ def test_manual_kra_rating(self):
def test_final_score(self):
cycle = create_appraisal_cycle(designation="Engineer", kra_evaluation_method="Manual Rating")
cycle.create_appraisals()
appraisal = self.setup_appraisal(cycle)

self.assertEqual(appraisal.final_score, 3.767)

def test_final_score_using_formula(self):
cycle = create_appraisal_cycle(designation="Engineer", kra_evaluation_method="Manual Rating")
cycle.update(
{
"calculate_final_score_based_on_formula": 1,
"final_score_formula": "(goal_score + self_appraisal_score + average_feedback_score)/3 if self_appraisal_score else (goal_score + self_appraisal_score)/2",
}
)
cycle.save()
cycle.create_appraisals()

appraisal = self.setup_appraisal(cycle)

self.assertEqual(appraisal.final_score, 3.767)

def setup_appraisal(self, cycle):
appraisal = frappe.db.exists("Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1})
appraisal = frappe.get_doc("Appraisal", appraisal)

Expand Down Expand Up @@ -97,7 +116,8 @@ def test_final_score(self):
feedback.submit()

appraisal.reload()
self.assertEqual(appraisal.final_score, 3.767)

return appraisal

def test_goal_score(self):
"""
Expand Down
81 changes: 66 additions & 15 deletions hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ frappe.ui.form.on("Appraisal Cycle", {

frm.trigger("show_custom_buttons");
frm.trigger("show_appraisal_summary");
frm.trigger("set_autocompletions_for_final_score_formula");
},

show_custom_buttons(frm) {
Expand All @@ -26,28 +27,42 @@ frappe.ui.form.on("Appraisal Cycle", {
frappe.set_route("Tree", "Goal");
});

let className = "";
let appraisals_created = frm.doc.__onload?.appraisals_created;

if (frm.doc.status !== "Completed") {
className = appraisals_created ? "btn-default" : "btn-primary";

frm.add_custom_button(__("Create Appraisals"), () => {
frm.trigger("create_appraisals");
}).addClass(className);
if (appraisals_created) {
frm.add_custom_button(__("Create Appraisals"), () => {
frm.trigger("create_appraisals");
});
} else {
frm.page.set_primary_action(__("Create Appraisals"), () => {
frm.trigger("create_appraisals");
});
}
}

className = appraisals_created ? "btn-primary" : "btn-default";

if (frm.doc.status === "Not Started") {
frm.add_custom_button(__("Start"), () => {
frm.set_value("status", "In Progress");
frm.save();
}).addClass(className);
if (appraisals_created) {
frm.page.set_primary_action(__("Start"), () => {
frm.set_value("status", "In Progress");
frm.save();
});
} else {
frm.add_custom_button(__("Start"), () => {
frm.set_value("status", "In Progress");
frm.save();
});
}
} else if (frm.doc.status === "In Progress") {
frm.add_custom_button(__("Mark as Completed"), () => {
frm.trigger("complete_cycle");
}).addClass(className);
if (appraisals_created) {
frm.page.set_primary_action(__("Mark as Completed"), () => {
frm.trigger("complete_cycle");
});
} else {
frm.add_custom_button(__("Mark as Completed"), () => {
frm.trigger("complete_cycle");
});
}
} else if (frm.doc.status === "Completed") {
frm.add_custom_button(__("Mark as In Progress"), () => {
frm.set_value("status", "In Progress");
Expand All @@ -56,6 +71,42 @@ frappe.ui.form.on("Appraisal Cycle", {
}
},

set_autocompletions_for_final_score_formula: async (frm) => {
const autocompletions = [
{
value: "goal_score",
score: 10,
meta: __("Total Goal Score"),
},
{
value: "average_feedback_score",
score: 10,
meta: __("Average Feedback Score"),
},
{
value: "self_appraisal_score",
score: 10,
meta: __("Self Appraisal Score"),
},
];

await Promise.all(
["Employee", "Appraisal Cycle", "Appraisal"].map((doctype) =>
frappe.model.with_doctype(doctype, () => {
autocompletions.push(
...frappe.get_meta(doctype).fields.map((f) => ({
value: f.fieldname,
score: 8,
meta: __("{0} Field", [doctype]),
})),
);
}),
),
);

frm.set_df_property("final_score_formula", "autocompletions", autocompletions);
},

get_employees(frm) {
frappe.call({
method: "set_employees",
Expand Down
Loading

0 comments on commit f3684c1

Please sign in to comment.