fix(formatting): run new pre commit on the codebase

This commit is contained in:
RitvikSardana
2024-08-25 09:53:08 +05:30
parent fb234a9d63
commit b7fb25fe7c
75 changed files with 6171 additions and 6071 deletions

37
.flake8 Normal file
View File

@@ -0,0 +1,37 @@
[flake8]
ignore =
E121,
E126,
E127,
E128,
E203,
E225,
E226,
E231,
E241,
E251,
E261,
E265,
E302,
E303,
E305,
E402,
E501,
E741,
W291,
W292,
W293,
W391,
W503,
W504,
F403,
B007,
B950,
W191,
E124, # closing bracket, irritating while writing QB code
E131, # continuation line unaligned for hanging indent
E123, # closing bracket does not match indentation of opening bracket's line
E101, # ensured by use of black
max-line-length = 200
exclude=.github/helper/semgrep_rules

73
.github/helpers/.flake8_strict vendored Normal file
View File

@@ -0,0 +1,73 @@
[flake8]
ignore =
B007,
B009,
B010,
B950,
E101,
E111,
E114,
E116,
E117,
E121,
E122,
E123,
E124,
E125,
E126,
E127,
E128,
E131,
E201,
E202,
E203,
E211,
E221,
E222,
E223,
E224,
E225,
E226,
E228,
E231,
E241,
E242,
E251,
E261,
E262,
E265,
E266,
E271,
E272,
E273,
E274,
E301,
E302,
E303,
E305,
E306,
E402,
E501,
E502,
E701,
E702,
E703,
E741,
F403,
W191,
W291,
W292,
W293,
W391,
W503,
W504,
E711,
E129,
F841,
E713,
E712,
B023
max-line-length = 200
exclude=.github/helper/semgrep_rules,test_*.py

View File

@@ -1,44 +1,41 @@
exclude: 'node_modules|.git'
exclude: "node_modules|.git"
default_stages: [commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
- id: trailing-whitespace
files: "helpdesk.*"
exclude: ".*json$|.*txt$|.*csv|.*md"
- id: check-yaml
- id: no-commit-to-branch
args: ['--branch', 'develop']
- id: check-merge-conflict
- id: check-ast
- id: black
types_or: [python, pyi]
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: [
'flake8-bugbear',
]
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"
- repo: https://github.com/adityahase/black
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
hooks:
- id: black
additional_dependencies: ['click==8.0.4']
additional_dependencies:
- flake8-bugbear
- flake8-comprehensions
- flake8-simplify
args: ['--config', '.github/helpers/.flake8_strict']
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
rev: v2.6.2
hooks:
- id: prettier
types_or: [javascript, vue, scss,ts]
additional_dependencies:
- prettier@2.6.2
- prettier-plugin-tailwindcss@0.1.11
types: [file]
types_or: [javascript, vue, html]
- repo: https://github.com/timothycrosley/isort
rev: 5.12.0
hooks:
- id: isort
args: [--profile, black]
ci:
autoupdate_schedule: weekly
skip: []
submodules: false
autoupdate_schedule: weekly
skip: []
submodules: false

View File

@@ -1,25 +1,25 @@
module.exports = {
parserPreset: "conventional-changelog-conventionalcommits",
rules: {
"subject-empty": [2, "never"],
"type-case": [2, "always", "lower-case"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test",
],
],
},
parserPreset: "conventional-changelog-conventionalcommits",
rules: {
"subject-empty": [2, "never"],
"type-case": [2, "always", "lower-case"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test",
],
],
},
};

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,212 +1,208 @@
<template>
<div>
<Dialog
:options="{ title: 'Add Agents' }"
:show="show"
@close="close()"
>
<template #body-content>
<div class="space-y-3">
<form
@submit.prevent="onSubmit"
class="flex flex-row space-x-2 items-center"
>
<Input
id="searchInput"
class="w-full"
type="text"
v-model="searchInput"
placeholder="Type emails"
@input="(val) => onSearchInputChange(val)"
/>
<Button
appearance="primary"
type="submit"
:disabled="!currentInputIsValidEmail"
@click="
() => {
addToInviteQueue(searchInput)
clearSearchInput()
}
"
>
Add
</Button>
</form>
<div
class="bg-gray-100 min-h-[100px] max-h-[300px] overflow-y-auto px-2 rounded border flex flex-col"
v-if="inviteQueue.length"
>
<ul
class="flex flex-wrap gap-2 py-2"
>
<li
class="flex items-center p-1 space-x-2 bg-white shadow rounded"
v-for="email in inviteQueue.slice().reverse()"
:key="email"
:title="email"
>
<span class="text-base ml-2">
{{ email }}
</span>
<button
class="grid w-4 h-4 text-gray-700 rounded hover:bg-gray-300 place-items-center"
@click="removeEmailFromQueue(email)"
>
<FeatherIcon class="w-3" name="x" />
</button>
</li>
</ul>
</div>
</div>
</template>
<template #actions v-if="inviteQueue.length">
<Button
:disabled="inviteQueue.length == 0"
appearance="primary"
@click="sentInvites()"
class="mr-2"
:loading="$resources.sentInvites.loading"
>Send Invites</Button
>
<Button appearance="secondary" class="mr-2" @click="close()">Cancel</Button>
<div class="grow">
<Button
@click="removeAllEmailFromQueue()"
v-if="inviteQueue.length > 1"
>
Clear All
</Button>
</div>
</template>
</Dialog>
</div>
<div>
<Dialog :options="{ title: 'Add Agents' }" :show="show" @close="close()">
<template #body-content>
<div class="space-y-3">
<form
@submit.prevent="onSubmit"
class="flex flex-row items-center space-x-2"
>
<Input
id="searchInput"
class="w-full"
type="text"
v-model="searchInput"
placeholder="Type emails"
@input="(val) => onSearchInputChange(val)"
/>
<Button
appearance="primary"
type="submit"
:disabled="!currentInputIsValidEmail"
@click="
() => {
addToInviteQueue(searchInput);
clearSearchInput();
}
"
>
Add
</Button>
</form>
<div
class="flex max-h-[300px] min-h-[100px] flex-col overflow-y-auto rounded border bg-gray-100 px-2"
v-if="inviteQueue.length"
>
<ul class="flex flex-wrap gap-2 py-2">
<li
class="flex items-center space-x-2 rounded bg-white p-1 shadow"
v-for="email in inviteQueue.slice().reverse()"
:key="email"
:title="email"
>
<span class="ml-2 text-base">
{{ email }}
</span>
<button
class="grid h-4 w-4 place-items-center rounded text-gray-700 hover:bg-gray-300"
@click="removeEmailFromQueue(email)"
>
<FeatherIcon class="w-3" name="x" />
</button>
</li>
</ul>
</div>
</div>
</template>
<template #actions v-if="inviteQueue.length">
<Button
:disabled="inviteQueue.length == 0"
appearance="primary"
@click="sentInvites()"
class="mr-2"
:loading="$resources.sentInvites.loading"
>Send Invites</Button
>
<Button appearance="secondary" class="mr-2" @click="close()"
>Cancel</Button
>
<div class="grow">
<Button
@click="removeAllEmailFromQueue()"
v-if="inviteQueue.length > 1"
>
Clear All
</Button>
</div>
</template>
</Dialog>
</div>
</template>
<script>
import { Dialog, Input, FeatherIcon } from "frappe-ui"
import { ref } from "@vue/reactivity"
import { Dialog, Input, FeatherIcon } from "frappe-ui";
import { ref } from "@vue/reactivity";
export default {
name: "AddNewAgentsDialog",
props: ["show"],
components: {
Dialog,
Input,
FeatherIcon,
},
setup() {
const searchInput = ref("")
const inviteQueue = ref([])
name: "AddNewAgentsDialog",
props: ["show"],
components: {
Dialog,
Input,
FeatherIcon,
},
setup() {
const searchInput = ref("");
const inviteQueue = ref([]);
const currentInputIsValidEmail = ref(false)
const currentInputIsValidEmail = ref(false);
return {
searchInput,
inviteQueue,
currentInputIsValidEmail,
}
},
methods: {
testEmailRegex(val) {
let emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
return emailRegex.test(val)
},
onSearchInputChange(val) {
val = val.replaceAll(" ", "")
return {
searchInput,
inviteQueue,
currentInputIsValidEmail,
};
},
methods: {
testEmailRegex(val) {
let emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
return emailRegex.test(val);
},
onSearchInputChange(val) {
val = val.replaceAll(" ", "");
if (val == "") {
document.getElementById("searchInput").value = ""
return
}
if (val == "") {
document.getElementById("searchInput").value = "";
return;
}
const valStr = val
const inputs = val.split(",")
const valStr = val;
const inputs = val.split(",");
let clearInputFlag = false
this.currentInputIsValidEmail = false
inputs.forEach((input) => {
if (this.testEmailRegex(input)) {
if (inputs.length > 1) {
this.addToInviteQueue(input)
clearInputFlag = true
} else {
if (valStr.includes(",")) {
this.addToInviteQueue(input)
clearInputFlag = true
} else {
this.currentInputIsValidEmail = true
}
}
}
})
if (clearInputFlag) {
this.clearSearchInput()
}
},
addToInviteQueue(email) {
this.inviteQueue = [...new Set([...this.inviteQueue, email])]
},
removeEmailFromQueue(email) {
this.inviteQueue = this.inviteQueue.filter((item) => item !== email)
},
removeAllEmailFromQueue() {
this.inviteQueue = []
},
clearSearchInput() {
this.currentInputIsValidEmail = false
this.searchInput = ""
let clearInputFlag = false;
this.currentInputIsValidEmail = false;
inputs.forEach((input) => {
if (this.testEmailRegex(input)) {
if (inputs.length > 1) {
this.addToInviteQueue(input);
clearInputFlag = true;
} else {
if (valStr.includes(",")) {
this.addToInviteQueue(input);
clearInputFlag = true;
} else {
this.currentInputIsValidEmail = true;
}
}
}
});
if (clearInputFlag) {
this.clearSearchInput();
}
},
addToInviteQueue(email) {
this.inviteQueue = [...new Set([...this.inviteQueue, email])];
},
removeEmailFromQueue(email) {
this.inviteQueue = this.inviteQueue.filter((item) => item !== email);
},
removeAllEmailFromQueue() {
this.inviteQueue = [];
},
clearSearchInput() {
this.currentInputIsValidEmail = false;
this.searchInput = "";
const input = document.getElementById("searchInput")
input.value = ""
input.focus()
},
close() {
this.searchInput = ""
this.inviteQueue = []
this.$emit("close")
},
sentInvites() {
this.$resources.sentInvites.submit({
emails: this.inviteQueue,
})
},
},
resources: {
sentInvites() {
return {
url: "helpdesk.api.agent.sent_invites",
onSuccess: (res) => {
this.currentInputIsValidEmail = false
this.searchInput = ""
this.inviteQueue = []
const input = document.getElementById("searchInput");
input.value = "";
input.focus();
},
close() {
this.searchInput = "";
this.inviteQueue = [];
this.$emit("close");
},
sentInvites() {
this.$resources.sentInvites.submit({
emails: this.inviteQueue,
});
},
},
resources: {
sentInvites() {
return {
url: "helpdesk.api.agent.sent_invites",
onSuccess: (res) => {
this.currentInputIsValidEmail = false;
this.searchInput = "";
this.inviteQueue = [];
this.$toast({
title: "Invites Sent Successfully!",
icon: "check",
iconClasses: "text-green-500"
})
this.$toast({
title: "Invites Sent Successfully!",
icon: "check",
iconClasses: "text-green-500",
});
this.close()
},
onError: (err) => {
if (err.exc_type == "PaywallReachedError") {
this.$toast({
title: "Paywall Reached!",
text: "You have reached the maximum number of agents you can add. Please upgrade your plan to add more agents.",
icon: "x",
iconClasses: "text-red-500",
})
} else {
this.$toast({
title: "Error Sending Invites!",
icon: "x",
iconClasses: "text-red-500",
})
}
},
}
},
},
}
this.close();
},
onError: (err) => {
if (err.exc_type == "PaywallReachedError") {
this.$toast({
title: "Paywall Reached!",
text: "You have reached the maximum number of agents you can add. Please upgrade your plan to add more agents.",
icon: "x",
iconClasses: "text-red-500",
});
} else {
this.$toast({
title: "Error Sending Invites!",
icon: "x",
iconClasses: "text-red-500",
});
}
},
};
},
},
};
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,126 +1,126 @@
<template>
<div
v-on-outside-click="
() => {
editing = false
}
"
>
<div>
<div
class="cursor-pointer w-24 text-right p-1.5 bg-gray-100 rounded"
@click="
() => {
editing = !editing
}
"
>
{{ convertSecondsToTimeStr(modelValue) }}
</div>
</div>
<div v-if="editing" class="relative">
<div class="absolute z-10 rounded shadow bg-white p-3 mt-2">
<div class="flex space-x-2">
<div class="space-y-1">
<Input class="w-16" v-model="hours" type="number" />
<div>hours</div>
</div>
<div class="space-y-1">
<Input class="w-16" v-model="minutes" type="number" />
<div>minutes</div>
</div>
</div>
</div>
</div>
</div>
<div
v-on-outside-click="
() => {
editing = false;
}
"
>
<div>
<div
class="w-24 cursor-pointer rounded bg-gray-100 p-1.5 text-right"
@click="
() => {
editing = !editing;
}
"
>
{{ convertSecondsToTimeStr(modelValue) }}
</div>
</div>
<div v-if="editing" class="relative">
<div class="absolute z-10 mt-2 rounded bg-white p-3 shadow">
<div class="flex space-x-2">
<div class="space-y-1">
<Input class="w-16" v-model="hours" type="number" />
<div>hours</div>
</div>
<div class="space-y-1">
<Input class="w-16" v-model="minutes" type="number" />
<div>minutes</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Input } from "frappe-ui"
import { ref } from "@vue/reactivity"
import { Input } from "frappe-ui";
import { ref } from "@vue/reactivity";
export default {
name: "TimeDurationInput",
props: ["modelValue"],
components: {
Input,
},
setup() {
const hours = ref(0)
const minutes = ref(0)
const timeStr = ref("")
name: "TimeDurationInput",
props: ["modelValue"],
components: {
Input,
},
setup() {
const hours = ref(0);
const minutes = ref(0);
const timeStr = ref("");
const editing = ref(false)
const editing = ref(false);
return {
hours,
minutes,
timeStr,
editing,
}
},
mounted() {
if (this.modelValue) {
this.timeStr = this.convertSecondsToTimeStr(this.modelValue)
return {
hours,
minutes,
timeStr,
editing,
};
},
mounted() {
if (this.modelValue) {
this.timeStr = this.convertSecondsToTimeStr(this.modelValue);
let time = this.convertSecondsToHoursAndMinutes(this.modelValue)
this.hours = time.hours ? time.hours : 0
this.minutes = time.minutes ? time.minutes : 0
}
},
watch: {
hours(newValue) {
this.updateModelValue()
},
minutes(newValue) {
this.updateModelValue()
},
},
methods: {
updateModelValue() {
this.$emit(
"update:modelValue",
this.convertTimeStToSeconds(
`${this.hours ? this.hours : "0"}h ${
this.minutes ? this.minutes : "0"
}m`
)
)
},
convertSecondsToTimeStr(seconds) {
let time = this.convertSecondsToHoursAndMinutes(seconds)
let time = this.convertSecondsToHoursAndMinutes(this.modelValue);
this.hours = time.hours ? time.hours : 0;
this.minutes = time.minutes ? time.minutes : 0;
}
},
watch: {
hours(newValue) {
this.updateModelValue();
},
minutes(newValue) {
this.updateModelValue();
},
},
methods: {
updateModelValue() {
this.$emit(
"update:modelValue",
this.convertTimeStToSeconds(
`${this.hours ? this.hours : "0"}h ${
this.minutes ? this.minutes : "0"
}m`
)
);
},
convertSecondsToTimeStr(seconds) {
let time = this.convertSecondsToHoursAndMinutes(seconds);
let h = time.hours
let m = time.minutes
let h = time.hours;
let m = time.minutes;
let hStr = h > 0 ? `${h}h` : ""
let mStr = m > 0 ? `${m}m` : ""
let hStr = h > 0 ? `${h}h` : "";
let mStr = m > 0 ? `${m}m` : "";
return `${hStr} ${mStr}`.trim() || "0h 0m"
},
convertTimeStToSeconds(strTime) {
let timeList = strTime.split(" ")
let seconds = 0
timeList.forEach((t) => {
if (t.includes("h")) {
seconds += t.replace("h", "") * 3600
} else if (t.includes("m")) {
seconds += t.replace("m", "") * 60
}
})
return seconds
},
convertSecondsToHoursAndMinutes(seconds) {
let hours = Math.floor(seconds / 3600)
let minutes = Math.floor((seconds % 3600) / 60)
return `${hStr} ${mStr}`.trim() || "0h 0m";
},
convertTimeStToSeconds(strTime) {
let timeList = strTime.split(" ");
let seconds = 0;
timeList.forEach((t) => {
if (t.includes("h")) {
seconds += t.replace("h", "") * 3600;
} else if (t.includes("m")) {
seconds += t.replace("m", "") * 60;
}
});
return seconds;
},
convertSecondsToHoursAndMinutes(seconds) {
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds % 3600) / 60);
return {
hours,
minutes,
}
},
},
}
return {
hours,
minutes,
};
},
},
};
</script>
<style></style>

View File

@@ -1,86 +1,79 @@
<template>
<div>
<div
class="bg-[#F7F7F7] group flex items-center text-base font-medium text-gray-500 py-[10px] px-[11px] rounded-[6px] select-none"
>
<div class="w-[37px] h-[14px]">
<Input
type="checkbox"
@click="manager.selectAll()"
:checked="manager.allItemsSelected"
role="button"
/>
</div>
<div class="flex flex-row items-center group w-full">
<div class="sm:w-10/12">Title</div>
<div class="sm:w-2/12">Author</div>
<div class="sm:w-2/12"></div>
</div>
</div>
<div
id="rows"
class="flex flex-col space-y-2 overflow-auto"
:style="{
height: viewportWidth > 768 ? 'calc(100vh - 120px)' : null,
}"
>
<div v-if="!manager.loading">
<div v-if="manager.list.length > 0">
<div
v-for="(canned_response, index) in manager.list"
:key="canned_response.name"
>
<CannedResponseListItem
:class="
index == 0 ? 'mt-[9px] mb-[2px]' : 'my-[2px]'
"
:canned_response="canned_response"
@toggle-select="manager.select(canned_response)"
:selected="manager.itemSelected(canned_response)"
/>
</div>
</div>
<div v-else>
<div class="grid place-content-center h-48 w-full">
<div>
<CustomIcons
name="empty-list"
class="h-12 w-12 mx-auto mb-2"
/>
<div class="text-gray-500 mb-2">
No canned response found
</div>
</div>
</div>
</div>
</div>
<div class="pb-2">
<Button v-if="manager.hasNextPage" @click="manager.nextPage()">
Load More
</Button>
</div>
</div>
</div>
<div>
<div
class="group flex select-none items-center rounded-[6px] bg-[#F7F7F7] py-[10px] px-[11px] text-base font-medium text-gray-500"
>
<div class="h-[14px] w-[37px]">
<Input
type="checkbox"
@click="manager.selectAll()"
:checked="manager.allItemsSelected"
role="button"
/>
</div>
<div class="group flex w-full flex-row items-center">
<div class="sm:w-10/12">Title</div>
<div class="sm:w-2/12">Author</div>
<div class="sm:w-2/12"></div>
</div>
</div>
<div
id="rows"
class="flex flex-col space-y-2 overflow-auto"
:style="{
height: viewportWidth > 768 ? 'calc(100vh - 120px)' : null,
}"
>
<div v-if="!manager.loading">
<div v-if="manager.list.length > 0">
<div
v-for="(canned_response, index) in manager.list"
:key="canned_response.name"
>
<CannedResponseListItem
:class="index == 0 ? 'mt-[9px] mb-[2px]' : 'my-[2px]'"
:canned_response="canned_response"
@toggle-select="manager.select(canned_response)"
:selected="manager.itemSelected(canned_response)"
/>
</div>
</div>
<div v-else>
<div class="grid h-48 w-full place-content-center">
<div>
<CustomIcons name="empty-list" class="mx-auto mb-2 h-12 w-12" />
<div class="mb-2 text-gray-500">No canned response found</div>
</div>
</div>
</div>
</div>
<div class="pb-2">
<Button v-if="manager.hasNextPage" @click="manager.nextPage()">
Load More
</Button>
</div>
</div>
</div>
</template>
<script>
import { inject } from "vue"
import { Input } from "frappe-ui"
import CannedResponseListItem from "@/components/desk/settings/canned_responses/CannedResponseListItem.vue"
import CustomIcons from "@/components/desk/global/CustomIcons.vue"
import { inject } from "vue";
import { Input } from "frappe-ui";
import CannedResponseListItem from "@/components/desk/settings/canned_responses/CannedResponseListItem.vue";
import CustomIcons from "@/components/desk/global/CustomIcons.vue";
export default {
name: "CannedResponseList",
props: ["manager"],
components: {
CannedResponseListItem,
CustomIcons,
Input,
},
setup() {
const viewportWidth = inject("viewportWidth")
return {
viewportWidth,
}
},
}
name: "CannedResponseList",
props: ["manager"],
components: {
CannedResponseListItem,
CustomIcons,
Input,
},
setup() {
const viewportWidth = inject("viewportWidth");
return {
viewportWidth,
};
},
};
</script>

View File

@@ -1,47 +1,47 @@
<template>
<div
class="block select-none rounded-[6px] py-[7px] px-[11px]"
:class="selected ? 'bg-blue-50 hover:bg-blue-100' : 'hover:bg-gray-50'"
>
<div
v-if="canned_response"
role="button"
class="flex items-center text-base"
>
<div class="w-[37px] h-[14px] flex items-center">
<Input
type="checkbox"
@click="$emit('toggleSelect')"
:checked="selected"
role="button"
/>
</div>
<RouterLink
:to="`/settings/canned_responses/${canned_response.name}`"
class="w-full group flex items-center"
>
<div class="sm:w-10/12 truncate pr-10">
{{ canned_response.title }}
</div>
<div class="sm:w-2/12 truncate pr-10">
{{ canned_response.owner }}
</div>
<div class="sm:w-2/12 truncate"></div>
</RouterLink>
</div>
</div>
<div
class="block select-none rounded-[6px] py-[7px] px-[11px]"
:class="selected ? 'bg-blue-50 hover:bg-blue-100' : 'hover:bg-gray-50'"
>
<div
v-if="canned_response"
role="button"
class="flex items-center text-base"
>
<div class="flex h-[14px] w-[37px] items-center">
<Input
type="checkbox"
@click="$emit('toggleSelect')"
:checked="selected"
role="button"
/>
</div>
<RouterLink
:to="`/settings/canned_responses/${canned_response.name}`"
class="group flex w-full items-center"
>
<div class="truncate pr-10 sm:w-10/12">
{{ canned_response.title }}
</div>
<div class="truncate pr-10 sm:w-2/12">
{{ canned_response.owner }}
</div>
<div class="truncate sm:w-2/12"></div>
</RouterLink>
</div>
</div>
</template>
<script>
import { Input, FeatherIcon } from "frappe-ui"
import Badge from "frappe-ui/src/components/Badge.vue"
import { Input, FeatherIcon } from "frappe-ui";
import Badge from "frappe-ui/src/components/Badge.vue";
export default {
name: "CannedResponseListItem",
props: ["canned_response", "selected"],
components: {
Input,
FeatherIcon,
Badge,
},
}
name: "CannedResponseListItem",
props: ["canned_response", "selected"],
components: {
Input,
FeatherIcon,
Badge,
},
};
</script>

View File

@@ -1,175 +1,164 @@
<template>
<div>
<div
v-if="!editable"
class="flex flex-col space-y-[16px] rounded-[8px] border shadow-sm p-[32px]"
>
<div
class="font-semibold text-[24px] prose prose-p:my-1 border-b pb-[16px] mb-[10px]"
>
{{ title }}
</div>
<div
class="overflow-y-auto"
style="
min-height: calc(100vh - 500px);
max-height: calc(100vh - 245px);
"
v-html="message"
></div>
</div>
<div v-else>
<div class="flex flex-col space-y-[16px] h-full">
<div>
<Input
label="Title"
type="text"
:value="title"
@input="
(val) => {
tempNewTitle = val
}
"
/>
</div>
<div class="flex flex-col">
<div
v-if="editMode"
class="mb-2 block text-sm leading-4 text-gray-700"
>
Message
</div>
<TextEditor
:class="editMode ? 'bg-gray-100' : ''"
ref="textEditor"
:editor-class="
!editable
? 'flex flex-col'
: editMode
? 'min-h-[20rem] overflow-y-auto max-h-[73vh] px-3 max-w-full'
: 'min-h-[20rem] overflow-y-auto max-h-[85vh]'
"
:content="message"
:starterkit-options="{
heading: { levels: [2, 3, 4, 5, 6] },
}"
@change="
(val) => {
tempNewMessage = val
}
"
:editable="editMode"
>
<template v-slot:top>
<div v-if="editMode">
<TextEditorFixedMenu
class="m-3 overflow-x-auto"
:buttons="textEditorMenuButtons"
/>
</div>
</template>
</TextEditor>
</div>
</div>
</div>
</div>
<div>
<div
v-if="!editable"
class="flex flex-col space-y-[16px] rounded-[8px] border p-[32px] shadow-sm"
>
<div
class="prose prose-p:my-1 mb-[10px] border-b pb-[16px] text-[24px] font-semibold"
>
{{ title }}
</div>
<div
class="overflow-y-auto"
style="min-height: calc(100vh - 500px); max-height: calc(100vh - 245px)"
v-html="message"
></div>
</div>
<div v-else>
<div class="flex h-full flex-col space-y-[16px]">
<div>
<Input
label="Title"
type="text"
:value="title"
@input="
(val) => {
tempNewTitle = val;
}
"
/>
</div>
<div class="flex flex-col">
<div
v-if="editMode"
class="mb-2 block text-sm leading-4 text-gray-700"
>
Message
</div>
<TextEditor
:class="editMode ? 'bg-gray-100' : ''"
ref="textEditor"
:editor-class="
!editable
? 'flex flex-col'
: editMode
? 'min-h-[20rem] overflow-y-auto max-h-[73vh] px-3 max-w-full'
: 'min-h-[20rem] overflow-y-auto max-h-[85vh]'
"
:content="message"
:starterkit-options="{
heading: { levels: [2, 3, 4, 5, 6] },
}"
@change="
(val) => {
tempNewMessage = val;
}
"
:editable="editMode"
>
<template v-slot:top>
<div v-if="editMode">
<TextEditorFixedMenu
class="m-3 overflow-x-auto"
:buttons="textEditorMenuButtons"
/>
</div>
</template>
</TextEditor>
</div>
</div>
</div>
</div>
</template>
<script>
import { TextEditor } from "frappe-ui"
import { TextEditorFixedMenu } from "frappe-ui/src/components/TextEditor"
import { ref, inject } from "vue"
import { TextEditor } from "frappe-ui";
import { TextEditorFixedMenu } from "frappe-ui/src/components/TextEditor";
import { ref, inject } from "vue";
export default {
name: "ResponseTitleAndMessage",
props: ["title", "message", "editable", "responseResource"],
components: {
TextEditor,
TextEditorFixedMenu,
},
mounted() {
this.saveResponseTitleAndMessage = this.save
},
watch: {
tempNewTitle(val) {
this.updateNewResponseInput({ field: "title", value: val })
},
tempNewMessage(val) {
this.updateNewResponseInput({ field: "message", value: val })
},
},
setup(props) {
const tempNewTitle = ref(props.title)
const tempNewMessage = ref(props.message)
const editMode = inject("editMode")
const updateNewResponseInput = inject("updateNewResponseInput")
const saveResponseTitleAndMessage = inject(
"saveResponseTitleAndMessage"
)
return {
tempNewTitle,
tempNewMessage,
editMode,
updateNewResponseInput,
saveResponseTitleAndMessage,
}
},
computed: {
textEditorMenuButtons() {
return [
"Paragraph",
[
"Heading 2",
"Heading 3",
"Heading 4",
"Heading 5",
"Heading 6",
],
"Separator",
"Bold",
"Italic",
"Separator",
"Bullet List",
"Numbered List",
"Separator",
"Align Left",
"Align Center",
"Align Right",
"Separator",
"Image",
"Video",
"Link",
"Blockquote",
"Code",
"Horizontal Rule",
[
"InsertTable",
"AddColumnBefore",
"AddColumnAfter",
"DeleteColumn",
"AddRowBefore",
"AddRowAfter",
"DeleteRow",
"MergeCells",
"SplitCell",
"ToggleHeaderColumn",
"ToggleHeaderRow",
"ToggleHeaderCell",
"DeleteTable",
],
"Separator",
"Undo",
"Redo",
]
},
},
methods: {
save() {
this.responseResource.setValue.submit({
title: this.tempNewTitle,
message: this.tempNewMessage,
})
this.editMode = false
},
},
}
name: "ResponseTitleAndMessage",
props: ["title", "message", "editable", "responseResource"],
components: {
TextEditor,
TextEditorFixedMenu,
},
mounted() {
this.saveResponseTitleAndMessage = this.save;
},
watch: {
tempNewTitle(val) {
this.updateNewResponseInput({ field: "title", value: val });
},
tempNewMessage(val) {
this.updateNewResponseInput({ field: "message", value: val });
},
},
setup(props) {
const tempNewTitle = ref(props.title);
const tempNewMessage = ref(props.message);
const editMode = inject("editMode");
const updateNewResponseInput = inject("updateNewResponseInput");
const saveResponseTitleAndMessage = inject("saveResponseTitleAndMessage");
return {
tempNewTitle,
tempNewMessage,
editMode,
updateNewResponseInput,
saveResponseTitleAndMessage,
};
},
computed: {
textEditorMenuButtons() {
return [
"Paragraph",
["Heading 2", "Heading 3", "Heading 4", "Heading 5", "Heading 6"],
"Separator",
"Bold",
"Italic",
"Separator",
"Bullet List",
"Numbered List",
"Separator",
"Align Left",
"Align Center",
"Align Right",
"Separator",
"Image",
"Video",
"Link",
"Blockquote",
"Code",
"Horizontal Rule",
[
"InsertTable",
"AddColumnBefore",
"AddColumnAfter",
"DeleteColumn",
"AddRowBefore",
"AddRowAfter",
"DeleteRow",
"MergeCells",
"SplitCell",
"ToggleHeaderColumn",
"ToggleHeaderRow",
"ToggleHeaderCell",
"DeleteTable",
],
"Separator",
"Undo",
"Redo",
];
},
},
methods: {
save() {
this.responseResource.setValue.submit({
title: this.tempNewTitle,
message: this.tempNewMessage,
});
this.editMode = false;
},
},
};
</script>

View File

@@ -1,265 +1,233 @@
<template>
<Combobox
v-model="selectedValue"
nullable
v-slot="{ open: isComboboxOpen }"
>
<Popover class="w-full">
<template #target="{ open: openPopover }">
<div class="w-full">
<ComboboxButton
class="w-full"
@click="
() => {
if (searchable) {
openPopover()
}
}
"
>
<slot
name="input-holder"
:selectedValue="selectedValue"
>
<div
class="h-[28px] flex items-center justify-between w-full py-1.5 pl-3 pr-2 rounded-md bg-gray-100"
:class="{
'rounded-b-none': isComboboxOpen,
}"
>
<slot
name="input"
:selectedValue="selectedValue"
:placeholder="searchable ? placeholder : ''"
>
<span
class="text-base line-clamp-1"
v-if="selectedValue"
>
{{ displayValue(selectedValue) }}
</span>
<span
class="text-base text-gray-500"
v-else
>
{{
searchable ? placeholder : "" || ""
}}
</span>
</slot>
<CustomIcons
v-if="searchable"
name="select"
class="w-[12px] h-[12px] stroke-gray-500"
/>
</div>
</slot>
</ComboboxButton>
</div>
</template>
<template #body>
<ComboboxOptions
class="max-w-[240px] px-1.5 pb-1.5 bg-white rounded-md mt-2 border shadow-md max-h-[11rem] overflow-y-auto"
static
:class="width ? `w-[${width}px]` : 'w-[250px]'"
v-show="isComboboxOpen"
>
<div
class="flex items-st items-stretch space-x-1.5 sticky top-0 pt-1.5 mb-1.5 bg-white"
>
<ComboboxInput
class="w-full placeholder-gray-500 form-input"
type="text"
@change="
(e) => {
query = e.target.value
if (resourceOptions) {
dsearch(query)
}
}
"
:value="query"
autocomplete="off"
placeholder="Search by keyword"
/>
<Button icon="x" @click="selectedValue = null" />
</div>
<ComboboxOption
as="template"
v-for="option in filteredOptions"
:key="option.value"
:value="option"
v-slot="{ active, selected }"
>
<li
class="px-2.5 py-1.5 cursor-pointer rounded-md text-base"
:class="[{ 'bg-gray-100': active }]"
>
<Tooltip
v-if="option.label.length > (20 * width) / 220"
placement="top"
:text="option.label"
>
<div
:class="[
'line-clamp-1',
`w-[${width - 25 + 'px'}]`,
]"
>
{{ option.label }}
</div>
</Tooltip>
<div v-else>{{ option.label }}</div>
</li>
</ComboboxOption>
<slot
v-if="filteredOptions?.length == 0"
name="no-result-found"
>
<li
class="px-2.5 py-1.5 rounded-md text-base text-gray-600"
>
No results found
</li>
</slot>
</ComboboxOptions>
</template>
</Popover>
</Combobox>
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
<Popover class="w-full">
<template #target="{ open: openPopover }">
<div class="w-full">
<ComboboxButton
class="w-full"
@click="
() => {
if (searchable) {
openPopover();
}
}
"
>
<slot name="input-holder" :selectedValue="selectedValue">
<div
class="flex h-[28px] w-full items-center justify-between rounded-md bg-gray-100 py-1.5 pl-3 pr-2"
:class="{
'rounded-b-none': isComboboxOpen,
}"
>
<slot
name="input"
:selectedValue="selectedValue"
:placeholder="searchable ? placeholder : ''"
>
<span class="line-clamp-1 text-base" v-if="selectedValue">
{{ displayValue(selectedValue) }}
</span>
<span class="text-base text-gray-500" v-else>
{{ searchable ? placeholder : "" || "" }}
</span>
</slot>
<CustomIcons
v-if="searchable"
name="select"
class="h-[12px] w-[12px] stroke-gray-500"
/>
</div>
</slot>
</ComboboxButton>
</div>
</template>
<template #body>
<ComboboxOptions
class="mt-2 max-h-[11rem] max-w-[240px] overflow-y-auto rounded-md border bg-white px-1.5 pb-1.5 shadow-md"
static
:class="width ? `w-[${width}px]` : 'w-[250px]'"
v-show="isComboboxOpen"
>
<div
class="items-st sticky top-0 mb-1.5 flex items-stretch space-x-1.5 bg-white pt-1.5"
>
<ComboboxInput
class="form-input w-full placeholder-gray-500"
type="text"
@change="
(e) => {
query = e.target.value;
if (resourceOptions) {
dsearch(query);
}
}
"
:value="query"
autocomplete="off"
placeholder="Search by keyword"
/>
<Button icon="x" @click="selectedValue = null" />
</div>
<ComboboxOption
as="template"
v-for="option in filteredOptions"
:key="option.value"
:value="option"
v-slot="{ active, selected }"
>
<li
class="cursor-pointer rounded-md px-2.5 py-1.5 text-base"
:class="[{ 'bg-gray-100': active }]"
>
<Tooltip
v-if="option.label.length > (20 * width) / 220"
placement="top"
:text="option.label"
>
<div :class="['line-clamp-1', `w-[${width - 25 + 'px'}]`]">
{{ option.label }}
</div>
</Tooltip>
<div v-else>{{ option.label }}</div>
</li>
</ComboboxOption>
<slot v-if="filteredOptions?.length == 0" name="no-result-found">
<li class="rounded-md px-2.5 py-1.5 text-base text-gray-600">
No results found
</li>
</slot>
</ComboboxOptions>
</template>
</Popover>
</Combobox>
</template>
<script>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
ComboboxButton,
} from "@headlessui/vue"
import { Popover, debounce } from "frappe-ui"
import CustomIcons from "@/components/desk/global/CustomIcons.vue"
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
ComboboxButton,
} from "@headlessui/vue";
import { Popover, debounce } from "frappe-ui";
import CustomIcons from "@/components/desk/global/CustomIcons.vue";
export default {
name: "Autocomplete",
props: {
modelValue: {
type: Object,
default: null,
},
options: {
type: Array,
default: () => [],
},
resourceOptions: {
type: Function,
default: () => {},
},
placeholder: {
type: String,
},
searchable: {
type: Boolean,
default: true,
},
width: {
type: Number,
default: 250,
},
show: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "change"],
components: {
Popover,
CustomIcons,
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
ComboboxButton,
},
data() {
return {
query: "",
}
},
mounted() {
this.dsearch("")
},
computed: {
valuePropPassed() {
return "value" in this.$attrs
},
selectedValue: {
get() {
return this.valuePropPassed
? this.$attrs.value
: this.modelValue
},
set(val) {
this.query = ""
this.$emit(
this.valuePropPassed ? "change" : "update:modelValue",
val
)
},
},
filteredOptions() {
if (!this.resourceOptions) {
if (!this.query) {
return this.options
}
return this.options.filter((option) => {
let searchTexts = [option.label, option.value]
return searchTexts.some((text) =>
(text || "")
.toLowerCase()
.includes(this.query.toLowerCase())
)
})
} else {
let data = this.$resources.search.data || []
if (data.length == 0) {
return []
}
return this.resourceOptions.responseMap(data)
}
},
},
methods: {
displayValue(option) {
if (typeof option === "string") {
return option
}
return option?.label
},
dsearch: debounce(function (query) {
this.search(query)
}, 300),
search(query) {
if (!this.searchable) return
this.$resources.search.fetch(this.resourceOptions.inputMap(query))
},
},
watch: {
resourceOptions(val) {
if (val) {
this.$nextTick(() => {
this.search("")
})
}
},
},
resources: {
search() {
if (!this.resourceOptions) {
return
}
return {
url: this.resourceOptions.url,
}
},
},
}
name: "Autocomplete",
props: {
modelValue: {
type: Object,
default: null,
},
options: {
type: Array,
default: () => [],
},
resourceOptions: {
type: Function,
default: () => {},
},
placeholder: {
type: String,
},
searchable: {
type: Boolean,
default: true,
},
width: {
type: Number,
default: 250,
},
show: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "change"],
components: {
Popover,
CustomIcons,
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
ComboboxButton,
},
data() {
return {
query: "",
};
},
mounted() {
this.dsearch("");
},
computed: {
valuePropPassed() {
return "value" in this.$attrs;
},
selectedValue: {
get() {
return this.valuePropPassed ? this.$attrs.value : this.modelValue;
},
set(val) {
this.query = "";
this.$emit(this.valuePropPassed ? "change" : "update:modelValue", val);
},
},
filteredOptions() {
if (!this.resourceOptions) {
if (!this.query) {
return this.options;
}
return this.options.filter((option) => {
let searchTexts = [option.label, option.value];
return searchTexts.some((text) =>
(text || "").toLowerCase().includes(this.query.toLowerCase())
);
});
} else {
let data = this.$resources.search.data || [];
if (data.length == 0) {
return [];
}
return this.resourceOptions.responseMap(data);
}
},
},
methods: {
displayValue(option) {
if (typeof option === "string") {
return option;
}
return option?.label;
},
dsearch: debounce(function (query) {
this.search(query);
}, 300),
search(query) {
if (!this.searchable) return;
this.$resources.search.fetch(this.resourceOptions.inputMap(query));
},
},
watch: {
resourceOptions(val) {
if (val) {
this.$nextTick(() => {
this.search("");
});
}
},
},
resources: {
search() {
if (!this.resourceOptions) {
return;
}
return {
url: this.resourceOptions.url,
};
},
},
};
</script>

View File

@@ -1,12 +1,12 @@
<template>
<div class="flex flex-col gap-2 text-gray-800">
<div class="text-base text-gray-900">
{{ text }}
</div>
<div class="text-base text-gray-900">
{{ subText }}
</div>
</div>
<div class="flex flex-col gap-2 text-gray-800">
<div class="text-base text-gray-900">
{{ text }}
</div>
<div class="text-base text-gray-900">
{{ subText }}
</div>
</div>
</template>
<script setup lang="ts">
@@ -14,11 +14,11 @@ import { onMounted } from "vue";
import { capture } from "@/telemetry";
const text =
"We're excited to have you join us and explore the world of efficient customer support. \
"We're excited to have you join us and explore the world of efficient customer support. \
We are here to transform the way you handle customer inquiries, streamline ticket \
management, and deliver outstanding service.";
const subText =
"Once again, welcome to Frappe Helpdesk! Let's hope for a long and smooth journey!";
"Once again, welcome to Frappe Helpdesk! Let's hope for a long and smooth journey!";
onMounted(() => capture("onboarding_started"));
</script>

View File

@@ -1,22 +1,22 @@
<template>
<div class="flex flex-col gap-4 text-gray-800">
{{ text }}
<div class="relative flex items-center justify-end">
<Input
type="text"
class="w-full"
:placeholder="placeholder"
@input="update"
/>
<IconCheck
v-if="isCheckVisible"
class="absolute mr-2 w-6 text-green-500"
/>
</div>
<div class="italic text-gray-800">
{{ subText }}
</div>
</div>
<div class="flex flex-col gap-4 text-gray-800">
{{ text }}
<div class="relative flex items-center justify-end">
<Input
type="text"
class="w-full"
:placeholder="placeholder"
@input="update"
/>
<IconCheck
v-if="isCheckVisible"
class="absolute mr-2 w-6 text-green-500"
/>
</div>
<div class="italic text-gray-800">
{{ subText }}
</div>
</div>
</template>
<script setup lang="ts">
@@ -26,31 +26,31 @@ import { capture } from "@/telemetry";
import IconCheck from "~icons/ph/check-bold";
const text =
"Now, let's set a name for your Helpdesk that reflects your organization's \
"Now, let's set a name for your Helpdesk that reflects your organization's \
identity and values. So, what would you like to name your Helpdesk?";
const subText =
"Choose a name that resonates with your brand and instills \
"Choose a name that resonates with your brand and instills \
trust in your customers";
const placeholder = "My Helpdesk";
const isCheckVisible = ref(false);
const r = createResource({
url: "frappe.client.set_value",
debounce: 1000,
onSuccess() {
isCheckVisible.value = true;
capture("onboarding_name_changed");
},
url: "frappe.client.set_value",
debounce: 1000,
onSuccess() {
isCheckVisible.value = true;
capture("onboarding_name_changed");
},
});
function update(value: string) {
isCheckVisible.value = false;
r.submit({
doctype: "HD Settings",
name: "HD Settings",
fieldname: "helpdesk_name",
value,
});
isCheckVisible.value = false;
r.submit({
doctype: "HD Settings",
name: "HD Settings",
fieldname: "helpdesk_name",
value,
});
}
onMounted(() => capture("onboarding_name_reached"));

View File

@@ -113,7 +113,7 @@ function finish() {
}
function redirect() {
router.replace({ path: '/' });
router.replace({ path: "/" });
}
onBeforeMount(() => {

View File

@@ -1,34 +1,34 @@
<template>
<div class="flex flex-col gap-4">
<div>
<div class="font-medium">Don't forget to star our GitHub repo</div>
<a class="text-sm text-gray-800" :href="github" target="_blank">
{{ github }}
</a>
</div>
<div>
<div class="font-medium">
If you find any bugs, report them at the issue tracker
</div>
<a class="text-sm text-gray-800" :href="issues" target="_blank">
{{ issues }}
</a>
</div>
<div>
<div class="font-medium">
For any queries or support, reach out to our support portal
</div>
<a class="text-sm text-gray-800" :href="support" target="_blank">
{{ support }}
</a>
</div>
<div>
<div class="font-medium">Or via our e-mail</div>
<a class="text-sm text-gray-800" :href="'mailto:' + email">
{{ email }}
</a>
</div>
</div>
<div class="flex flex-col gap-4">
<div>
<div class="font-medium">Don't forget to star our GitHub repo</div>
<a class="text-sm text-gray-800" :href="github" target="_blank">
{{ github }}
</a>
</div>
<div>
<div class="font-medium">
If you find any bugs, report them at the issue tracker
</div>
<a class="text-sm text-gray-800" :href="issues" target="_blank">
{{ issues }}
</a>
</div>
<div>
<div class="font-medium">
For any queries or support, reach out to our support portal
</div>
<a class="text-sm text-gray-800" :href="support" target="_blank">
{{ support }}
</a>
</div>
<div>
<div class="font-medium">Or via our e-mail</div>
<a class="text-sm text-gray-800" :href="'mailto:' + email">
{{ email }}
</a>
</div>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col gap-4">
<component :is="steps[step]" />
</div>
<div class="flex flex-col gap-4">
<component :is="steps[step]" />
</div>
</template>
<script setup lang="ts">

View File

@@ -1,12 +1,12 @@
<template>
<div class="flex flex-col items-center justify-center gap-4">
<div class="text-7xl">
{{ emoji }}
</div>
<div class="text-center text-base italic text-gray-900">
{{ message }}
</div>
</div>
<div class="flex flex-col items-center justify-center gap-4">
<div class="text-7xl">
{{ emoji }}
</div>
<div class="text-center text-base italic text-gray-900">
{{ message }}
</div>
</div>
</template>
<script setup lang="ts">
@@ -15,7 +15,7 @@ import { capture } from "@/telemetry";
const emoji = "✔️";
const message =
"Fantastic! Your email is now active. You are ready unleash true \
"Fantastic! Your email is now active. You are ready unleash true \
potential of Frappe Helpdesk!";
onMounted(() => capture("onboarding_email_finished"));

View File

@@ -2,16 +2,16 @@ import { ref } from "vue";
import { defineStore } from "pinia";
export const useOnboardingEmailStore = defineStore("onboarding", () => {
const step = ref(0);
const service = ref("");
const step = ref(0);
const service = ref("");
function next() {
step.value++;
}
function next() {
step.value++;
}
return {
next,
service,
step,
};
return {
next,
service,
step,
};
});

View File

@@ -3,26 +3,26 @@ import { defineStore } from "pinia";
import { createListResource } from "frappe-ui";
type Contact = {
name: string;
first_name: string;
last_name: string;
full_name: string;
email_id: string;
name: string;
first_name: string;
last_name: string;
full_name: string;
email_id: string;
};
export const useContactStore = defineStore("contact", () => {
const d__ = createListResource({
doctype: "Contact",
fields: ["*"],
auto: true,
pageLength: 99999,
});
const d__ = createListResource({
doctype: "Contact",
fields: ["*"],
auto: true,
pageLength: 99999,
});
const options: ComputedRef<Array<Contact>> = computed(
() => d__.list?.data || []
);
const options: ComputedRef<Array<Contact>> = computed(
() => d__.list?.data || []
);
return {
options,
};
return {
options,
};
});

View File

@@ -8,63 +8,67 @@ from frappe.core.doctype.user.user import test_password_strength
@frappe.whitelist(allow_guest=True)
def signup(email, first_name, last_name):
frappe.utils.validate_email_address(email, True)
frappe.utils.validate_email_address(email, True)
current_user = frappe.session.user
frappe.set_user("Administrator")
current_user = frappe.session.user
frappe.set_user("Administrator")
email = email.strip().lower()
email = email.strip().lower()
if not frappe.db.exists("User", email):
user = frappe.get_doc(
{
"doctype": "User",
"email": email,
"first_name": first_name,
"last_name": last_name,
"send_welcome_email": "0"
# "role_profile_name": "Helpdesk Contact" TODO: add this role profile and then un-comment this part
}
).insert()
if not frappe.db.exists("User", email):
user = frappe.get_doc(
{
"doctype": "User",
"email": email,
"first_name": first_name,
"last_name": last_name,
"send_welcome_email": "0"
# "role_profile_name": "Helpdesk Contact" TODO: add this role profile and then un-comment this part
}
).insert()
frappe.get_doc(
{"doctype": "HD Desk Account Request", "email": email, "user": user.name,}
).insert()
frappe.get_doc(
{
"doctype": "HD Desk Account Request",
"email": email,
"user": user.name,
}
).insert()
frappe.set_user(current_user)
else:
frappe.set_user(current_user)
frappe.throw("User already exists, please try loggin in using this email")
frappe.set_user(current_user)
else:
frappe.set_user(current_user)
frappe.throw("User already exists, please try loggin in using this email")
@frappe.whitelist(allow_guest=True)
def verify_and_create_account(request_key, email, password):
account_request = frappe.get_doc("HD Desk Account Request", {"email": email})
if account_request:
if account_request.request_key == request_key:
current_user = frappe.session.user
frappe.set_user("Administrator")
account_request = frappe.get_doc("HD Desk Account Request", {"email": email})
if account_request:
if account_request.request_key == request_key:
current_user = frappe.session.user
frappe.set_user("Administrator")
user = frappe.get_doc("User", account_request.user)
user.new_password = password
user.save()
user = frappe.get_doc("User", account_request.user)
user.new_password = password
user.save()
frappe.set_user(current_user)
else:
frappe.throw("Ivalid request key")
else:
frappe.throw(f"Account request for {email} not found, please signup first")
frappe.set_user(current_user)
else:
frappe.throw("Ivalid request key")
else:
frappe.throw(f"Account request for {email} not found, please signup first")
@frappe.whitelist(allow_guest=True)
def validate_password(password, first_name, last_name, email):
available = True
available = True
user_data = (first_name, last_name, email)
result = test_password_strength(password, "", None, user_data)
feedback = result.get("feedback", None)
user_data = (first_name, last_name, email)
result = test_password_strength(password, "", None, user_data)
feedback = result.get("feedback")
if feedback and not feedback.get("password_policy_validation_passed", False):
available = False
if feedback and not feedback.get("password_policy_validation_passed", False):
available = False
return available
return available

View File

@@ -1,8 +1,8 @@
import frappe
from frappe.utils.caching import redis_cache
from pypika import Criterion
from frappe.model import no_value_fields
from frappe.model.document import get_controller
from frappe.utils.caching import redis_cache
from pypika import Criterion
from helpdesk.utils import check_permissions
@@ -10,190 +10,204 @@ from helpdesk.utils import check_permissions
@frappe.whitelist()
@redis_cache()
def get_filterable_fields(doctype):
check_permissions(doctype, None)
QBDocField = frappe.qb.DocType("DocField")
QBCustomField = frappe.qb.DocType("Custom Field")
allowed_fieldtypes = [
"Check",
"Data",
"Float",
"Int",
"Link",
"Long Text",
"Select",
"Small Text",
"Text Editor",
"Text",
]
check_permissions(doctype, None)
QBDocField = frappe.qb.DocType("DocField")
QBCustomField = frappe.qb.DocType("Custom Field")
allowed_fieldtypes = [
"Check",
"Data",
"Float",
"Int",
"Link",
"Long Text",
"Select",
"Small Text",
"Text Editor",
"Text",
]
from_doc_fields = (
frappe.qb.from_(QBDocField)
.select(
QBDocField.fieldname,
QBDocField.fieldtype,
QBDocField.label,
QBDocField.name,
QBDocField.options,
)
.where(QBDocField.parent == doctype)
.where(QBDocField.hidden == False)
.where(Criterion.any([QBDocField.fieldtype == i for i in allowed_fieldtypes]))
.run(as_dict=True)
)
from_doc_fields = (
frappe.qb.from_(QBDocField)
.select(
QBDocField.fieldname,
QBDocField.fieldtype,
QBDocField.label,
QBDocField.name,
QBDocField.options,
)
.where(QBDocField.parent == doctype)
.where(QBDocField.hidden == False)
.where(Criterion.any([QBDocField.fieldtype == i for i in allowed_fieldtypes]))
.run(as_dict=True)
)
from_custom_fields = (
frappe.qb.from_(QBCustomField)
.select(
QBCustomField.fieldname,
QBCustomField.fieldtype,
QBCustomField.label,
QBCustomField.name,
QBCustomField.options,
)
.where(QBCustomField.dt == doctype)
.where(QBCustomField.hidden == False)
.where(
Criterion.any([QBCustomField.fieldtype == i for i in allowed_fieldtypes])
)
.run(as_dict=True)
)
from_custom_fields = (
frappe.qb.from_(QBCustomField)
.select(
QBCustomField.fieldname,
QBCustomField.fieldtype,
QBCustomField.label,
QBCustomField.name,
QBCustomField.options,
)
.where(QBCustomField.dt == doctype)
.where(QBCustomField.hidden == False)
.where(
Criterion.any([QBCustomField.fieldtype == i for i in allowed_fieldtypes])
)
.run(as_dict=True)
)
res = []
res.extend(from_doc_fields)
res.extend(from_custom_fields)
res.extend(
[
{
"fieldname": "_assign",
"fieldtype": "Link",
"label": "Assigned to",
"name": "_assign",
"options": "HD Agent",
},
{
"fieldname": "name",
"fieldtype": "Data",
"label": "ID",
"name": "name",
},
]
)
return res
res = []
res.extend(from_doc_fields)
res.extend(from_custom_fields)
res.extend(
[{
"fieldname": "_assign",
"fieldtype": "Link",
"label": "Assigned to",
"name": "_assign",
"options": "HD Agent",
},
{
"fieldname": "name",
"fieldtype": "Data",
"label": "ID",
"name": "name",
}]
)
return res
@frappe.whitelist()
def get_list_data(
doctype: str,
filters: dict={},
order_by: str="modified desc",
page_length=20,
columns=None,
rows=None,
doctype: str,
# flake8: noqa
filters: dict = {},
order_by: str = "modified desc",
page_length=20,
columns=None,
rows=None,
):
is_default = True
is_default = True
if columns or rows:
is_default = False
columns = frappe.parse_json(columns)
rows = frappe.parse_json(rows)
if columns or rows:
is_default = False
columns = frappe.parse_json(columns)
rows = frappe.parse_json(rows)
if not columns:
columns = [
{"label": "Name", "type": "Data", "key": "name", "width": "16rem"},
{"label": "Last Modified", "type": "Datetime", "key": "modified", "width": "8rem"},
]
if not columns:
columns = [
{"label": "Name", "type": "Data", "key": "name", "width": "16rem"},
{
"label": "Last Modified",
"type": "Datetime",
"key": "modified",
"width": "8rem",
},
]
if not rows:
rows = ["name"]
if not rows:
rows = ["name"]
# if frappe.db.exists("HD List View Settings", doctype):
# list_view_settings = frappe.get_doc("CRM List View Settings", doctype)
# columns = frappe.parse_json(list_view_settings.columns)
# rows = frappe.parse_json(list_view_settings.rows)
# is_default = False
# else:
list = get_controller(doctype)
# if frappe.db.exists("HD List View Settings", doctype):
# list_view_settings = frappe.get_doc("CRM List View Settings", doctype)
# columns = frappe.parse_json(list_view_settings.columns)
# rows = frappe.parse_json(list_view_settings.rows)
# is_default = False
# else:
list = get_controller(doctype)
if is_default:
if hasattr(list, "default_list_data"):
columns = list.default_list_data().get("columns")
rows = list.default_list_data().get("rows")
# flake8: noqa
if is_default:
if hasattr(list, "default_list_data"):
columns = list.default_list_data().get("columns")
rows = list.default_list_data().get("rows")
# check if rows has all keys from columns if not add them
for column in columns:
if column.get("key") not in rows:
rows.append(column.get("key"))
# check if rows has all keys from columns if not add them
for column in columns:
if column.get("key") not in rows:
rows.append(column.get("key"))
rows.append("name") if "name" not in rows else rows
data = frappe.get_list(
doctype,
fields=rows,
filters=filters,
order_by=order_by,
page_length=page_length,
) or []
rows.append("name") if "name" not in rows else rows
data = (
frappe.get_list(
doctype,
fields=rows,
filters=filters,
order_by=order_by,
page_length=page_length,
)
or []
)
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
"label": field.label,
"type": field.fieldtype,
"value": field.fieldname,
"options": field.options,
}
for field in fields
if field.label and field.fieldname
]
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
"label": field.label,
"type": field.fieldtype,
"value": field.fieldname,
"options": field.options,
}
for field in fields
if field.label and field.fieldname
]
std_fields = [
{"label": "Name", "type": "Data", "value": "name"},
{"label": "Created On", "type": "Datetime", "value": "creation"},
{"label": "Last Modified", "type": "Datetime", "value": "modified"},
{
"label": "Modified By",
"type": "Link",
"value": "modified_by",
"options": "User",
},
{"label": "Assigned To", "type": "Text", "value": "_assign"},
{"label": "Owner", "type": "Link", "value": "owner", "options": "User"},
]
std_fields = [
{"label": "Name", "type": "Data", "value": "name"},
{"label": "Created On", "type": "Datetime", "value": "creation"},
{"label": "Last Modified", "type": "Datetime", "value": "modified"},
{
"label": "Modified By",
"type": "Link",
"value": "modified_by",
"options": "User",
},
{"label": "Assigned To", "type": "Text", "value": "_assign"},
{"label": "Owner", "type": "Link", "value": "owner", "options": "User"},
]
for field in std_fields:
if field.get('value') not in rows:
rows.append(field.get('value'))
if field not in fields:
fields.append(field)
for field in std_fields:
if field.get("value") not in rows:
rows.append(field.get("value"))
if field not in fields:
fields.append(field)
return {
"data": data,
"columns": columns,
"rows": rows,
"fields": fields,
"total_count": len(frappe.get_list(doctype, filters=filters)),
"row_count": len(data),
}
return {
"data": data,
"columns": columns,
"rows": rows,
"fields": fields,
"total_count": len(frappe.get_list(doctype, filters=filters)),
"row_count": len(data),
}
@frappe.whitelist()
def sort_options(doctype: str):
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
"label": field.label,
"value": field.fieldname,
}
for field in fields
if field.label and field.fieldname
]
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
"label": field.label,
"value": field.fieldname,
}
for field in fields
if field.label and field.fieldname
]
standard_fields = [
{"label": "Name", "value": "name"},
{"label": "Created On", "value": "creation"},
{"label": "Last Modified", "value": "modified"},
{"label": "Modified By", "value": "modified_by"},
{"label": "Owner", "value": "owner"},
]
standard_fields = [
{"label": "Name", "value": "name"},
{"label": "Created On", "value": "creation"},
{"label": "Last Modified", "value": "modified"},
{"label": "Modified By", "value": "modified_by"},
{"label": "Owner", "value": "owner"},
]
for field in standard_fields:
fields.append(field)
for field in standard_fields:
fields.append(field)
return fields
return fields

View File

@@ -4,39 +4,39 @@ from pypika import Order
@frappe.whitelist()
def get_preset_filters(doctype):
"""
Args:
doctype (_type_): _description_
"""
Args:
doctype (_type_): _description_
Returns:
Dict {"user": [], "global": []}: Returns the preset filters for a given doctype
"""
Returns:
Dict {"user": [], "global": []}: Returns the preset filters for a given doctype
"""
options = {"user": [], "global": []}
options = {"user": [], "global": []}
fd_preset_filter = frappe.qb.DocType("HD Preset Filter")
preset_filters = list(
(
frappe.qb.from_(fd_preset_filter)
.select(fd_preset_filter.name)
.where(
(
(
(fd_preset_filter.type == "User")
& (fd_preset_filter.user == frappe.session.user)
)
| (fd_preset_filter.type.isin(["Global", "System"]))
)
& (fd_preset_filter.reference_doctype == doctype)
)
.orderby(fd_preset_filter.type)
.orderby(fd_preset_filter.modified, order=Order.desc)
).run()
)
fd_preset_filter = frappe.qb.DocType("HD Preset Filter")
preset_filters = list(
(
frappe.qb.from_(fd_preset_filter)
.select(fd_preset_filter.name)
.where(
(
(
(fd_preset_filter.type == "User")
& (fd_preset_filter.user == frappe.session.user)
)
| (fd_preset_filter.type.isin(["Global", "System"]))
)
& (fd_preset_filter.reference_doctype == doctype)
)
.orderby(fd_preset_filter.type)
.orderby(fd_preset_filter.modified, order=Order.desc)
).run()
)
for preset_filter in preset_filters:
preset_filter_doc = frappe.get_doc("HD Preset Filter", preset_filter[0])
options[
"global" if preset_filter_doc.type in ["Global", "System"] else "user"
].append(preset_filter_doc)
return options
for preset_filter in preset_filters:
preset_filter_doc = frappe.get_doc("HD Preset Filter", preset_filter[0])
options[
"global" if preset_filter_doc.type in ["Global", "System"] else "user"
].append(preset_filter_doc)
return options

View File

@@ -1,19 +1,20 @@
import frappe
@frappe.whitelist()
def get_users():
if frappe.session.user == "Guest":
frappe.throw(frappe._("Authentication failed"), exc=frappe.AuthenticationError)
if frappe.session.user == "Guest":
frappe.throw(frappe._("Authentication failed"), exc=frappe.AuthenticationError)
users = frappe.qb.get_query(
"User",
fields=["name", "email", "enabled", "user_image", "full_name", "user_type"],
order_by="full_name asc",
distinct=True,
).run(as_dict=1)
users = frappe.qb.get_query(
"User",
fields=["name", "email", "enabled", "user_image", "full_name", "user_type"],
order_by="full_name asc",
distinct=True,
).run(as_dict=1)
for user in users:
if frappe.session.user == user.name:
user.session_user = True
for user in users:
if frappe.session.user == user.name:
user.session_user = True
return users
return users

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Agent", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -6,189 +6,189 @@ from frappe.model.document import Document
class HDAgent(Document):
def before_save(self):
if self.name == self.user:
return
def before_save(self):
if self.name == self.user:
return
self.name = self.user
self.set_user_roles()
self.name = self.user
self.set_user_roles()
def set_user_roles(self):
user = frappe.get_doc("User", self.user)
def set_user_roles(self):
user = frappe.get_doc("User", self.user)
for role in ["Agent"]:
user.append("roles", {"role": role})
for role in ["Agent"]:
user.append("roles", {"role": role})
user.save()
user.save()
def on_update(self):
if self.has_value_changed("is_active"):
if not self.is_active:
self.remove_from_support_rotations()
else:
self.add_to_support_rotations()
def on_update(self):
if self.has_value_changed("is_active"):
if not self.is_active:
self.remove_from_support_rotations()
else:
self.add_to_support_rotations()
if self.has_value_changed("groups") and self.is_active:
previous = self.get_doc_before_save()
if previous:
for group in previous.groups:
if not next(
(g for g in self.groups if g.team == group.team),
None,
):
self.remove_from_support_rotations(group.team)
if self.has_value_changed("groups") and self.is_active:
previous = self.get_doc_before_save()
if previous:
for group in previous.groups:
if not next(
(g for g in self.groups if g.team == group.team),
None,
):
self.remove_from_support_rotations(group.team)
self.add_to_support_rotations()
self.add_to_support_rotations()
def on_trash(self):
self.remove_from_support_rotations()
def on_trash(self):
self.remove_from_support_rotations()
def add_to_support_rotations(self, group=None):
"""
Add the hd_agent to the support rotation for the given group or all groups
the hd_agent belongs to if hd_agent already added to the support roatation
for a group, skip
def add_to_support_rotations(self, group=None):
"""
Add the hd_agent to the support rotation for the given group or all groups
the hd_agent belongs to if hd_agent already added to the support roatation
for a group, skip
:param str group: Team name, defaults to None.
"""
rule_docs = []
if not group:
# Add the hd_agent to the base support rotation
:param str group: Team name, defaults to None.
"""
rule_docs = []
if not group:
# Add the hd_agent to the base support rotation
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Settings").get_base_support_rotation(),
)
)
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Settings").get_base_support_rotation(),
)
)
# Add the hd_agent to the support rotation for each group they belong to
if self.groups:
for group in self.groups:
try:
team_assignment_rule = frappe.get_doc(
"HD Team", group.team
).get_assignment_rule()
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
team_assignment_rule,
)
)
except frappe.DoesNotExistError:
frappe.throw(
frappe._(
"Assignment Rule for HD Team {0} does not exist"
).format(group.team)
)
else:
# check if the group is in self.groups
if next(
(group for group in self.groups if group["group_name"] == group), None
):
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Team", group).get_assignment_rule(),
)
)
else:
frappe.throw(
frappe._(
"Agent {0} does not belong to team {1}".format(
self.agent_name, group
)
)
)
# Add the hd_agent to the support rotation for each group they belong to
if self.groups:
for group in self.groups:
try:
team_assignment_rule = frappe.get_doc(
"HD Team", group.team
).get_assignment_rule()
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
team_assignment_rule,
)
)
except frappe.DoesNotExistError:
frappe.throw(
frappe._(
"Assignment Rule for HD Team {0} does not exist"
).format(group.team)
)
else:
# check if the group is in self.groups
if next(
(group for group in self.groups if group["group_name"] == group), None
):
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Team", group).get_assignment_rule(),
)
)
else:
frappe.throw(
frappe._(
"Agent {0} does not belong to team {1}".format(
self.agent_name, group
)
)
)
for rule_doc in rule_docs:
skip = False
if rule_doc:
if rule_doc.users and len(rule_doc.users) > 0:
for user in rule_doc.users:
if (
user.user == self.user
): # if the user is already in the rule, skip
skip = True
break
if skip:
continue
for rule_doc in rule_docs:
skip = False
if rule_doc:
if rule_doc.users and len(rule_doc.users) > 0:
for user in rule_doc.users:
if (
user.user == self.user
): # if the user is already in the rule, skip
skip = True
break
if skip:
continue
user_doc = frappe.get_doc(
{"doctype": "Assignment Rule User", "user": self.user}
)
rule_doc.append("users", user_doc)
rule_doc.disabled = False # enable the rule if it is disabled
rule_doc.save(ignore_permissions=True)
user_doc = frappe.get_doc(
{"doctype": "Assignment Rule User", "user": self.user}
)
rule_doc.append("users", user_doc)
rule_doc.disabled = False # enable the rule if it is disabled
rule_doc.save(ignore_permissions=True)
def remove_from_support_rotations(self, group=None):
rule_docs = []
def remove_from_support_rotations(self, group=None):
rule_docs = []
if group:
# remove the hd_agent from the support rotation for the given group
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Team", group).get_assignment_rule(),
)
)
if group:
# remove the hd_agent from the support rotation for the given group
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Team", group).get_assignment_rule(),
)
)
else:
# Remove the hd_agent from the base support rotation
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Settings").get_base_support_rotation(),
)
)
else:
# Remove the hd_agent from the base support rotation
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Settings").get_base_support_rotation(),
)
)
# Remove the hd_agent from the support rotation for each group they belong to
for group in self.groups:
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Team", group.team).get_assignment_rule(),
)
)
# Remove the hd_agent from the support rotation for each group they belong to
for group in self.groups:
rule_docs.append(
frappe.get_doc(
"Assignment Rule",
frappe.get_doc("HD Team", group.team).get_assignment_rule(),
)
)
for rule_doc in rule_docs:
if rule_doc.users and len(rule_doc.users) > 0:
for user in rule_doc.users:
if user.user == self.user:
if len(rule_doc.users) == 1:
rule_doc.disabled = (
True # disable the rule if there are no users left
)
rule_doc.remove(user)
rule_doc.save()
for rule_doc in rule_docs:
if rule_doc.users and len(rule_doc.users) > 0:
for user in rule_doc.users:
if user.user == self.user:
if len(rule_doc.users) == 1:
rule_doc.disabled = (
True # disable the rule if there are no users left
)
rule_doc.remove(user)
rule_doc.save()
def in_group(self, group):
"""
Check if agent is in the given group
"""
if self.groups:
return next((g for g in self.groups if g.team == group), False)
def in_group(self, group):
"""
Check if agent is in the given group
"""
if self.groups:
return next((g for g in self.groups if g.team == group), False)
return False
return False
@frappe.whitelist()
def create_hd_agent(first_name, last_name, email, signature, team):
if frappe.db.exists("User", email):
user = frappe.get_doc("User", email)
else:
user = frappe.get_doc(
{
"doctype": "User",
"first_name": first_name,
"last_name": last_name,
"email": email,
"email_signature": signature,
}
).insert()
if frappe.db.exists("User", email):
user = frappe.get_doc("User", email)
else:
user = frappe.get_doc(
{
"doctype": "User",
"first_name": first_name,
"last_name": last_name,
"email": email,
"email_signature": signature,
}
).insert()
user.send_welcome_mail_to_user()
user.send_welcome_mail_to_user()
return frappe.get_doc(
{"doctype": "HD Agent", "user": user.name, "group": team}
).insert()
return frappe.get_doc(
{"doctype": "HD Agent", "user": user.name, "group": team}
).insert()

View File

@@ -6,20 +6,20 @@ from helpdesk.utils import is_agent
@frappe.whitelist(allow_guest=True)
def get_article(name: str):
article = frappe.get_doc("HD Article", name).as_dict()
article = frappe.get_doc("HD Article", name).as_dict()
if not is_agent() and article["status"] != "Published":
frappe.throw(_("Access denied"), frappe.PermissionError)
if not is_agent() and article["status"] != "Published":
frappe.throw(_("Access denied"), frappe.PermissionError)
author = frappe.get_cached_doc("User", article["author"])
sub_category = frappe.get_cached_doc("HD Article Category", article["category"])
category = frappe.get_cached_doc(
"HD Article Category", sub_category.parent_category
)
author = frappe.get_cached_doc("User", article["author"])
sub_category = frappe.get_cached_doc("HD Article Category", article["category"])
category = frappe.get_cached_doc(
"HD Article Category", sub_category.parent_category
)
return {
**article,
"author": author,
"category": category,
"sub_category": sub_category,
}
return {
**article,
"author": author,
"category": category,
"sub_category": sub_category,
}

View File

@@ -8,70 +8,71 @@ from frappe.utils import cint
class HDArticle(Document):
@staticmethod
def get_list_filters(query):
QBArticle = DocType("HD Article")
QBCategory = DocType("HD Article Category")
@staticmethod
def get_list_filters(query):
QBArticle = DocType("HD Article")
QBCategory = DocType("HD Article Category")
query = (
query.where(QBArticle.status != "Archived")
.left_join(QBCategory)
.on(QBCategory.name == QBArticle.category)
.select(QBCategory.category_name)
.select(
QBArticle.title,
QBArticle.status,
QBArticle.author,
QBArticle.modified,
)
)
query = (
query.where(QBArticle.status != "Archived")
.left_join(QBCategory)
.on(QBCategory.name == QBArticle.category)
.select(QBCategory.category_name)
.select(
QBArticle.title,
QBArticle.status,
QBArticle.author,
QBArticle.modified,
)
)
return query
return query
def before_insert(self):
self.author = frappe.session.user
def before_insert(self):
self.author = frappe.session.user
def before_save(self):
# set published date of the hd_article
if self.status == "Published" and not self.published_on:
self.published_on = frappe.utils.now()
elif self.status == "Draft" and self.published_on:
self.published_on = None
def before_save(self):
# set published date of the hd_article
if self.status == "Published" and not self.published_on:
self.published_on = frappe.utils.now()
elif self.status == "Draft" and self.published_on:
self.published_on = None
if self.status == "Archived" and self.category != None:
self.category = None
if self.status == "Archived" and self.category != None:
self.category = None
# index is only set if its not set already, this allows defining index
# at the time of creation itself if not set the index is set to the
# last index + 1, i.e. the hd_article is added at the end
if self.status == "Published" and self.idx == -1:
self.idx = cint(
frappe.db.count(
"HD Article", {"category": self.category, "status": "Published"},
)
)
# index is only set if its not set already, this allows defining index
# at the time of creation itself if not set the index is set to the
# last index + 1, i.e. the hd_article is added at the end
if self.status == "Published" and self.idx == -1:
self.idx = cint(
frappe.db.count(
"HD Article",
{"category": self.category, "status": "Published"},
)
)
@property
def title_slug(self) -> str:
"""
Generate slug from article title.
Example: "Introduction to Frappe Helpdesk" -> "introduction-to-frappe-helpdesk"
@property
def title_slug(self) -> str:
"""
Generate slug from article title.
Example: "Introduction to Frappe Helpdesk" -> "introduction-to-frappe-helpdesk"
:return: Generated slug
"""
return self.title.lower().replace(" ", "-")
:return: Generated slug
"""
return self.title.lower().replace(" ", "-")
def get_breadcrumbs(self):
breadcrumbs = [{"name": self.name, "label": self.title}]
current_category = frappe.get_doc("Category", self.category)
breadcrumbs.append(
{"name": current_category.name, "label": current_category.category_name}
)
while current_category.parent_category:
current_category = frappe.get_doc(
"Category", current_category.parent_category
)
breadcrumbs.append(
{"name": current_category.name, "label": current_category.category_name}
)
return breadcrumbs[::-1]
def get_breadcrumbs(self):
breadcrumbs = [{"name": self.name, "label": self.title}]
current_category = frappe.get_doc("Category", self.category)
breadcrumbs.append(
{"name": current_category.name, "label": current_category.category_name}
)
while current_category.parent_category:
current_category = frappe.get_doc(
"Category", current_category.parent_category
)
breadcrumbs.append(
{"name": current_category.name, "label": current_category.category_name}
)
return breadcrumbs[::-1]

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Article Category", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,7 +2,8 @@
# For license information, please see license.txt
import frappe
from frappe import _
# from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
from pypika.functions import Count
@@ -10,47 +11,47 @@ from pypika.queries import Query
class HDArticleCategory(Document):
@staticmethod
def get_list_select(query: Query):
QBCategory = frappe.qb.DocType("HD Article Category")
QBArticle = frappe.qb.DocType("HD Article")
count_article = (
frappe.qb.from_(QBArticle)
.select(Count("*"))
.as_("count_article")
.where(QBArticle.category == QBCategory.name)
)
query = query.select(QBCategory.star).select(count_article)
return query
@staticmethod
def get_list_select(query: Query):
QBCategory = frappe.qb.DocType("HD Article Category")
QBArticle = frappe.qb.DocType("HD Article")
count_article = (
frappe.qb.from_(QBArticle)
.select(Count("*"))
.as_("count_article")
.where(QBArticle.category == QBCategory.name)
)
query = query.select(QBCategory.star).select(count_article)
return query
def before_save(self):
if self.idx == -1 and self.status == "Published":
# index is only set if its not set already, this allows defining
# index at the time of creation itself if not set the index is set
# to the last index + 1, i.e. the category is added at the end
self.idx = cint(
frappe.db.count(
"HD Article Category", {"parent_category": self.parent_category}
)
)
def before_save(self):
if self.idx == -1 and self.status == "Published":
# index is only set if its not set already, this allows defining
# index at the time of creation itself if not set the index is set
# to the last index + 1, i.e. the category is added at the end
self.idx = cint(
frappe.db.count(
"HD Article Category", {"parent_category": self.parent_category}
)
)
def archive(self):
self.idx = -1
self.status = "Archived"
self.save()
def archive(self):
self.idx = -1
self.status = "Archived"
self.save()
def unarchive(self):
self.status = "Published"
self.save()
def unarchive(self):
self.status = "Published"
self.save()
def get_breadcrumbs(self):
breadcrumbs = [{"name": self.name, "label": self.category_name}]
current_category = self
while current_category.parent_category:
current_category = frappe.get_doc(
"HD Article Category", current_category.parent_category
)
breadcrumbs.append(
{"name": current_category.name, "label": current_category.category_name}
)
return breadcrumbs[::-1]
def get_breadcrumbs(self):
breadcrumbs = [{"name": self.name, "label": self.category_name}]
current_category = self
while current_category.parent_category:
current_category = frappe.get_doc(
"HD Article Category", current_category.parent_category
)
breadcrumbs.append(
{"name": current_category.name, "label": current_category.category_name}
)
return breadcrumbs[::-1]

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Article Feedback", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Canned Response", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -6,38 +6,33 @@ from frappe.model.document import Document
class HDCannedResponse(Document):
def default_list_data():
def default_list_data():
columns = [
{
'label': 'Title',
'type': 'Data',
'key': 'title',
'width': '5rem',
},
{
'label': 'Message',
'type': 'Text Editor',
'key': 'message',
'width': '25rem',
},
{
'label': 'Owner',
'type': 'Link',
'key': 'owner',
'width': '5rem',
},
{
'label': 'Modified On',
'type': 'Datetime',
'key': 'modified',
'width': '5rem',
}
]
rows = [
"title",
"message",
"owner",
"modified"
]
return {'columns': columns, 'rows': rows}
columns = [
{
"label": "Title",
"type": "Data",
"key": "title",
"width": "5rem",
},
{
"label": "Message",
"type": "Text Editor",
"key": "message",
"width": "25rem",
},
{
"label": "Owner",
"type": "Link",
"key": "owner",
"width": "5rem",
},
{
"label": "Modified On",
"type": "Datetime",
"key": "modified",
"width": "5rem",
},
]
rows = ["title", "message", "owner", "modified"]
return {"columns": columns, "rows": rows}

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Customer", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Desk Account Request", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -1,39 +1,40 @@
import frappe
from frappe.model.document import Document
from frappe.utils import random_string, get_url
from frappe.utils import get_url, random_string
class HDDeskAccountRequest(Document):
def before_save(self):
if not self.request_key:
self.request_key = random_string(32)
def before_save(self):
if not self.request_key:
self.request_key = random_string(32)
self.ip_address = frappe.local.request_ip
self.ip_address = frappe.local.request_ip
def after_insert(self):
self.send_verification_email()
def after_insert(self):
self.send_verification_email()
def send_verification_email(self):
url = get_url(f"/helpdesk/verify/{self.request_key}")
subject = "Verify your account"
sender = None
def send_verification_email(self):
url = get_url(f"/helpdesk/verify/{self.request_key}")
subject = "Verify your account"
sender = None
if frappe.db.exists(
"Email Account", {"name": "Support", "enable_outgoing": True}
):
sender = frappe.get_doc("Email Account", "Support").email_id
if frappe.db.exists(
"Email Account", {"name": "Support", "enable_outgoing": True}
):
sender = frappe.get_doc("Email Account", "Support").email_id
try:
frappe.sendmail(
recipients=self.email,
sender=sender,
subject=subject,
template="email_verification",
args=dict(link=url),
now=True,
)
except Exception:
frappe.throw(
"Either setup up Support email account or there should be a default"
" outgoing email account"
)
try:
frappe.sendmail(
recipients=self.email,
sender=sender,
subject=subject,
template="email_verification",
# flake8: noqa
args=dict(link=url),
now=True,
)
except Exception:
frappe.throw(
"Either setup up Support email account or there should be a default"
" outgoing email account"
)

View File

@@ -9,47 +9,47 @@ from helpdesk.utils import capture_event, publish_event
class HDEscalationRule(Document):
def validate(self):
self.validate_criterion()
self.validate_duplicate()
def validate(self):
self.validate_criterion()
self.validate_duplicate()
def after_insert(self):
self.emit_after_insert()
def after_insert(self):
self.emit_after_insert()
def on_update(self):
self.emit_on_update()
def on_update(self):
self.emit_on_update()
def after_delete(self):
self.emit_after_delete()
def after_delete(self):
self.emit_after_delete()
def validate_criterion(self):
if not (self.priority or self.team or self.ticket_type):
frappe.throw(
_("At-least one of priority, team and ticket type is required")
)
def validate_criterion(self):
if not (self.priority or self.team or self.ticket_type):
frappe.throw(
_("At-least one of priority, team and ticket type is required")
)
def validate_duplicate(self):
is_duplicate = frappe.db.count(
"HD Escalation Rule",
filters={
"name": ["!=", self.name],
"priority": self.priority or "",
"team": self.team or "",
"ticket_type": self.ticket_type or "",
},
)
def validate_duplicate(self):
is_duplicate = frappe.db.count(
"HD Escalation Rule",
filters={
"name": ["!=", self.name],
"priority": self.priority or "",
"team": self.team or "",
"ticket_type": self.ticket_type or "",
},
)
if is_duplicate:
frappe.throw(_("Escalation rule already exists for this criteria"))
if is_duplicate:
frappe.throw(_("Escalation rule already exists for this criteria"))
def emit_after_insert(self):
capture_event("escalation_rule_created")
publish_event("helpdesk:new-escalation-rule", self)
def emit_after_insert(self):
capture_event("escalation_rule_created")
publish_event("helpdesk:new-escalation-rule", self)
def emit_on_update(self):
capture_event("escalation_rule_updated")
publish_event("helpdesk:update-escalation-rule", self)
def emit_on_update(self):
capture_event("escalation_rule_updated")
publish_event("helpdesk:update-escalation-rule", self)
def emit_after_delete(self):
capture_event("escalation_rule_deleted")
publish_event("helpdesk:delete-escalation-rule", self)
def emit_after_delete(self):
capture_event("escalation_rule_deleted")
publish_event("helpdesk:delete-escalation-rule", self)

View File

@@ -6,21 +6,22 @@ from frappe.model.document import Document
class HDFormScript(Document):
pass
pass
def get_form_script(dt, apply_to="Form"):
"""Returns the form script for the given doctype"""
FormScript = frappe.qb.DocType("HD Form Script")
query = (
frappe.qb.from_(FormScript)
.select("script")
.where(FormScript.dt == dt)
.where(FormScript.apply_to == apply_to)
.where(FormScript.enabled == 1)
)
"""Returns the form script for the given doctype"""
FormScript = frappe.qb.DocType("HD Form Script")
query = (
frappe.qb.from_(FormScript)
.select("script")
.where(FormScript.dt == dt)
.where(FormScript.apply_to == apply_to)
.where(FormScript.enabled == 1)
)
doc = query.run(as_dict=True)
if doc:
return [d.script for d in doc] if len(doc) > 1 else doc[0].script
else:
return None
doc = query.run(as_dict=True)
if doc:
return [d.script for d in doc] if len(doc) > 1 else doc[0].script
else:
return None

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Notification", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -5,60 +5,60 @@ from helpdesk.utils import refetch_resource
class HDNotification(Document):
def format_message(self):
user_from = self.get_from()
if self.notification_type == "Mention":
if self.reference_comment:
return f"{user_from} mentioned you in a comment"
return f"{user_from} mentioned you"
return ""
def format_message(self):
user_from = self.get_from()
if self.notification_type == "Mention":
if self.reference_comment:
return f"{user_from} mentioned you in a comment"
return f"{user_from} mentioned you"
return ""
def get_from(self):
return frappe.db.get_value(
"User", {"name": self.user_from}, fieldname="full_name"
)
def get_from(self):
return frappe.db.get_value(
"User", {"name": self.user_from}, fieldname="full_name"
)
def get_button_label(self):
if self.reference_comment:
return "See Comment"
return "Visit"
def get_button_label(self):
if self.reference_comment:
return "See Comment"
return "Visit"
def get_url(self):
res = "/helpdesk"
if self.reference_ticket:
res += "/tickets/" + str(self.reference_ticket)
if self.reference_comment:
res += "#" + self.reference_comment
return frappe.utils.get_url(res)
def get_url(self):
res = "/helpdesk"
if self.reference_ticket:
res += "/tickets/" + str(self.reference_ticket)
if self.reference_comment:
res += "#" + self.reference_comment
return frappe.utils.get_url(res)
def parse_html(self):
from bs4 import BeautifulSoup
def parse_html(self):
from bs4 import BeautifulSoup
soup = BeautifulSoup(self.message, "html.parser")
if soup.find("img"):
img = soup.find("img")
img["src"] = ("").join([frappe.utils.get_url(), img["src"]])
return str(soup)
return str(soup)
soup = BeautifulSoup(self.message, "html.parser")
if soup.find("img"):
img = soup.find("img")
img["src"] = ("").join([frappe.utils.get_url(), img["src"]])
return str(soup)
return str(soup)
def get_args(self):
if self.notification_type == "Mention":
return {
"title": self.format_message(),
"button_label": self.get_button_label(),
"callback_url": self.get_url(),
"comment": self.parse_html(),
}
def get_args(self):
if self.notification_type == "Mention":
return {
"title": self.format_message(),
"button_label": self.get_button_label(),
"callback_url": self.get_url(),
"comment": self.parse_html(),
}
def after_insert(self):
if self.notification_type == "Mention":
frappe.sendmail(
recipients=self.user_to,
subject="New notification",
message=self.format_message(),
template="notification",
args=self.get_args(),
)
def after_insert(self):
if self.notification_type == "Mention":
frappe.sendmail(
recipients=self.user_to,
subject="New notification",
message=self.format_message(),
template="notification",
args=self.get_args(),
)
def on_update(self):
refetch_resource("Notifications")
def on_update(self):
refetch_resource("Notifications")

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Organization", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Portal Signup Request", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Preset Filter", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,62 +2,56 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Service Holiday List", {
refresh: function (frm) {
if (frm.doc.holidays) {
frm.set_value("total_holidays", frm.doc.holidays.length)
}
},
from_date: function (frm) {
if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(
frm.doc.from_date,
12
)
frm.set_value(
"to_date",
frappe.datetime.add_days(a_year_from_start, -1)
)
}
},
})
refresh: function (frm) {
if (frm.doc.holidays) {
frm.set_value("total_holidays", frm.doc.holidays.length);
}
},
from_date: function (frm) {
if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1));
}
},
});
frappe.tour["HD Service Holiday List"] = [
{
fieldname: "holiday_list_name",
title: "HD Service Holiday List Name",
description: __("Enter a name for this HD Service Holiday List."),
},
{
fieldname: "from_date",
title: "From Date",
description: __(
"Based on your HR Policy, select your leave allocation period's start date"
),
},
{
fieldname: "to_date",
title: "To Date",
description: __(
"Based on your HR Policy, select your leave allocation period's end date"
),
},
{
fieldname: "weekly_off",
title: "Weekly Off",
description: __("Select your weekly off day"),
},
{
fieldname: "get_weekly_off_dates",
title: "Add Holidays",
description: __(
"Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"
),
},
{
fieldname: "holidays",
title: "Holidays",
description: __(
"Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually."
),
},
]
{
fieldname: "holiday_list_name",
title: "HD Service Holiday List Name",
description: __("Enter a name for this HD Service Holiday List."),
},
{
fieldname: "from_date",
title: "From Date",
description: __(
"Based on your HR Policy, select your leave allocation period's start date"
),
},
{
fieldname: "to_date",
title: "To Date",
description: __(
"Based on your HR Policy, select your leave allocation period's end date"
),
},
{
fieldname: "weekly_off",
title: "Weekly Off",
description: __("Select your weekly off day"),
},
{
fieldname: "get_weekly_off_dates",
title: "Add Holidays",
description: __(
"Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"
),
},
{
fieldname: "holidays",
title: "Holidays",
description: __(
"Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually."
),
},
];

View File

@@ -11,113 +11,122 @@ from frappe.utils import cint, formatdate, getdate, today
class OverlapError(frappe.ValidationError):
pass
pass
class HDServiceHolidayList(Document):
def validate(self):
self.validate_days()
self.total_holidays = len(self.holidays)
def validate(self):
self.validate_days()
self.total_holidays = len(self.holidays)
@frappe.whitelist()
def get_weekly_off_dates(self):
self.validate_values()
date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
last_idx = max([cint(d.idx) for d in self.get("holidays")] or [0,])
for i, d in enumerate(date_list):
ch = self.append("holidays", {})
ch.description = _(self.weekly_off)
ch.holiday_date = d
ch.weekly_off = 1
ch.idx = last_idx + i + 1
@frappe.whitelist()
def get_weekly_off_dates(self):
self.validate_values()
date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
last_idx = max(
[cint(d.idx) for d in self.get("holidays")]
or [
0,
]
)
for i, d in enumerate(date_list):
ch = self.append("holidays", {})
ch.description = _(self.weekly_off)
ch.holiday_date = d
ch.weekly_off = 1
ch.idx = last_idx + i + 1
def validate_values(self):
if not self.weekly_off:
throw(_("Please select weekly off day"))
def validate_values(self):
if not self.weekly_off:
throw(_("Please select weekly off day"))
def validate_days(self):
if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date"))
def validate_days(self):
if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date"))
for day in self.get("holidays"):
if not (
getdate(self.from_date) <= getdate(day.holiday_date) <= getdate(self.to_date)
):
frappe.throw(
_("The holiday on {0} is not between From Date and To Date").format(
formatdate(day.holiday_date)
)
)
for day in self.get("holidays"):
if not (
getdate(self.from_date)
<= getdate(day.holiday_date)
<= getdate(self.to_date)
):
frappe.throw(
_("The holiday on {0} is not between From Date and To Date").format(
formatdate(day.holiday_date)
)
)
def get_weekly_off_date_list(self, start_date, end_date):
start_date, end_date = getdate(start_date), getdate(end_date)
def get_weekly_off_date_list(self, start_date, end_date):
start_date, end_date = getdate(start_date), getdate(end_date)
import calendar
from datetime import timedelta
import calendar
from datetime import timedelta
from dateutil import relativedelta
from dateutil import relativedelta
date_list = []
existing_date_list = []
weekday = getattr(calendar, (self.weekly_off).upper())
reference_date = start_date + relativedelta.relativedelta(weekday=weekday)
date_list = []
existing_date_list = []
weekday = getattr(calendar, (self.weekly_off).upper())
reference_date = start_date + relativedelta.relativedelta(weekday=weekday)
existing_date_list = [
getdate(holiday.holiday_date) for holiday in self.get("holidays")
]
existing_date_list = [
getdate(holiday.holiday_date) for holiday in self.get("holidays")
]
while reference_date <= end_date:
if reference_date not in existing_date_list:
date_list.append(reference_date)
reference_date += timedelta(days=7)
while reference_date <= end_date:
if reference_date not in existing_date_list:
date_list.append(reference_date)
reference_date += timedelta(days=7)
return date_list
return date_list
@frappe.whitelist()
def clear_table(self):
self.set("holidays", [])
@frappe.whitelist()
def clear_table(self):
self.set("holidays", [])
@frappe.whitelist()
def get_events(start, end, filters=None):
"""Returns events for Gantt / Calendar view rendering.
"""Returns events for Gantt / Calendar view rendering.
:param start: Start date-time.
:param end: End date-time.
:param filters: Filters (JSON).
"""
if filters:
filters = json.loads(filters)
else:
filters = []
:param start: Start date-time.
:param end: End date-time.
:param filters: Filters (JSON).
"""
if filters:
filters = json.loads(filters)
else:
filters = []
if start:
filters.append(["HD Holiday", "holiday_date", ">", getdate(start)])
if end:
filters.append(["HD Holiday", "holiday_date", "<", getdate(end)])
if start:
filters.append(["HD Holiday", "holiday_date", ">", getdate(start)])
if end:
filters.append(["HD Holiday", "holiday_date", "<", getdate(end)])
return frappe.get_list(
"HD Service Holiday List",
fields=[
"name",
"`tabHD Holiday`.holiday_date",
"`tabHD Holiday`.description",
"`tabHD Service Holiday List`.color",
],
filters=filters,
update={"allDay": 1},
)
return frappe.get_list(
"HD Service Holiday List",
fields=[
"name",
"`tabHD Holiday`.holiday_date",
"`tabHD Holiday`.description",
"`tabHD Service Holiday List`.color",
],
filters=filters,
update={"allDay": 1},
)
def is_holiday(hd_service_holiday_list, date=None):
"""Returns true if the given date is a holiday in the given holiday list"""
if date is None:
date = today()
if hd_service_holiday_list:
return bool(
frappe.get_all(
"HD Service Holiday List", dict(name=hd_service_holiday_list, holiday_date=date)
)
)
else:
return False
"""Returns true if the given date is a holiday in the given holiday list"""
if date is None:
date = today()
if hd_service_holiday_list:
return bool(
frappe.get_all(
"HD Service Holiday List",
# flake8: noqa
dict(name=hd_service_holiday_list, holiday_date=date),
)
)
else:
return False

View File

@@ -2,22 +2,22 @@
// License: GNU General Public License v3. See license.txt
frappe.views.calendar["HD Service Holiday List"] = {
field_map: {
start: "holiday_date",
end: "holiday_date",
id: "name",
title: "description",
allDay: "allDay",
},
order_by: `from_date`,
get_events_method:
"helpdesk.helpdesk.doctype.hd_service_holiday_list.hd_service_holiday_list.get_events",
filters: [
{
fieldtype: "Link",
fieldname: "holiday_list",
options: "HD Service Holiday List",
label: __("HD Service Holiday List"),
},
],
}
field_map: {
start: "holiday_date",
end: "holiday_date",
id: "name",
title: "description",
allDay: "allDay",
},
order_by: `from_date`,
get_events_method:
"helpdesk.helpdesk.doctype.hd_service_holiday_list.hd_service_holiday_list.get_events",
filters: [
{
fieldtype: "Link",
fieldname: "holiday_list",
options: "HD Service Holiday List",
label: __("HD Service Holiday List"),
},
],
};

View File

@@ -2,130 +2,130 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Service Level Agreement", {
setup: function (frm) {
if (cint(frm.doc.apply_sla_for_resolution) === 1) {
frm.get_field("priorities").grid.editable_fields = [
{ fieldname: "default_priority", columns: 1 },
{ fieldname: "priority", columns: 2 },
{ fieldname: "response_time", columns: 2 },
{ fieldname: "resolution_time", columns: 2 },
]
} else {
frm.get_field("priorities").grid.editable_fields = [
{ fieldname: "default_priority", columns: 1 },
{ fieldname: "priority", columns: 2 },
{ fieldname: "response_time", columns: 3 },
]
}
},
setup: function (frm) {
if (cint(frm.doc.apply_sla_for_resolution) === 1) {
frm.get_field("priorities").grid.editable_fields = [
{ fieldname: "default_priority", columns: 1 },
{ fieldname: "priority", columns: 2 },
{ fieldname: "response_time", columns: 2 },
{ fieldname: "resolution_time", columns: 2 },
];
} else {
frm.get_field("priorities").grid.editable_fields = [
{ fieldname: "default_priority", columns: 1 },
{ fieldname: "priority", columns: 2 },
{ fieldname: "response_time", columns: 3 },
];
}
},
refresh: function (frm) {
frm.trigger("fetch_status_fields")
frm.trigger("toggle_resolution_fields")
},
refresh: function (frm) {
frm.trigger("fetch_status_fields");
frm.trigger("toggle_resolution_fields");
},
document_type: function (frm) {
frm.trigger("fetch_status_fields")
},
document_type: function (frm) {
frm.trigger("fetch_status_fields");
},
fetch_status_fields: function (frm) {
let allow_statuses = []
let exclude_statuses = []
fetch_status_fields: function (frm) {
let allow_statuses = [];
let exclude_statuses = [];
if (frm.doc.document_type) {
frappe.model.with_doctype(frm.doc.document_type, () => {
let statuses = frappe.meta.get_docfield(
frm.doc.document_type,
"status",
frm.doc.name
).options
statuses = statuses.split("\n")
if (frm.doc.document_type) {
frappe.model.with_doctype(frm.doc.document_type, () => {
let statuses = frappe.meta.get_docfield(
frm.doc.document_type,
"status",
frm.doc.name
).options;
statuses = statuses.split("\n");
exclude_statuses = ["Open", "Closed"]
allow_statuses = statuses.filter(
(status) => !exclude_statuses.includes(status)
)
exclude_statuses = ["Open", "Closed"];
allow_statuses = statuses.filter(
(status) => !exclude_statuses.includes(status)
);
frm.fields_dict.pause_sla_on.grid.update_docfield_property(
"status",
"options",
[""].concat(allow_statuses)
)
frm.fields_dict.pause_sla_on.grid.update_docfield_property(
"status",
"options",
[""].concat(allow_statuses)
);
exclude_statuses = ["Open"]
allow_statuses = statuses.filter(
(status) => !exclude_statuses.includes(status)
)
frm.fields_dict.sla_fulfilled_on.grid.update_docfield_property(
"status",
"options",
[""].concat(allow_statuses)
)
})
}
exclude_statuses = ["Open"];
allow_statuses = statuses.filter(
(status) => !exclude_statuses.includes(status)
);
frm.fields_dict.sla_fulfilled_on.grid.update_docfield_property(
"status",
"options",
[""].concat(allow_statuses)
);
});
}
frm.refresh_field("pause_sla_on")
},
frm.refresh_field("pause_sla_on");
},
apply_sla_for_resolution: function (frm) {
frm.trigger("toggle_resolution_fields")
},
apply_sla_for_resolution: function (frm) {
frm.trigger("toggle_resolution_fields");
},
toggle_resolution_fields: function (frm) {
if (cint(frm.doc.apply_sla_for_resolution) === 1) {
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"hidden",
0
)
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"reqd",
1
)
} else {
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"hidden",
1
)
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"reqd",
0
)
}
toggle_resolution_fields: function (frm) {
if (cint(frm.doc.apply_sla_for_resolution) === 1) {
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"hidden",
0
);
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"reqd",
1
);
} else {
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"hidden",
1
);
frm.fields_dict.priorities.grid.update_docfield_property(
"resolution_time",
"reqd",
0
);
}
frm.refresh_field("priorities")
},
frm.refresh_field("priorities");
},
onload: function (frm) {
frm.set_query("document_type", function () {
let invalid_doctypes = frappe.model.core_doctypes_list
invalid_doctypes.push(frm.doc.doctype, "Cost Center", "Company")
onload: function (frm) {
frm.set_query("document_type", function () {
let invalid_doctypes = frappe.model.core_doctypes_list;
invalid_doctypes.push(frm.doc.doctype, "Cost Center", "Company");
return {
filters: [
["DocType", "issingle", "=", 0],
["DocType", "istable", "=", 0],
["DocType", "name", "not in", invalid_doctypes],
[
"DocType",
"module",
"not in",
[
"Email",
"Core",
"Custom",
"Event Streaming",
"Social",
"Data Migration",
"Geo",
"Desk",
],
],
],
}
})
},
})
return {
filters: [
["DocType", "issingle", "=", 0],
["DocType", "istable", "=", 0],
["DocType", "name", "not in", invalid_doctypes],
[
"DocType",
"module",
"not in",
[
"Email",
"Core",
"Custom",
"Event Streaming",
"Social",
"Data Migration",
"Geo",
"Desk",
],
],
],
};
});
},
});

View File

@@ -1,363 +1,373 @@
from datetime import timedelta
from typing import Literal
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import (
add_to_date,
get_datetime,
get_weekdays,
getdate,
now_datetime,
time_diff_in_seconds,
to_timedelta,
cint
add_to_date,
cint,
get_datetime,
get_weekdays,
getdate,
now_datetime,
time_diff_in_seconds,
to_timedelta,
)
from datetime import timedelta
from helpdesk.utils import get_context
class HDServiceLevelAgreement(Document):
doctype_ticket = "HD Ticket"
doctype_ticket = "HD Ticket"
def validate(self):
self.validate_priorities() # To refactor
self.validate_support_and_resolution() # To refactor
self.validate_condition() # Looks okay but check again
def validate(self):
self.validate_priorities() # To refactor
self.validate_support_and_resolution() # To refactor
self.validate_condition() # Looks okay but check again
def validate_priorities(self):
priorities = []
def validate_priorities(self):
priorities = []
for priority in self.priorities:
# Check if response and resolution time is set for every priority
if not priority.response_time:
frappe.throw(
_("Set Response Time for Priority {0} in row {1}.").format(
priority.priority, priority.idx
)
)
for priority in self.priorities:
# Check if response and resolution time is set for every priority
if not priority.response_time:
frappe.throw(
_("Set Response Time for Priority {0} in row {1}.").format(
priority.priority, priority.idx
)
)
if self.apply_sla_for_resolution:
if not priority.resolution_time:
frappe.throw(
_("Set Response Time for Priority {0} in row {1}.").format(
priority.priority, priority.idx
)
)
if self.apply_sla_for_resolution:
if not priority.resolution_time:
frappe.throw(
_("Set Response Time for Priority {0} in row {1}.").format(
priority.priority, priority.idx
)
)
response = cint(priority.response_time)
resolution = cint(priority.resolution_time)
if response > resolution:
frappe.throw(
_(
"Response Time for {0} priority in row {1} can't be greater"
" than Resolution Time."
).format(priority.priority, priority.idx)
)
response = cint(priority.response_time)
resolution = cint(priority.resolution_time)
if response > resolution:
frappe.throw(
_(
"Response Time for {0} priority in row {1} can't be greater"
" than Resolution Time."
).format(priority.priority, priority.idx)
)
priorities.append(priority.priority)
priorities.append(priority.priority)
# Check if repeated priority
if not len(set(priorities)) == len(priorities):
repeated_priority = get_repeated(priorities)
frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority))
# Check if repeated priority
# flake8: noqa
if not len(set(priorities)) == len(priorities):
repeated_priority = get_repeated(priorities)
frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority))
# set default priority from priorities
try:
self.default_priority = next(
d.priority for d in self.priorities if d.default_priority
)
except Exception:
frappe.throw(_("Select a Default Priority."))
# set default priority from priorities
try:
self.default_priority = next(
d.priority for d in self.priorities if d.default_priority
)
except Exception:
frappe.throw(_("Select a Default Priority."))
def validate_support_and_resolution(self):
week = get_weekdays()
support_days = []
def validate_support_and_resolution(self):
week = get_weekdays()
support_days = []
for support_and_resolution in self.support_and_resolution:
support_days.append(support_and_resolution.workday)
support_and_resolution.idx = week.index(support_and_resolution.workday) + 1
for support_and_resolution in self.support_and_resolution:
support_days.append(support_and_resolution.workday)
support_and_resolution.idx = week.index(support_and_resolution.workday) + 1
if to_timedelta(support_and_resolution.start_time) >= to_timedelta(
support_and_resolution.end_time
):
frappe.throw(
_(
"Start Time can't be greater than or equal to End Time for {0}."
).format(support_and_resolution.workday)
)
if to_timedelta(support_and_resolution.start_time) >= to_timedelta(
support_and_resolution.end_time
):
frappe.throw(
_(
"Start Time can't be greater than or equal to End Time for {0}."
).format(support_and_resolution.workday)
)
# Check for repeated workday
if not len(set(support_days)) == len(support_days):
repeated_days = get_repeated(support_days)
frappe.throw(_("Workday {0} has been repeated.").format(repeated_days))
# Check for repeated workday
# flake8: noqa
if not len(set(support_days)) == len(support_days):
repeated_days = get_repeated(support_days)
frappe.throw(_("Workday {0} has been repeated.").format(repeated_days))
def validate_condition(self):
if not self.condition:
return
try:
temp_doc = frappe.new_doc(self.doctype_ticket)
frappe.safe_eval(self.condition, None, get_context(temp_doc))
except Exception as e:
frappe.throw(
_("The Condition '{0}' is invalid: {1}").format(self.condition, str(e))
)
def validate_condition(self):
if not self.condition:
return
try:
temp_doc = frappe.new_doc(self.doctype_ticket)
frappe.safe_eval(self.condition, None, get_context(temp_doc))
except Exception as e:
frappe.throw(
_("The Condition '{0}' is invalid: {1}").format(self.condition, str(e))
)
# What?
def get_hd_service_level_agreement_priority(self, priority):
priority = frappe.get_doc(
"HD Service Level Priority", {"priority": priority, "parent": self.name}
)
# What?
def get_hd_service_level_agreement_priority(self, priority):
priority = frappe.get_doc(
"HD Service Level Priority", {"priority": priority, "parent": self.name}
)
return frappe._dict(
{
"priority": priority.priority,
"response_time": priority.response_time,
"resolution_time": priority.resolution_time,
}
)
return frappe._dict(
{
"priority": priority.priority,
"response_time": priority.response_time,
"resolution_time": priority.resolution_time,
}
)
def apply(self, doc: Document):
self.handle_new(doc)
self.handle_status(doc)
self.handle_targets(doc)
self.handle_agreement_status(doc)
def apply(self, doc: Document):
self.handle_new(doc)
self.handle_status(doc)
self.handle_targets(doc)
self.handle_agreement_status(doc)
def handle_new(self, doc: Document):
if not doc.is_new():
return
creation = doc.service_level_agreement_creation or now_datetime()
doc.service_level_agreement_creation = creation
doc.priority = doc.priority or self.default_priority
def handle_new(self, doc: Document):
if not doc.is_new():
return
creation = doc.service_level_agreement_creation or now_datetime()
doc.service_level_agreement_creation = creation
doc.priority = doc.priority or self.default_priority
def handle_status(self, doc: Document):
if doc.is_new() or not doc.has_value_changed("status"):
return
self.set_first_response_time(doc)
self.set_resolution_date(doc)
self.set_hold_time(doc)
def handle_status(self, doc: Document):
if doc.is_new() or not doc.has_value_changed("status"):
return
self.set_first_response_time(doc)
self.set_resolution_date(doc)
self.set_hold_time(doc)
def set_first_response_time(self, doc: Document):
start_at = doc.service_level_agreement_creation
end_at = doc.first_responded_on
if not start_at or not end_at:
return
doc.first_response_time = self.calc_elapsed_time(start_at, end_at)
def set_first_response_time(self, doc: Document):
start_at = doc.service_level_agreement_creation
end_at = doc.first_responded_on
if not start_at or not end_at:
return
doc.first_response_time = self.calc_elapsed_time(start_at, end_at)
def set_resolution_date(self, doc: Document):
fullfill_on = [row.status for row in self.sla_fulfilled_on]
next_state = doc.get("status")
is_fulfilled = next_state in fullfill_on
if not is_fulfilled:
doc.resolution_date = None
doc.resolution_time = None
return
doc.resolution_date = now_datetime()
start_at = doc.service_level_agreement_creation
end_at = doc.resolution_date
time_took = self.calc_elapsed_time(start_at, end_at)
time_hold = doc.total_hold_time or 0
time_took_effective = time_took - time_hold
doc.resolution_time = time_took_effective
def set_resolution_date(self, doc: Document):
fullfill_on = [row.status for row in self.sla_fulfilled_on]
next_state = doc.get("status")
is_fulfilled = next_state in fullfill_on
if not is_fulfilled:
doc.resolution_date = None
doc.resolution_time = None
return
doc.resolution_date = now_datetime()
start_at = doc.service_level_agreement_creation
end_at = doc.resolution_date
time_took = self.calc_elapsed_time(start_at, end_at)
time_hold = doc.total_hold_time or 0
time_took_effective = time_took - time_hold
doc.resolution_time = time_took_effective
def set_hold_time(self, doc: Document):
pause_on = [row.status for row in self.pause_sla_on]
doc_old = doc.get_doc_before_save()
prev_state = doc_old.get("status")
next_state = doc.get("status")
was_paused = prev_state in pause_on
is_paused = next_state in pause_on
paused_since = doc.on_hold_since or doc_old.get("resolution_date")
if is_paused and not was_paused:
doc.response_by = doc.resolution_by if doc.first_responded_on else None
doc.resolution_date = None
doc.resolution_by = None
doc.resolution_time = None
doc.on_hold_since = now_datetime()
else:
doc.on_hold_since = None
if is_paused or not paused_since:
return
doc.on_hold_since = None
curr_val = time_diff_in_seconds(now_datetime(), paused_since)
doc.total_hold_time = (doc.total_hold_time or 0) + curr_val
def set_hold_time(self, doc: Document):
pause_on = [row.status for row in self.pause_sla_on]
doc_old = doc.get_doc_before_save()
prev_state = doc_old.get("status")
next_state = doc.get("status")
was_paused = prev_state in pause_on
is_paused = next_state in pause_on
paused_since = doc.on_hold_since or doc_old.get("resolution_date")
if is_paused and not was_paused:
doc.response_by = doc.resolution_by if doc.first_responded_on else None
doc.resolution_date = None
doc.resolution_by = None
doc.resolution_time = None
doc.on_hold_since = now_datetime()
else:
doc.on_hold_since = None
if is_paused or not paused_since:
return
doc.on_hold_since = None
curr_val = time_diff_in_seconds(now_datetime(), paused_since)
doc.total_hold_time = (doc.total_hold_time or 0) + curr_val
def handle_targets(self, doc: Document):
self.set_response_by(doc)
self.set_resolution_by(doc)
def handle_targets(self, doc: Document):
self.set_response_by(doc)
self.set_resolution_by(doc)
def set_response_by(self, doc: Document):
start = doc.service_level_agreement_creation
doc.response_by = self.calc_time(start, doc.priority, "response_time")
def set_response_by(self, doc: Document):
start = doc.service_level_agreement_creation
doc.response_by = self.calc_time(start, doc.priority, "response_time")
def set_resolution_by(self, doc: Document):
total_hold_time = doc.total_hold_time or 0
start = add_to_date(
doc.service_level_agreement_creation,
seconds=total_hold_time,
as_datetime=True,
)
doc.resolution_by = self.calc_time(start, doc.priority, "resolution_time")
def set_resolution_by(self, doc: Document):
total_hold_time = doc.total_hold_time or 0
start = add_to_date(
doc.service_level_agreement_creation,
seconds=total_hold_time,
as_datetime=True,
)
doc.resolution_by = self.calc_time(start, doc.priority, "resolution_time")
def reset_resolution_metrics(self, doc: Document):
pause_on = [row.status for row in self.pause_sla_on]
fullfill_on = [row.status for row in self.sla_fulfilled_on]
prev_state = doc.get_doc_before_save().get("status")
next_state = doc.get("status")
was_paused = prev_state in pause_on
was_fulfilled = prev_state in fullfill_on
is_paused = next_state in pause_on
is_fulfilled = next_state in fullfill_on
is_open = not is_paused and not is_fulfilled
if is_open and (was_paused or was_fulfilled):
return
doc.response_date = None
doc.resolution_date = None
doc.user_resolution_time = None
def reset_resolution_metrics(self, doc: Document):
pause_on = [row.status for row in self.pause_sla_on]
fullfill_on = [row.status for row in self.sla_fulfilled_on]
prev_state = doc.get_doc_before_save().get("status")
next_state = doc.get("status")
was_paused = prev_state in pause_on
was_fulfilled = prev_state in fullfill_on
is_paused = next_state in pause_on
is_fulfilled = next_state in fullfill_on
is_open = not is_paused and not is_fulfilled
if is_open and (was_paused or was_fulfilled):
return
doc.response_date = None
doc.resolution_date = None
doc.user_resolution_time = None
def handle_agreement_status(self, doc: Document):
is_failed = self.is_first_response_failed(doc) or self.is_resolution_failed(doc)
options = {
"Fulfilled": True,
"Resolution Due": self.apply_sla_for_resolution and not doc.resolution_date,
"First Response Due": not doc.first_responded_on,
"Failed": is_failed,
"Paused": doc.on_hold_since,
}
for status in options:
if options[status]:
doc.agreement_status = status
def handle_agreement_status(self, doc: Document):
is_failed = self.is_first_response_failed(doc) or self.is_resolution_failed(doc)
options = {
"Fulfilled": True,
"Resolution Due": self.apply_sla_for_resolution and not doc.resolution_date,
"First Response Due": not doc.first_responded_on,
"Failed": is_failed,
"Paused": doc.on_hold_since,
}
for status in options:
if options[status]:
doc.agreement_status = status
def is_first_response_failed(self, doc: Document):
if not doc.first_responded_on:
return get_datetime(doc.response_by) < now_datetime()
return get_datetime(doc.response_by) < get_datetime(doc.first_responded_on)
def is_first_response_failed(self, doc: Document):
if not doc.first_responded_on:
return get_datetime(doc.response_by) < now_datetime()
return get_datetime(doc.response_by) < get_datetime(doc.first_responded_on)
def is_resolution_failed(self, doc: Document):
if not self.apply_sla_for_resolution or not doc.resolution_by:
return
if not doc.resolution_date:
return get_datetime(doc.resolution_by) < now_datetime()
return get_datetime(doc.resolution_by) < get_datetime(doc.resolution_date)
def is_resolution_failed(self, doc: Document):
if not self.apply_sla_for_resolution or not doc.resolution_by:
return
if not doc.resolution_date:
return get_datetime(doc.resolution_by) < now_datetime()
return get_datetime(doc.resolution_by) < get_datetime(doc.resolution_date)
def calc_time(
self,
start_at: str,
priority: str,
target: Literal["response_time", "resolution_time"],
):
res = get_datetime(start_at)
priority = self.get_priorities()[priority]
time_needed = priority.get(target, 0)
holidays = self.get_holidays()
weekdays = get_weekdays()
workdays = self.get_workdays()
while time_needed:
today = res
today_day = getdate(today)
today_weekday = weekdays[today.weekday()]
is_workday = today_weekday in workdays
is_holiday = today_day in holidays
if is_holiday or not is_workday:
res = add_to_date(res, days=1, as_datetime=True)
continue
today_workday = workdays[today_weekday]
now_in_seconds = time_diff_in_seconds(today, today_day)
start_time = max(today_workday.start_time.total_seconds(), now_in_seconds)
till_start_time = max(start_time - now_in_seconds, 0)
end_time = max(today_workday.end_time.total_seconds(), now_in_seconds)
time_left = max(end_time - start_time, 0)
if not time_left:
res = getdate(add_to_date(res, days=1, as_datetime=True))
continue
time_taken = min(time_needed, time_left)
time_needed -= time_taken
time_required = till_start_time + time_taken
res = add_to_date(res, seconds=time_required, as_datetime=True)
return res
def calc_time(
self,
start_at: str,
priority: str,
target: Literal["response_time", "resolution_time"],
):
res = get_datetime(start_at)
priority = self.get_priorities()[priority]
time_needed = priority.get(target, 0)
holidays = self.get_holidays()
weekdays = get_weekdays()
workdays = self.get_workdays()
while time_needed:
today = res
today_day = getdate(today)
today_weekday = weekdays[today.weekday()]
is_workday = today_weekday in workdays
is_holiday = today_day in holidays
if is_holiday or not is_workday:
res = add_to_date(res, days=1, as_datetime=True)
continue
today_workday = workdays[today_weekday]
now_in_seconds = time_diff_in_seconds(today, today_day)
start_time = max(today_workday.start_time.total_seconds(), now_in_seconds)
till_start_time = max(start_time - now_in_seconds, 0)
end_time = max(today_workday.end_time.total_seconds(), now_in_seconds)
time_left = max(end_time - start_time, 0)
if not time_left:
res = getdate(add_to_date(res, days=1, as_datetime=True))
continue
time_taken = min(time_needed, time_left)
time_needed -= time_taken
time_required = till_start_time + time_taken
res = add_to_date(res, seconds=time_required, as_datetime=True)
return res
def get_working_days(self) -> dict[str, dict]:
workdays = []
for row in self.support_and_resolution:
workdays.append(row.workday)
return workdays
def get_working_days(self) -> dict[str, dict]:
workdays = []
for row in self.support_and_resolution:
workdays.append(row.workday)
return workdays
def get_working_hours(self) -> dict[str, dict]:
res = {}
for row in self.support_and_resolution:
res[row.workday] = (row.start_time, row.end_time)
return res
def get_working_hours(self) -> dict[str, dict]:
res = {}
for row in self.support_and_resolution:
res[row.workday] = (row.start_time, row.end_time)
return res
def is_working_time(self, date_time, working_hours):
day_of_week = get_weekdays()[date_time.weekday()]
start_time, end_time = working_hours.get(day_of_week, (0, 0))
date_time = timedelta(hours=date_time.hour, minutes=date_time.minute, seconds=date_time.second)
return start_time <= date_time < end_time
def is_working_time(self, date_time, working_hours):
day_of_week = get_weekdays()[date_time.weekday()]
start_time, end_time = working_hours.get(day_of_week, (0, 0))
date_time = timedelta(
hours=date_time.hour, minutes=date_time.minute, seconds=date_time.second
)
return start_time <= date_time < end_time
def calc_elapsed_time(self, start_time, end_time) -> float:
"""
Get took from start to end, excluding non-working hours
def calc_elapsed_time(self, start_time, end_time) -> float:
"""
Get took from start to end, excluding non-working hours
:param start_at: Date at which calculation starts
:param end_at: Date at which calculation ends
:return: Number of seconds
"""
start_time = get_datetime(start_time)
end_time = get_datetime(end_time)
holiday_list = []
working_day_list = self.get_working_days()
working_hours = self.get_working_hours()
:param start_at: Date at which calculation starts
:param end_at: Date at which calculation ends
:return: Number of seconds
"""
start_time = get_datetime(start_time)
end_time = get_datetime(end_time)
holiday_list = []
working_day_list = self.get_working_days()
working_hours = self.get_working_hours()
total_seconds = 0
current_time = start_time
total_seconds = 0
current_time = start_time
while current_time < end_time:
in_holiday_list = current_time.date() in holiday_list
not_in_working_day_list = get_weekdays()[current_time.weekday()] not in working_day_list
if in_holiday_list or not_in_working_day_list or not self.is_working_time(current_time, working_hours):
current_time += timedelta(seconds=1)
continue
total_seconds += 1
current_time += timedelta(seconds=1)
while current_time < end_time:
in_holiday_list = current_time.date() in holiday_list
not_in_working_day_list = (
get_weekdays()[current_time.weekday()] not in working_day_list
)
if (
in_holiday_list
or not_in_working_day_list
or not self.is_working_time(current_time, working_hours)
):
current_time += timedelta(seconds=1)
continue
total_seconds += 1
current_time += timedelta(seconds=1)
return total_seconds
return total_seconds
def get_holidays(self):
res = []
if not self.holiday_list:
return res
holiday_list = frappe.get_doc("HD Service Holiday List", self.holiday_list)
for row in holiday_list.holidays:
res.append(row.holiday_date)
return res
def get_holidays(self):
res = []
if not self.holiday_list:
return res
holiday_list = frappe.get_doc("HD Service Holiday List", self.holiday_list)
for row in holiday_list.holidays:
res.append(row.holiday_date)
return res
def get_priorities(self):
"""
Return priorities related info as a dict. With `priority` as key
"""
res = {}
for row in self.priorities:
res[row.priority] = row
return res
def get_priorities(self):
"""
Return priorities related info as a dict. With `priority` as key
"""
res = {}
for row in self.priorities:
res[row.priority] = row
return res
def get_workdays(self) -> dict[str, dict]:
"""
Return workdays related info as a dict. With `workday` as key
"""
res = {}
for row in self.support_and_resolution:
res[row.workday] = row
return res
def get_workdays(self) -> dict[str, dict]:
"""
Return workdays related info as a dict. With `workday` as key
"""
res = {}
for row in self.support_and_resolution:
res[row.workday] = row
return res
def get_repeated(values):
unique_list = []
diff = []
for value in values:
if value not in unique_list:
unique_list.append(str(value))
else:
if value not in diff:
diff.append(str(value))
return " ".join(diff)
unique_list = []
diff = []
for value in values:
if value not in unique_list:
unique_list.append(str(value))
else:
if value not in diff:
diff.append(str(value))
return " ".join(diff)

View File

@@ -2,7 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Settings", {
refresh: function (frm) {
//
},
})
refresh: function (frm) {
//
},
});

View File

@@ -3,53 +3,54 @@
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.naming import append_number_if_name_exists
from frappe.model.document import Document
import frappe
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.realtime import get_website_room
class HDSettings(Document):
def get_base_support_rotation(self):
"""Returns the base support rotation rule if it exists, else creats once and returns it"""
def get_base_support_rotation(self):
"""Returns the base support rotation rule if it exists, else creats once and returns it"""
if not self.base_support_rotation:
self.create_base_support_rotation()
if not self.base_support_rotation:
self.create_base_support_rotation()
return self.base_support_rotation
return self.base_support_rotation
def create_base_support_rotation(self):
"""Creates the base support rotation rule, and set it to frappe desk settings"""
def create_base_support_rotation(self):
"""Creates the base support rotation rule, and set it to frappe desk settings"""
rule_doc = frappe.new_doc("Assignment Rule")
rule_doc.name = append_number_if_name_exists(
"Assignment Rule", "Support Rotation"
)
rule_doc.document_type = "HD Ticket"
rule_doc.assign_condition = "status == 'Open'"
rule_doc.priority = 0
rule_doc.disabled = True # Disable the rule by default, when agents are added to the group, the rule will be enabled
rule_doc = frappe.new_doc("Assignment Rule")
rule_doc.name = append_number_if_name_exists(
"Assignment Rule", "Support Rotation"
)
rule_doc.document_type = "HD Ticket"
rule_doc.assign_condition = "status == 'Open'"
rule_doc.priority = 0
rule_doc.disabled = True # Disable the rule by default, when agents are added to the group, the rule will be enabled
for day in [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]:
day_doc = frappe.get_doc({"doctype": "Assignment Rule Day", "day": day})
rule_doc.append("assignment_days", day_doc)
for day in [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]:
day_doc = frappe.get_doc({"doctype": "Assignment Rule Day", "day": day})
rule_doc.append("assignment_days", day_doc)
rule_doc.save(ignore_permissions=True)
self.base_support_rotation = rule_doc.name
self.save(ignore_permissions=True)
rule_doc.save(ignore_permissions=True)
self.base_support_rotation = rule_doc.name
self.save(ignore_permissions=True)
return
return
def on_update(self):
event = "helpdesk:settings-updated"
room = get_website_room()
def on_update(self):
event = "helpdesk:settings-updated"
room = get_website_room()
frappe.publish_realtime(event, room=room, after_commit=True)
frappe.publish_realtime(event, room=room, after_commit=True)

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Team", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -5,204 +5,205 @@ from frappe.utils.caching import redis_cache
from pypika import Criterion, Order
from helpdesk.consts import DEFAULT_TICKET_TEMPLATE
from helpdesk.helpdesk.doctype.hd_form_script.hd_form_script import get_form_script
from helpdesk.helpdesk.doctype.hd_ticket_template.api import get_one as get_template
from helpdesk.utils import check_permissions, get_customer, is_agent
from helpdesk.helpdesk.doctype.hd_form_script.hd_form_script import get_form_script
@frappe.whitelist()
# flake8: noqa
def new(doc, attachments=[]):
doc["doctype"] = "HD Ticket"
doc["via_customer_portal"] = bool(frappe.session.user)
d = frappe.get_doc(doc).insert()
d.create_communication_via_contact(d.description, attachments)
return d
doc["doctype"] = "HD Ticket"
doc["via_customer_portal"] = bool(frappe.session.user)
d = frappe.get_doc(doc).insert()
d.create_communication_via_contact(d.description, attachments)
return d
@frappe.whitelist()
def get_one(name):
check_permissions("HD Ticket", None)
QBContact = frappe.qb.DocType("Contact")
QBTicket = frappe.qb.DocType("HD Ticket")
check_permissions("HD Ticket", None)
QBContact = frappe.qb.DocType("Contact")
QBTicket = frappe.qb.DocType("HD Ticket")
_is_agent = is_agent()
_is_agent = is_agent()
query = (
frappe.qb.from_(QBTicket)
.select(QBTicket.star)
.where(QBTicket.name == name)
.limit(1)
)
query = (
frappe.qb.from_(QBTicket)
.select(QBTicket.star)
.where(QBTicket.name == name)
.limit(1)
)
if not _is_agent:
query = query.where(get_customer_criteria())
if not _is_agent:
query = query.where(get_customer_criteria())
ticket = query.run(as_dict=True)
if not len(ticket):
frappe.throw(_("Ticket not found"), frappe.DoesNotExistError)
ticket = ticket.pop()
ticket = query.run(as_dict=True)
if not len(ticket):
frappe.throw(_("Ticket not found"), frappe.DoesNotExistError)
ticket = ticket.pop()
contact = (
frappe.qb.from_(QBContact)
.select(
QBContact.company_name,
QBContact.email_id,
QBContact.image,
QBContact.mobile_no,
QBContact.name,
QBContact.phone,
)
.where(QBContact.name == ticket.contact)
.run(as_dict=True)
)
if contact:
contact = contact[0]
else:
contact = {
"email_id": ticket.raised_by,
"name": ticket.raised_by.split("@")[0],
}
contact = (
frappe.qb.from_(QBContact)
.select(
QBContact.company_name,
QBContact.email_id,
QBContact.image,
QBContact.mobile_no,
QBContact.name,
QBContact.phone,
)
.where(QBContact.name == ticket.contact)
.run(as_dict=True)
)
if contact:
contact = contact[0]
else:
contact = {
"email_id": ticket.raised_by,
"name": ticket.raised_by.split("@")[0],
}
return {
**ticket,
"comments": get_comments(name),
"communications": get_communications(name),
"contact": contact,
"history": get_history(name),
"tags": get_tags(name),
"template": get_template(ticket.template or DEFAULT_TICKET_TEMPLATE),
"views": get_views(name),
"_form_script": get_form_script('HD Ticket')
}
return {
**ticket,
"comments": get_comments(name),
"communications": get_communications(name),
"contact": contact,
"history": get_history(name),
"tags": get_tags(name),
"template": get_template(ticket.template or DEFAULT_TICKET_TEMPLATE),
"views": get_views(name),
"_form_script": get_form_script("HD Ticket"),
}
def get_customer_criteria():
QBTicket = frappe.qb.DocType("HD Ticket")
user = frappe.session.user
conditions = [
QBTicket.contact == user,
QBTicket.raised_by == user,
]
customer = get_customer(user)
for c in customer:
conditions.append(QBTicket.customer == c)
return Criterion.any(conditions)
QBTicket = frappe.qb.DocType("HD Ticket")
user = frappe.session.user
conditions = [
QBTicket.contact == user,
QBTicket.raised_by == user,
]
customer = get_customer(user)
for c in customer:
conditions.append(QBTicket.customer == c)
return Criterion.any(conditions)
def get_assignee(_assign: str):
j = frappe.parse_json(_assign)
if not j or len(j) < 1:
return
return get_user_info_for_avatar(j.pop())
j = frappe.parse_json(_assign)
if not j or len(j) < 1:
return
return get_user_info_for_avatar(j.pop())
def get_communications(ticket: str):
QBCommunication = frappe.qb.DocType("Communication")
communications = (
frappe.qb.from_(QBCommunication)
.select(
QBCommunication.bcc,
QBCommunication.cc,
QBCommunication.content,
QBCommunication.creation,
QBCommunication.name,
QBCommunication.sender,
QBCommunication.recipients
)
.where(QBCommunication.reference_doctype == "HD Ticket")
.where(QBCommunication.reference_name == ticket)
.orderby(QBCommunication.creation, order=Order.asc)
.run(as_dict=True)
)
for c in communications:
c.attachments = get_attachments("Communication", c.name)
c.user = get_user_info_for_avatar(c.sender)
return communications
QBCommunication = frappe.qb.DocType("Communication")
communications = (
frappe.qb.from_(QBCommunication)
.select(
QBCommunication.bcc,
QBCommunication.cc,
QBCommunication.content,
QBCommunication.creation,
QBCommunication.name,
QBCommunication.sender,
QBCommunication.recipients,
)
.where(QBCommunication.reference_doctype == "HD Ticket")
.where(QBCommunication.reference_name == ticket)
.orderby(QBCommunication.creation, order=Order.asc)
.run(as_dict=True)
)
for c in communications:
c.attachments = get_attachments("Communication", c.name)
c.user = get_user_info_for_avatar(c.sender)
return communications
def get_comments(ticket: str):
if not frappe.has_permission("HD Ticket Comment", "read"):
return []
QBComment = frappe.qb.DocType("HD Ticket Comment")
comments = (
frappe.qb.from_(QBComment)
.select(
QBComment.commented_by,
QBComment.content,
QBComment.creation,
QBComment.is_pinned,
QBComment.name,
)
.where(QBComment.reference_ticket == ticket)
.orderby(QBComment.creation, order=Order.asc)
.run(as_dict=True)
)
for c in comments:
c.user = get_user_info_for_avatar(c.commented_by)
return comments
if not frappe.has_permission("HD Ticket Comment", "read"):
return []
QBComment = frappe.qb.DocType("HD Ticket Comment")
comments = (
frappe.qb.from_(QBComment)
.select(
QBComment.commented_by,
QBComment.content,
QBComment.creation,
QBComment.is_pinned,
QBComment.name,
)
.where(QBComment.reference_ticket == ticket)
.orderby(QBComment.creation, order=Order.asc)
.run(as_dict=True)
)
for c in comments:
c.user = get_user_info_for_avatar(c.commented_by)
return comments
def get_history(ticket: str):
if not frappe.has_permission("HD Ticket Activity", "read"):
return []
QBActivity = frappe.qb.DocType("HD Ticket Activity")
history = (
frappe.qb.from_(QBActivity)
.select(
QBActivity.name, QBActivity.action, QBActivity.owner, QBActivity.creation
)
.where(QBActivity.ticket == ticket)
.orderby(QBActivity.creation, order=Order.desc)
)
history = history.run(as_dict=True)
for h in history:
h.user = get_user_info_for_avatar(h.owner)
return history
if not frappe.has_permission("HD Ticket Activity", "read"):
return []
QBActivity = frappe.qb.DocType("HD Ticket Activity")
history = (
frappe.qb.from_(QBActivity)
.select(
QBActivity.name, QBActivity.action, QBActivity.owner, QBActivity.creation
)
.where(QBActivity.ticket == ticket)
.orderby(QBActivity.creation, order=Order.desc)
)
history = history.run(as_dict=True)
for h in history:
h.user = get_user_info_for_avatar(h.owner)
return history
def get_views(ticket: str):
QBViewLog = frappe.qb.DocType("View Log")
views = (
frappe.qb.from_(QBViewLog)
.select(
QBViewLog.creation,
QBViewLog.name,
QBViewLog.viewed_by,
)
.where(QBViewLog.reference_doctype == "HD Ticket")
.where(QBViewLog.reference_name == ticket)
.orderby(QBViewLog.creation, order=Order.desc)
.run(as_dict=True)
)
for v in views:
v.user = get_user_info_for_avatar(v.viewed_by)
return views
QBViewLog = frappe.qb.DocType("View Log")
views = (
frappe.qb.from_(QBViewLog)
.select(
QBViewLog.creation,
QBViewLog.name,
QBViewLog.viewed_by,
)
.where(QBViewLog.reference_doctype == "HD Ticket")
.where(QBViewLog.reference_name == ticket)
.orderby(QBViewLog.creation, order=Order.desc)
.run(as_dict=True)
)
for v in views:
v.user = get_user_info_for_avatar(v.viewed_by)
return views
def get_tags(ticket: str):
QBTag = frappe.qb.DocType("Tag Link")
rows = (
frappe.qb.from_(QBTag)
.select(QBTag.tag)
.where(QBTag.document_type == "HD Ticket")
.where(QBTag.document_name == ticket)
.orderby(QBTag.creation, order=Order.asc)
.run(as_dict=True)
)
res = []
for tag in rows:
res.append(tag.tag)
return res
QBTag = frappe.qb.DocType("Tag Link")
rows = (
frappe.qb.from_(QBTag)
.select(QBTag.tag)
.where(QBTag.document_type == "HD Ticket")
.where(QBTag.document_name == ticket)
.orderby(QBTag.creation, order=Order.asc)
.run(as_dict=True)
)
res = []
for tag in rows:
res.append(tag.tag)
return res
@redis_cache()
def get_attachments(doctype, name):
QBFile = frappe.qb.DocType("File")
QBFile = frappe.qb.DocType("File")
return (
frappe.qb.from_(QBFile)
.select(QBFile.name, QBFile.file_url, QBFile.file_name)
.where(QBFile.attached_to_doctype == doctype)
.where(QBFile.attached_to_name == name)
.run(as_dict=True)
)
return (
frappe.qb.from_(QBFile)
.select(QBFile.name, QBFile.file_url, QBFile.file_name)
.where(QBFile.attached_to_doctype == doctype)
.where(QBFile.attached_to_name == name)
.run(as_dict=True)
)

File diff suppressed because it is too large Load Diff

View File

@@ -2,103 +2,118 @@
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.desk.form.assign_to import add as assign
from frappe.tests.utils import FrappeTestCase
def get_ticket_obj():
return {
"doctype": "HD Ticket",
"subject": "Test Ticket",
"description": "Test Ticket Description"
}
return {
"doctype": "HD Ticket",
"subject": "Test Ticket",
"description": "Test Ticket Description",
}
non_agent = "non_agent@test.com"
agent = "agent@test.com"
agent2 = "agent2@test.com"
class TestHDTicket(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
frappe.db.delete("HD Ticket")
frappe.get_doc({
"doctype": "User",
"first_name": "Non Agent",
"email": non_agent
}).insert()
@classmethod
def setUpClass(cls):
super().setUpClass()
frappe.db.delete("HD Ticket")
frappe.get_doc(
{"doctype": "User", "first_name": "Non Agent", "email": non_agent}
).insert()
frappe.get_doc({
"doctype": "User",
"first_name": "Agent",
"email": agent
}).insert()
frappe.get_doc({
"doctype": "HD Agent",
"user": agent
}).insert()
frappe.get_doc(
{"doctype": "User", "first_name": "Agent", "email": agent}
).insert()
frappe.get_doc({"doctype": "HD Agent", "user": agent}).insert()
frappe.get_doc({
"doctype": "User",
"first_name": "Agent2",
"email": agent2
}).insert()
frappe.get_doc({
"doctype": "HD Agent",
"user": agent2
}).insert()
frappe.get_doc(
{"doctype": "User", "first_name": "Agent2", "email": agent2}
).insert()
frappe.get_doc({"doctype": "HD Agent", "user": agent2}).insert()
def test_ticket_creation(self):
ticket = frappe.get_doc(get_ticket_obj())
ticket.insert()
self.assertTrue(ticket.name)
def test_agent_flow(self):
ticket = frappe.get_doc(get_ticket_obj())
ticket.insert()
def test_ticket_creation(self):
ticket = frappe.get_doc(get_ticket_obj())
ticket.insert()
self.assertTrue(ticket.name)
ticket.assign_agent(agent)
ticket.assign_agent(agent2)
notification = frappe.get_all("HD Notification", filters={"reference_ticket": ticket.name, "notification_type": "Assignment", "user_to": ["in", [agent, agent2]], "user_from": "Administrator"})
self.assertEqual(len(notification), 2)
ticket.status = "Replied"
ticket.save()
def test_agent_flow(self):
ticket = frappe.get_doc(get_ticket_obj())
ticket.insert()
ticket.status = "Open"
ticket.save()
self.assertTrue(ticket)
ticket.assign_agent(agent)
ticket.assign_agent(agent2)
notification = frappe.get_all(
"HD Notification",
filters={
"reference_ticket": ticket.name,
"notification_type": "Assignment",
"user_to": ["in", [agent, agent2]],
"user_from": "Administrator",
},
)
self.assertEqual(len(notification), 2)
notification = frappe.get_all("HD Notification", filters={"reference_ticket": ticket.name, "notification_type": "Reaction", "user_to": ["in", [agent, agent2]], "user_from": "Administrator"})
self.assertEqual(len(notification), 2)
ticket.status = "Replied"
ticket.save()
ticket.status = "Resolved"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Open"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Closed"
ticket.save()
self.assertTrue(ticket)
notification = frappe.get_all(
"HD Notification",
filters={
"reference_ticket": ticket.name,
"notification_type": "Reaction",
"user_to": ["in", [agent, agent2]],
"user_from": "Administrator",
},
)
self.assertEqual(len(notification), 2)
def test_non_agent_flow(self):
ticket = frappe.get_doc(get_ticket_obj())
ticket.insert()
ticket.status = "Resolved"
ticket.save()
self.assertTrue(ticket)
ticket.assign_agent(non_agent)
notification = frappe.get_all("HD Notification", filters={"reference_ticket": ticket.name, "notification_type": "Assignment", "user_to": non_agent, "user_from": "Administrator"})
self.assertEqual(len(notification), 1)
ticket.status = "Replied"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Closed"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Open"
ticket.save()
self.assertTrue(ticket)
def test_non_agent_flow(self):
ticket = frappe.get_doc(get_ticket_obj())
ticket.insert()
ticket.status = "Resolved"
ticket.save()
self.assertTrue(ticket)
ticket.assign_agent(non_agent)
notification = frappe.get_all(
"HD Notification",
filters={
"reference_ticket": ticket.name,
"notification_type": "Assignment",
"user_to": non_agent,
"user_from": "Administrator",
},
)
self.assertEqual(len(notification), 1)
ticket.status = "Closed"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Replied"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Open"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Resolved"
ticket.save()
self.assertTrue(ticket)
ticket.status = "Closed"
ticket.save()
self.assertTrue(ticket)

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Ticket Activity", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Ticket Comment", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -2,10 +2,10 @@ import frappe
def execute():
doctype = "HD Ticket Feedback Option"
options = frappe.get_list(
doctype, fields=["name", "label"], limit_page_length=99999
)
for opt in options:
if opt.name != opt.label:
frappe.rename_doc(doctype, opt.name, opt.label, ignore_if_exists=True)
doctype = "HD Ticket Feedback Option"
options = frappe.get_list(
doctype, fields=["name", "label"], limit_page_length=99999
)
for opt in options:
if opt.name != opt.label:
frappe.rename_doc(doctype, opt.name, opt.label, ignore_if_exists=True)

View File

@@ -2,6 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Ticket Priority", {
// refresh: function(frm) {
// }
})
// refresh: function(frm) {
// }
});

View File

@@ -1,7 +1,8 @@
from typing import Literal
import frappe
from frappe import _
# from frappe import _
from pypika import JoinType
from helpdesk.utils import check_permissions
@@ -13,51 +14,51 @@ DOCTYPE_TICKET = "HD Ticket"
@frappe.whitelist()
def get_one(name: str):
check_permissions(DOCTYPE_TEMPLATE, None)
found, about = frappe.get_value(DOCTYPE_TEMPLATE, name, ["name", "about"]) or [None, None]
if not found:
return {
"about": None,
"fields": []
}
check_permissions(DOCTYPE_TEMPLATE, None)
found, about = frappe.get_value(DOCTYPE_TEMPLATE, name, ["name", "about"]) or [
None,
None,
]
if not found:
return {"about": None, "fields": []}
fields = []
fields.extend(get_fields(name, "DocField"))
fields.extend(get_fields(name, "Custom Field"))
return {
"about": about,
"fields": fields,
}
fields = []
fields.extend(get_fields(name, "DocField"))
fields.extend(get_fields(name, "Custom Field"))
return {
"about": about,
"fields": fields,
}
def get_fields(template: str, fetch: Literal["Custom Field", "DocField"]):
QBField = frappe.qb.DocType(DOCTYPE_TEMPLATE_FIELD)
QBFetch = frappe.qb.DocType(fetch)
fields = (
frappe.qb.from_(QBField)
.select(QBField.star)
.where(QBField.parent == template)
.where(QBField.parentfield == "fields")
.where(QBField.parenttype == DOCTYPE_TEMPLATE)
)
where_parent = QBFetch.parent == DOCTYPE_TICKET
if fetch == "Custom Field":
where_parent = QBFetch.dt == DOCTYPE_TICKET
return (
frappe.qb.from_(fields)
.select(
QBFetch.description,
QBFetch.fieldtype,
QBFetch.label,
QBFetch.options,
fields.fieldname,
fields.hide_from_customer,
fields.required,
fields.url_method,
fields.placeholder,
)
.join(QBFetch, JoinType.inner)
.on(QBFetch.fieldname == fields.fieldname)
.where(where_parent)
.run(as_dict=True)
)
QBField = frappe.qb.DocType(DOCTYPE_TEMPLATE_FIELD)
QBFetch = frappe.qb.DocType(fetch)
fields = (
frappe.qb.from_(QBField)
.select(QBField.star)
.where(QBField.parent == template)
.where(QBField.parentfield == "fields")
.where(QBField.parenttype == DOCTYPE_TEMPLATE)
)
where_parent = QBFetch.parent == DOCTYPE_TICKET
if fetch == "Custom Field":
where_parent = QBFetch.dt == DOCTYPE_TICKET
return (
frappe.qb.from_(fields)
.select(
QBFetch.description,
QBFetch.fieldtype,
QBFetch.label,
QBFetch.options,
fields.fieldname,
fields.hide_from_customer,
fields.required,
fields.url_method,
fields.placeholder,
)
.join(QBFetch, JoinType.inner)
.on(QBFetch.fieldname == fields.fieldname)
.where(where_parent)
.run(as_dict=True)
)

View File

@@ -2,5 +2,5 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Ticket Template", {
setup: function (frm) {},
})
setup: function (frm) {},
});

View File

@@ -9,40 +9,40 @@ from helpdesk.consts import DEFAULT_TICKET_TEMPLATE
class HDTicketTemplate(Document):
def validate(self):
self.verify_field_exists()
def validate(self):
self.verify_field_exists()
def on_trash(self):
self.prevent_default_delete()
def on_trash(self):
self.prevent_default_delete()
def verify_field_exists(self):
for f in self.fields:
exists = self.docfield_exists(f.fieldname) or self.custom_field_exists(
f.fieldname
)
if not exists:
text = _("Field `{0}` does not exist in Ticket").format(f.fieldname)
frappe.throw(text)
def verify_field_exists(self):
for f in self.fields:
exists = self.docfield_exists(f.fieldname) or self.custom_field_exists(
f.fieldname
)
if not exists:
text = _("Field `{0}` does not exist in Ticket").format(f.fieldname)
frappe.throw(text)
def docfield_exists(self, fieldname: str):
return frappe.db.exists(
{
"doctype": "DocField",
"fieldname": fieldname,
"parent": "HD Ticket",
}
)
def docfield_exists(self, fieldname: str):
return frappe.db.exists(
{
"doctype": "DocField",
"fieldname": fieldname,
"parent": "HD Ticket",
}
)
def custom_field_exists(self, fieldname: str):
return frappe.db.exists(
{
"doctype": "Custom Field",
"fieldname": fieldname,
"dt": "HD Ticket",
}
)
def custom_field_exists(self, fieldname: str):
return frappe.db.exists(
{
"doctype": "Custom Field",
"fieldname": fieldname,
"dt": "HD Ticket",
}
)
def prevent_default_delete(self):
if self.name == DEFAULT_TICKET_TEMPLATE:
text = _("Default template can not be deleted")
frappe.throw(text, frappe.PermissionError)
def prevent_default_delete(self):
if self.name == DEFAULT_TICKET_TEMPLATE:
text = _("Default template can not be deleted")
frappe.throw(text, frappe.PermissionError)

View File

@@ -2,5 +2,5 @@
// For license information, please see license.txt
frappe.ui.form.on("HD Ticket Type", {
refresh: function (frm) {},
})
refresh: function (frm) {},
});

View File

@@ -3,46 +3,43 @@
/* eslint-disable */
frappe.query_reports["First Response Time for Tickets"] = {
filters: [
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
reqd: 1,
default: frappe.datetime.add_days(frappe.datetime.nowdate(), -30),
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
reqd: 1,
default: frappe.datetime.nowdate(),
},
],
get_chart_data: function (_columns, result) {
return {
data: {
labels: result.map((d) => d.creation_date),
datasets: [
{
name: "First Response Time",
values: result.map((d) => d.first_response_time),
},
],
},
type: "line",
tooltipOptions: {
formatTooltipY: (d) => {
let duration_options = {
hide_days: 0,
hide_seconds: 0,
}
return frappe.utils.get_formatted_duration(
d,
duration_options
)
},
},
}
},
}
filters: [
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
reqd: 1,
default: frappe.datetime.add_days(frappe.datetime.nowdate(), -30),
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
reqd: 1,
default: frappe.datetime.nowdate(),
},
],
get_chart_data: function (_columns, result) {
return {
data: {
labels: result.map((d) => d.creation_date),
datasets: [
{
name: "First Response Time",
values: result.map((d) => d.first_response_time),
},
],
},
type: "line",
tooltipOptions: {
formatTooltipY: (d) => {
let duration_options = {
hide_days: 0,
hide_seconds: 0,
};
return frappe.utils.get_formatted_duration(d, duration_options);
},
},
};
},
};

View File

@@ -3,20 +3,20 @@
/* eslint-disable */
frappe.query_reports["Support Hour Distribution"] = {
filters: [
{
lable: __("From Date"),
fieldname: "from_date",
fieldtype: "Date",
default: frappe.datetime.nowdate(),
reqd: 1,
},
{
lable: __("To Date"),
fieldname: "to_date",
fieldtype: "Date",
default: frappe.datetime.nowdate(),
reqd: 1,
},
],
}
filters: [
{
lable: __("From Date"),
fieldname: "from_date",
fieldtype: "Date",
default: frappe.datetime.nowdate(),
reqd: 1,
},
{
lable: __("To Date"),
fieldname: "to_date",
fieldtype: "Date",
default: frappe.datetime.nowdate(),
reqd: 1,
},
],
};

View File

@@ -3,129 +3,124 @@
/* eslint-disable */
frappe.query_reports["Ticket Analytics"] = {
filters: [
{
fieldname: "based_on",
label: __("Based On"),
fieldtype: "Select",
options: [
"Contact",
"Ticket Type",
"Ticket Priority",
"Assigned To",
],
default: "Contact",
reqd: 1,
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.nowdate(), -30),
reqd: 1,
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.nowdate(),
reqd: 1,
},
{
fieldname: "range",
label: __("Range"),
fieldtype: "Select",
options: [
{ value: "Weekly", label: __("Weekly") },
{ value: "Monthly", label: __("Monthly") },
{ value: "Quarterly", label: __("Quarterly") },
{ value: "Yearly", label: __("Yearly") },
],
default: "Weekly",
reqd: 1,
},
{
fieldname: "status",
label: __("Status"),
fieldtype: "Select",
options: [
"",
{ label: __("Open"), value: "Open" },
{ label: __("Replied"), value: "Replied" },
{ label: __("Resolved"), value: "Resolved" },
{ label: __("Closed"), value: "Closed" },
],
},
{
fieldname: "priority",
label: __("Ticket Priority"),
fieldtype: "Link",
options: "HD Ticket Priority",
},
{
fieldname: "contact",
label: __("Contact"),
fieldtype: "Link",
options: "Contact",
},
{
fieldname: "assigned_to",
label: __("Assigned To"),
fieldtype: "Link",
options: "User",
},
],
after_datatable_render: function (datatable_obj) {
$(datatable_obj.wrapper).find("input[type=checkbox]")[0].click()
},
filters: [
{
fieldname: "based_on",
label: __("Based On"),
fieldtype: "Select",
options: ["Contact", "Ticket Type", "Ticket Priority", "Assigned To"],
default: "Contact",
reqd: 1,
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.nowdate(), -30),
reqd: 1,
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.nowdate(),
reqd: 1,
},
{
fieldname: "range",
label: __("Range"),
fieldtype: "Select",
options: [
{ value: "Weekly", label: __("Weekly") },
{ value: "Monthly", label: __("Monthly") },
{ value: "Quarterly", label: __("Quarterly") },
{ value: "Yearly", label: __("Yearly") },
],
default: "Weekly",
reqd: 1,
},
{
fieldname: "status",
label: __("Status"),
fieldtype: "Select",
options: [
"",
{ label: __("Open"), value: "Open" },
{ label: __("Replied"), value: "Replied" },
{ label: __("Resolved"), value: "Resolved" },
{ label: __("Closed"), value: "Closed" },
],
},
{
fieldname: "priority",
label: __("Ticket Priority"),
fieldtype: "Link",
options: "HD Ticket Priority",
},
{
fieldname: "contact",
label: __("Contact"),
fieldtype: "Link",
options: "Contact",
},
{
fieldname: "assigned_to",
label: __("Assigned To"),
fieldtype: "Link",
options: "User",
},
],
after_datatable_render: function (datatable_obj) {
$(datatable_obj.wrapper).find("input[type=checkbox]")[0].click();
},
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
events: {
onCheckRow: function (data) {
if (data && data.length) {
row_name = data[2].content
row_values = data.slice(3).map(function (column) {
return column.content
})
entry = {
name: row_name,
values: row_values,
}
let raw_data = frappe.query_report.chart.data
let new_datasets = raw_data.datasets
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
events: {
onCheckRow: function (data) {
if (data && data.length) {
row_name = data[2].content;
row_values = data.slice(3).map(function (column) {
return column.content;
});
entry = {
name: row_name,
values: row_values,
};
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
var found = false
var found = false;
for (var i = 0; i < new_datasets.length; i++) {
if (new_datasets[i].name == row_name) {
found = true
new_datasets.splice(i, 1)
break
}
}
for (var i = 0; i < new_datasets.length; i++) {
if (new_datasets[i].name == row_name) {
found = true;
new_datasets.splice(i, 1);
break;
}
}
if (!found) {
new_datasets.push(entry)
}
if (!found) {
new_datasets.push(entry);
}
let new_data = {
labels: raw_data.labels,
datasets: new_datasets,
}
let new_data = {
labels: raw_data.labels,
datasets: new_datasets,
};
setTimeout(() => {
frappe.query_report.chart.update(new_data)
}, 500)
setTimeout(() => {
frappe.query_report.chart.update(new_data);
}, 500);
setTimeout(() => {
frappe.query_report.chart.draw(true)
}, 1000)
frappe.query_report.raw_chart_data = new_data
}
},
},
})
},
}
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000);
frappe.query_report.raw_chart_data = new_data;
}
},
},
});
},
};

View File

@@ -4,259 +4,257 @@
from __future__ import unicode_literals
import json
from datetime import date
import frappe
from frappe import _, scrub
from frappe.utils import add_days, add_to_date, flt, getdate
from six import iteritems
from datetime import date
def get_fiscal_year():
# TODO: handle this function properly
today_date = date.today()
return today_date
# TODO: handle this function properly
today_date = date.today()
return today_date
def execute(filters=None):
return TicketAnalytics(filters).run()
return TicketAnalytics(filters).run()
class TicketAnalytics(object):
def __init__(self, filters=None):
"""Ticket Analytics Report"""
self.filters = frappe._dict(filters or {})
self.get_period_date_ranges()
class TicketAnalytics:
def __init__(self, filters=None):
"""Ticket Analytics Report"""
self.filters = frappe._dict(filters or {})
self.get_period_date_ranges()
def run(self):
self.get_columns()
self.get_data()
self.get_chart_data()
def run(self):
self.get_columns()
self.get_data()
self.get_chart_data()
return self.columns, self.data, None, self.chart
return self.columns, self.data, None, self.chart
def get_columns(self):
self.columns = []
def get_columns(self):
self.columns = []
if self.filters.based_on == "Contact":
self.columns.append(
{
"label": _("Contact"),
"options": "Contact",
"fieldname": "contact",
"fieldtype": "Link",
"width": 200,
}
)
if self.filters.based_on == "Contact":
self.columns.append(
{
"label": _("Contact"),
"options": "Contact",
"fieldname": "contact",
"fieldtype": "Link",
"width": 200,
}
)
elif self.filters.based_on == "Assigned To":
self.columns.append(
{
"label": _("User"),
"fieldname": "user",
"fieldtype": "Link",
"options": "User",
"width": 200,
}
)
elif self.filters.based_on == "Assigned To":
self.columns.append(
{
"label": _("User"),
"fieldname": "user",
"fieldtype": "Link",
"options": "User",
"width": 200,
}
)
elif self.filters.based_on == "Ticket Type":
self.columns.append(
{
"label": _("Ticket Type"),
"fieldname": "ticket_type",
"fieldtype": "Link",
"options": "HD Ticket Type",
"width": 200,
}
)
elif self.filters.based_on == "Ticket Type":
self.columns.append(
{
"label": _("Ticket Type"),
"fieldname": "ticket_type",
"fieldtype": "Link",
"options": "HD Ticket Type",
"width": 200,
}
)
elif self.filters.based_on == "Ticket Priority":
self.columns.append(
{
"label": _("Ticket Priority"),
"fieldname": "priority",
"fieldtype": "Link",
"options": "HD Ticket Priority",
"width": 200,
}
)
elif self.filters.based_on == "Ticket Priority":
self.columns.append(
{
"label": _("Ticket Priority"),
"fieldname": "priority",
"fieldtype": "Link",
"options": "HD Ticket Priority",
"width": 200,
}
)
for end_date in self.periodic_daterange:
period = self.get_period(end_date)
self.columns.append(
{
"label": _(period),
"fieldname": scrub(period),
"fieldtype": "Int",
"width": 120,
}
)
for end_date in self.periodic_daterange:
period = self.get_period(end_date)
self.columns.append(
{
"label": _(period),
"fieldname": scrub(period),
"fieldtype": "Int",
"width": 120,
}
)
self.columns.append(
{
"label": _("Total"),
"fieldname": "total",
"fieldtype": "Int",
"width": 120,
}
)
self.columns.append(
{
"label": _("Total"),
"fieldname": "total",
"fieldtype": "Int",
"width": 120,
}
)
def get_data(self):
self.get_tickets()
self.get_rows()
def get_data(self):
self.get_tickets()
self.get_rows()
def get_period(self, date):
months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
def get_period(self, date):
months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
if self.filters.range == "Weekly":
period = "Week " + str(date.isocalendar()[1])
elif self.filters.range == "Monthly":
period = str(months[date.month - 1])
elif self.filters.range == "Quarterly":
period = "Quarter " + str(((date.month - 1) // 3) + 1)
else:
year = get_fiscal_year()
period = str(year)
if self.filters.range == "Weekly":
period = "Week " + str(date.isocalendar()[1])
elif self.filters.range == "Monthly":
period = str(months[date.month - 1])
elif self.filters.range == "Quarterly":
period = "Quarter " + str(((date.month - 1) // 3) + 1)
else:
year = get_fiscal_year()
period = str(year)
if (
getdate(self.filters.from_date).year != getdate(self.filters.to_date).year
and self.filters.range != "Yearly"
):
period += " " + str(date.year)
if (
getdate(self.filters.from_date).year != getdate(self.filters.to_date).year
and self.filters.range != "Yearly"
):
period += " " + str(date.year)
return period
return period
def get_period_date_ranges(self):
from dateutil.relativedelta import MO, relativedelta
def get_period_date_ranges(self):
from dateutil.relativedelta import MO, relativedelta
from_date, to_date = getdate(self.filters.from_date), getdate(
self.filters.to_date
)
from_date, to_date = getdate(self.filters.from_date), getdate(
self.filters.to_date
)
increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(
self.filters.range, 1
)
increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(
self.filters.range, 1
)
if self.filters.range in ["Monthly", "Quarterly"]:
from_date = from_date.replace(day=1)
elif self.filters.range == "Yearly":
from_date = get_fiscal_year()
else:
from_date = from_date + relativedelta(from_date, weekday=MO(-1))
if self.filters.range in ["Monthly", "Quarterly"]:
from_date = from_date.replace(day=1)
elif self.filters.range == "Yearly":
from_date = get_fiscal_year()
else:
from_date = from_date + relativedelta(from_date, weekday=MO(-1))
self.periodic_daterange = []
for dummy in range(1, 53):
if self.filters.range == "Weekly":
period_end_date = add_days(from_date, 6)
else:
period_end_date = add_to_date(from_date, months=increment, days=-1)
if self.filters.range == "Yearly":
period_end_date = to_date
elif period_end_date > to_date:
period_end_date = to_date
self.periodic_daterange = []
for dummy in range(1, 53):
if self.filters.range == "Weekly":
period_end_date = add_days(from_date, 6)
else:
period_end_date = add_to_date(from_date, months=increment, days=-1)
if self.filters.range == "Yearly" or period_end_date > to_date:
period_end_date = to_date
self.periodic_daterange.append(period_end_date)
self.periodic_daterange.append(period_end_date)
from_date = add_days(period_end_date, 1)
if period_end_date == to_date:
break
from_date = add_days(period_end_date, 1)
if period_end_date == to_date:
break
def get_tickets(self):
filters = self.get_common_filters()
self.field_map = {
"Contact": "contact",
"Ticket Type": "ticket_type",
"Ticket Priority": "priority",
"Assigned To": "_assign",
}
def get_tickets(self):
filters = self.get_common_filters()
self.field_map = {
"Contact": "contact",
"Ticket Type": "ticket_type",
"Ticket Priority": "priority",
"Assigned To": "_assign",
}
self.entries = frappe.db.get_all(
"HD Ticket",
fields=[self.field_map.get(self.filters.based_on), "name", "opening_date"],
filters=filters,
)
self.entries = frappe.db.get_all(
"HD Ticket",
fields=[self.field_map.get(self.filters.based_on), "name", "opening_date"],
filters=filters,
)
def get_common_filters(self):
filters = {}
filters["opening_date"] = (
"between",
[self.filters.from_date, self.filters.to_date],
)
def get_common_filters(self):
filters = {}
filters["opening_date"] = (
"between",
[self.filters.from_date, self.filters.to_date],
)
if self.filters.get("assigned_to"):
filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%")
if self.filters.get("assigned_to"):
filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%")
for entry in ["status", "priority", "contact"]:
if self.filters.get(entry):
filters[entry] = self.filters.get(entry)
for entry in ["status", "priority", "contact"]:
if self.filters.get(entry):
filters[entry] = self.filters.get(entry)
return filters
return filters
def get_rows(self):
self.data = []
self.get_periodic_data()
def get_rows(self):
self.data = []
self.get_periodic_data()
for entity, period_data in iteritems(self.ticket_periodic_data):
if self.filters.based_on == "Contact":
row = {"contact": entity}
elif self.filters.based_on == "Assigned To":
row = {"user": entity}
elif self.filters.based_on == "Ticket Type":
row = {"ticket_type": entity}
elif self.filters.based_on == "Ticket Priority":
row = {"priority": entity}
for entity, period_data in iteritems(self.ticket_periodic_data):
if self.filters.based_on == "Contact":
row = {"contact": entity}
elif self.filters.based_on == "Assigned To":
row = {"user": entity}
elif self.filters.based_on == "Ticket Type":
row = {"ticket_type": entity}
elif self.filters.based_on == "Ticket Priority":
row = {"priority": entity}
total = 0
for end_date in self.periodic_daterange:
period = self.get_period(end_date)
amount = flt(period_data.get(period, 0.0))
row[scrub(period)] = amount
total += amount
total = 0
for end_date in self.periodic_daterange:
period = self.get_period(end_date)
amount = flt(period_data.get(period, 0.0))
row[scrub(period)] = amount
total += amount
row["total"] = total
row["total"] = total
self.data.append(row)
self.data.append(row)
def get_periodic_data(self):
self.ticket_periodic_data = frappe._dict()
def get_periodic_data(self):
self.ticket_periodic_data = frappe._dict()
for d in self.entries:
period = self.get_period(d.get("opening_date"))
for d in self.entries:
period = self.get_period(d.get("opening_date"))
if self.filters.based_on == "Assigned To":
if d._assign:
for entry in json.loads(d._assign):
self.ticket_periodic_data.setdefault(
entry, frappe._dict()
).setdefault(period, 0.0)
self.ticket_periodic_data[entry][period] += 1
if self.filters.based_on == "Assigned To":
if d._assign:
for entry in json.loads(d._assign):
self.ticket_periodic_data.setdefault(
entry, frappe._dict()
).setdefault(period, 0.0)
self.ticket_periodic_data[entry][period] += 1
else:
field = self.field_map.get(self.filters.based_on)
value = d.get(field)
if not value:
value = _("Not Specified")
else:
field = self.field_map.get(self.filters.based_on)
value = d.get(field)
if not value:
value = _("Not Specified")
self.ticket_periodic_data.setdefault(value, frappe._dict()).setdefault(
period, 0.0
)
self.ticket_periodic_data[value][period] += 1
self.ticket_periodic_data.setdefault(value, frappe._dict()).setdefault(
period, 0.0
)
self.ticket_periodic_data[value][period] += 1
def get_chart_data(self):
length = len(self.columns)
labels = [d.get("label") for d in self.columns[1 : length - 1]]
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
def get_chart_data(self):
length = len(self.columns)
labels = [d.get("label") for d in self.columns[1 : length - 1]]
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}

View File

@@ -3,63 +3,58 @@
/* eslint-disable */
frappe.query_reports["Ticket Summary"] = {
filters: [
{
fieldname: "based_on",
label: __("Based On"),
fieldtype: "Select",
options: [
"Contact",
"Ticket Type",
"Ticket Priority",
"Assigned To",
],
default: "Contact",
reqd: 1,
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.defaults.get_global_default("year_start_date"),
reqd: 1,
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.defaults.get_global_default("year_end_date"),
reqd: 1,
},
{
fieldname: "status",
label: __("Status"),
fieldtype: "Select",
options: [
"",
{ label: __("Open"), value: "Open" },
{ label: __("Replied"), value: "Replied" },
{ label: __("Resolved"), value: "Resolved" },
{ label: __("Closed"), value: "Closed" },
],
},
{
fieldname: "priority",
label: __("Ticket Priority"),
fieldtype: "Link",
options: "HD Ticket Priority",
},
{
fieldname: "contact",
label: __("Contact"),
fieldtype: "Link",
options: "Contact",
},
{
fieldname: "assigned_to",
label: __("Assigned To"),
fieldtype: "Link",
options: "User",
},
],
}
filters: [
{
fieldname: "based_on",
label: __("Based On"),
fieldtype: "Select",
options: ["Contact", "Ticket Type", "Ticket Priority", "Assigned To"],
default: "Contact",
reqd: 1,
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.defaults.get_global_default("year_start_date"),
reqd: 1,
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.defaults.get_global_default("year_end_date"),
reqd: 1,
},
{
fieldname: "status",
label: __("Status"),
fieldtype: "Select",
options: [
"",
{ label: __("Open"), value: "Open" },
{ label: __("Replied"), value: "Replied" },
{ label: __("Resolved"), value: "Resolved" },
{ label: __("Closed"), value: "Closed" },
],
},
{
fieldname: "priority",
label: __("Ticket Priority"),
fieldtype: "Link",
options: "HD Ticket Priority",
},
{
fieldname: "contact",
label: __("Contact"),
fieldtype: "Link",
options: "Contact",
},
{
fieldname: "assigned_to",
label: __("Assigned To"),
fieldtype: "Link",
options: "User",
},
],
};

View File

@@ -1,3 +1,3 @@
frappe.ready(function () {
// bind events here
})
// bind events here
});

View File

@@ -6,7 +6,7 @@ def execute():
old_app_name = "frappedesk"
new_app_name = "helpdesk"
installed_apps = frappe.db.get_global("installed_apps")
if old_app_name in installed_apps:
remove_from_installed_apps(old_app_name)
add_to_installed_apps(new_app_name)

View File

@@ -4,47 +4,47 @@ from frappe.utils import get_table_name
from helpdesk.utils import alphanumeric_to_int
DOCTYPES = [
"HD Preset Filter Item",
"HD Team Item",
"HD Team Member",
"HD Ticket",
"HD Ticket Custom Field Item",
"HD Preset Filter Item",
"HD Team Item",
"HD Team Member",
"HD Ticket",
"HD Ticket Custom Field Item",
]
MODULE = "Helpdesk"
SEQ_SUFFIX = "_id_seq"
def execute():
modify_table()
create_sequence()
modify_table()
create_sequence()
def modify_table():
for doctype in DOCTYPES:
table = f"`{get_table_name(doctype)}`"
frappe.db.sql_ddl(f"ALTER TABLE {table} MODIFY COLUMN name BIGINT(20)")
frappe.reload_doc(MODULE, "DocType", doctype)
for doctype in DOCTYPES:
table = f"`{get_table_name(doctype)}`"
frappe.db.sql_ddl(f"ALTER TABLE {table} MODIFY COLUMN name BIGINT(20)")
frappe.reload_doc(MODULE, "DocType", doctype)
def create_sequence():
for doctype in DOCTYPES:
sequence_name = frappe.scrub(doctype + SEQ_SUFFIX)
frappe.db.sql_ddl(f"DROP SEQUENCE IF EXISTS {sequence_name}")
start_value = sequence_start(doctype)
frappe.db.create_sequence(
doctype, check_not_exists=False, start_value=start_value
)
for doctype in DOCTYPES:
sequence_name = frappe.scrub(doctype + SEQ_SUFFIX)
frappe.db.sql_ddl(f"DROP SEQUENCE IF EXISTS {sequence_name}")
start_value = sequence_start(doctype)
frappe.db.create_sequence(
doctype, check_not_exists=False, start_value=start_value
)
def sequence_start(doctype: str):
try:
last_doc = frappe.get_last_doc(doctype, order_by="name desc")
last_id = last_doc.name
try:
last_doc = frappe.get_last_doc(doctype, order_by="name desc")
last_id = last_doc.name
if isinstance(last_id, int):
return last_id
if isinstance(last_id, int):
return last_id
last_id = alphanumeric_to_int(last_id) or 0
return last_id + 1
except Exception:
return 1
last_id = alphanumeric_to_int(last_id) or 0
return last_id + 1
except Exception:
return 1

View File

@@ -4,6 +4,8 @@ from frappe.query_builder import Case
def execute():
QBDocField = frappe.qb.DocType("HD Ticket Template DocField")
case = Case.any([QBDocField.fieldname == "Subject", QBDocField.fieldname == "Description"])
case = Case.any(
[QBDocField.fieldname == "Subject", QBDocField.fieldname == "Description"]
)
frappe.qb.from_(QBDocField).delete().where(case).run()
frappe.db.commit()

View File

@@ -1 +1 @@
import "./utils"
import "./utils";

View File

@@ -1,90 +1,86 @@
window.FileAttachmentHandler = class FileAttachmentHandler {
constructor() {
this.add_attachment_handler()
this.set_listeners()
}
constructor() {
this.add_attachment_handler();
this.set_listeners();
}
add_attachment_handler() {
var me = this
$(".add-attachment-helpdesk").click(function () {
me.new_attachment()
})
}
add_attachment_handler() {
var me = this;
$(".add-attachment-helpdesk").click(function () {
me.new_attachment();
});
}
new_attachment() {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
on_success: (file_doc) => {
if (!this.attachments) this.attachments = []
if (!this.save_paths) this.save_paths = {}
this.attachments.push(file_doc)
$(".helpdesk-attachment")
.empty()
.append(this.build_attachment_table())
// $(".attachment-controls").find(".number").text(this.attachments.length);
},
})
}
new_attachment() {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
on_success: (file_doc) => {
if (!this.attachments) this.attachments = [];
if (!this.save_paths) this.save_paths = {};
this.attachments.push(file_doc);
$(".helpdesk-attachment").empty().append(this.build_attachment_table());
// $(".attachment-controls").find(".number").text(this.attachments.length);
},
});
}
build_attachment_table() {
var wrapper = $('<div class="helpdesk-attachment"></div>')
wrapper.empty()
build_attachment_table() {
var wrapper = $('<div class="helpdesk-attachment"></div>');
wrapper.empty();
var table = $(this.get_attachment_table_header_html()).appendTo(wrapper)
if (!this.attachments || !this.attachments.length)
return "No attachments uploaded"
var table = $(this.get_attachment_table_header_html()).appendTo(wrapper);
if (!this.attachments || !this.attachments.length)
return "No attachments uploaded";
this.attachments.forEach((f) => {
const row = $("<tr></tr>").appendTo(table.find("tbody"))
$(`<td>${f.file_name}</td>`).appendTo(row)
$(`<td>
this.attachments.forEach((f) => {
const row = $("<tr></tr>").appendTo(table.find("tbody"));
$(`<td>${f.file_name}</td>`).appendTo(row);
$(`<td>
<a class="btn btn-default btn-xs btn-primary-light text-nowrap copy-link" data-link="![](${f.file_url})" data-name = "${f.file_name}" >
Copy Link
</a>
</td>`).appendTo(row)
$(`<td>
</td>`).appendTo(row);
$(`<td>
<a class="btn btn-default btn-xs center delete-button" data-name = "${f.file_name}" >
<svg class="icon icon-sm"><use xlink:href="#icon-delete"></use></svg>
</a>
</td>`).appendTo(row)
})
return wrapper
}
</td>`).appendTo(row);
});
return wrapper;
}
get_attachment_table_header_html() {
return `<table class="table attachment-table" ">
get_attachment_table_header_html() {
return `<table class="table attachment-table" ">
<tbody></tbody>
</table>`
}
</table>`;
}
handle_attachment_upload_callback(method) {
if (this.attachments) {
}
}
handle_attachment_upload_callback(method) {
if (this.attachments) {
}
}
set_listeners() {
var me = this
set_listeners() {
var me = this;
$(`body`).on("click", `.copy-link`, function () {
frappe.utils.copy_to_clipboard($(this).attr("data-link"))
})
$(`body`).on("click", `.copy-link`, function () {
frappe.utils.copy_to_clipboard($(this).attr("data-link"));
});
$(`body`).on("click", `.delete-button`, function () {
frappe.confirm(
`Are you sure you want to delete the file "${$(this).attr(
"data-name"
)}"`,
() => {
me.attachments.forEach((f, index, object) => {
if (f.file_name == $(this).attr("data-name")) {
object.splice(index, 1)
}
})
$(".helpdesk-attachment")
.empty()
.append(me.build_attachment_table())
}
)
})
}
}
$(`body`).on("click", `.delete-button`, function () {
frappe.confirm(
`Are you sure you want to delete the file "${$(this).attr(
"data-name"
)}"`,
() => {
me.attachments.forEach((f, index, object) => {
if (f.file_name == $(this).attr("data-name")) {
object.splice(index, 1);
}
});
$(".helpdesk-attachment").empty().append(me.build_attachment_table());
}
);
});
}
};

View File

@@ -3,344 +3,346 @@
from __future__ import unicode_literals
from copy import deepcopy
import json
import re
from copy import deepcopy
import frappe
from frappe.utils.synchronization import filelock
from bs4 import BeautifulSoup, PageElement
from frappe.utils import cstr, strip_html_tags, update_progress_bar
from frappe.utils.synchronization import filelock
from redis.commands.search.field import TagField, TextField
from redis.commands.search.indexDefinition import IndexDefinition
from redis.commands.search.query import Query
from redis.exceptions import ResponseError
from bs4 import BeautifulSoup, PageElement
from helpdesk.utils import is_agent
STOPWORDS = [
"a",
"is",
"the",
"an",
"and",
"are",
"as",
"at",
"be",
"but",
"by",
"for",
"if",
"in",
"into",
"it",
"no",
"not",
"of",
"on",
"or",
"such",
"that",
"their",
"then",
"there",
"these",
"they",
"this",
"to",
"was",
"will",
"with",
"how",
"what",
"where",
"when",
"i",
"you",
"me",
"do",
"a",
"is",
"the",
"an",
"and",
"are",
"as",
"at",
"be",
"but",
"by",
"for",
"if",
"in",
"into",
"it",
"no",
"not",
"of",
"on",
"or",
"such",
"that",
"their",
"then",
"there",
"these",
"they",
"this",
"to",
"was",
"will",
"with",
"how",
"what",
"where",
"when",
"i",
"you",
"me",
"do",
]
class Search:
unsafe_chars = re.compile(r"[\[\]{}<>+]")
unsafe_chars = re.compile(r"[\[\]{}<>+]")
def __init__(self, index_name, prefix, schema) -> None:
self.redis = frappe.cache()
self.index_name = index_name
self.prefix = prefix
self.schema = []
for field in schema:
self.schema.append(frappe._dict(field))
def __init__(self, index_name, prefix, schema) -> None:
self.redis = frappe.cache()
self.index_name = index_name
self.prefix = prefix
self.schema = []
for field in schema:
self.schema.append(frappe._dict(field))
def create_index(self):
index_def = IndexDefinition(
prefix=[f"{self.redis.make_key(self.prefix).decode()}:"],
)
schema = []
for field in self.schema:
kwargs = {
k: v
for k, v in field.items()
if k in ["weight", "sortable", "no_index", "no_stem"]
}
if field.type == "tag":
schema.append(TagField(field.name, **kwargs))
else:
schema.append(TextField(field.name, **kwargs))
def create_index(self):
index_def = IndexDefinition(
prefix=[f"{self.redis.make_key(self.prefix).decode()}:"],
)
schema = []
for field in self.schema:
kwargs = {
k: v
for k, v in field.items()
if k in ["weight", "sortable", "no_index", "no_stem"]
}
if field.type == "tag":
schema.append(TagField(field.name, **kwargs))
else:
schema.append(TextField(field.name, **kwargs))
self.redis.ft(self.index_name).create_index(
schema,
definition=index_def,
stopwords=STOPWORDS,
)
self._index_exists = True
self.redis.ft(self.index_name).create_index(
schema,
definition=index_def,
stopwords=STOPWORDS,
)
self._index_exists = True
def add_document(self, id, doc):
doc = frappe._dict(doc)
doc_id = self.redis.make_key(f"{self.prefix}:{id}").decode()
mapping = {}
for field in self.schema:
if field.name in doc:
mapping[field.name] = cstr(doc[field.name])
if self.index_exists():
self.redis.ft(self.index_name).add_document(doc_id, replace=True, **mapping)
def add_document(self, id, doc):
doc = frappe._dict(doc)
doc_id = self.redis.make_key(f"{self.prefix}:{id}").decode()
mapping = {}
for field in self.schema:
if field.name in doc:
mapping[field.name] = cstr(doc[field.name])
if self.index_exists():
self.redis.ft(self.index_name).add_document(doc_id, replace=True, **mapping)
def remove_document(self, id):
key = self.redis.make_key(f"{self.prefix}:{id}").decode()
if self.index_exists():
self.redis.ft(self.index_name).delete_document(key)
def remove_document(self, id):
key = self.redis.make_key(f"{self.prefix}:{id}").decode()
if self.index_exists():
self.redis.ft(self.index_name).delete_document(key)
def search(
self,
query,
start=0,
page_length=5,
highlight=False,
):
query = self.clean_query(query)
query = Query(query).paging(start, page_length)
if highlight:
query = query.highlight(tags=["<mark>", "</mark>"])
def search(
self,
query,
start=0,
page_length=5,
highlight=False,
):
query = self.clean_query(query)
query = Query(query).paging(start, page_length)
if highlight:
query = query.highlight(tags=["<mark>", "</mark>"])
query.summarize(fields=["description"])
query.scorer("DISMAX")
query.summarize(fields=["description"])
query.scorer("DISMAX")
try:
result = self.redis.ft(self.index_name).search(query)
except ResponseError as e:
print(e)
return frappe._dict({"total": 0, "docs": [], "duration": 0})
try:
result = self.redis.ft(self.index_name).search(query)
except ResponseError as e:
print(e)
return frappe._dict({"total": 0, "docs": [], "duration": 0})
out = frappe._dict(docs=[], total=result.total, duration=result.duration)
for doc in result.docs:
id = doc.id.split(":", 1)[1]
_doc = frappe._dict(doc.__dict__)
_doc.id = id
_doc.payload = json.loads(doc.payload) if doc.payload else None
out.docs.append(_doc)
return out
out = frappe._dict(docs=[], total=result.total, duration=result.duration)
for doc in result.docs:
id = doc.id.split(":", 1)[1]
_doc = frappe._dict(doc.__dict__)
_doc.id = id
_doc.payload = json.loads(doc.payload) if doc.payload else None
out.docs.append(_doc)
return out
def clean_query(self, query):
query = query.strip().replace("-*", "*")
query = self.unsafe_chars.sub(" ", query)
query.strip()
return query
def clean_query(self, query):
query = query.strip().replace("-*", "*")
query = self.unsafe_chars.sub(" ", query)
query.strip()
return query
def spellcheck(self, query, **kwargs):
return self.redis.ft(self.index_name).spellcheck(query, **kwargs)
def spellcheck(self, query, **kwargs):
return self.redis.ft(self.index_name).spellcheck(query, **kwargs)
def drop_index(self):
if self.index_exists():
self.redis.ft(self.index_name).dropindex(delete_documents=True)
def drop_index(self):
if self.index_exists():
self.redis.ft(self.index_name).dropindex(delete_documents=True)
def index_exists(self):
self._index_exists = getattr(self, "_index_exists", None)
if self._index_exists is None:
try:
self.redis.ft(self.index_name).info()
self._index_exists = True
except ResponseError:
self._index_exists = False
return self._index_exists
def index_exists(self):
self._index_exists = getattr(self, "_index_exists", None)
if self._index_exists is None:
try:
self.redis.ft(self.index_name).info()
self._index_exists = True
except ResponseError:
self._index_exists = False
return self._index_exists
class HelpdeskSearch(Search):
schema = [
{"name": "name", "weight": 2},
{"name": "subject", "weight": 6},
{"name": "description", "weight": 6},
{"name": "headings", "weight": 8},
{"name": "team", "type": "tag"},
{"name": "modified", "sortable": True},
{"name": "creation", "sortable": True},
]
schema = [
{"name": "name", "weight": 2},
{"name": "subject", "weight": 6},
{"name": "description", "weight": 6},
{"name": "headings", "weight": 8},
{"name": "team", "type": "tag"},
{"name": "modified", "sortable": True},
{"name": "creation", "sortable": True},
]
DOCTYPE_FIELDS = {
"HD Ticket": [
"name",
"subject",
"description",
"agent_group",
"modified",
"creation",
],
"HD Article": [
"name",
"category",
"title",
"content",
"modified",
"creation",
"category.category_name as category",
],
}
DOCTYPE_FIELDS = {
"HD Ticket": [
"name",
"subject",
"description",
"agent_group",
"modified",
"creation",
],
"HD Article": [
"name",
"category",
"title",
"content",
"modified",
"creation",
"category.category_name as category",
],
}
def __init__(self):
super().__init__("helpdesk_idx", "search_doc", self.schema)
def __init__(self):
super().__init__("helpdesk_idx", "search_doc", self.schema)
def build_index(self):
self.drop_index()
self.create_index()
records = self.get_records("HD Ticket") + self.get_records("HD Article")
total = len(records)
for i, doc in enumerate(records):
self.index_doc(doc)
if not hasattr(frappe.local, "request"):
update_progress_bar("Indexing", i, total)
def build_index(self):
self.drop_index()
self.create_index()
records = self.get_records("HD Ticket") + self.get_records("HD Article")
total = len(records)
for i, doc in enumerate(records):
self.index_doc(doc)
if not hasattr(frappe.local, "request"):
update_progress_bar("Indexing", i, total)
def index_doc(self, doc):
id = f"{doc.doctype}:{doc.name}"
fields = None
if doc.doctype == "HD Ticket":
fields = {
"doctype": doc.doctype,
"name": doc.name,
"subject": doc.subject,
"team": doc.agent_group,
"modified": doc.modified,
}
if doc.doctype == "HD Article":
fields = {
"doctype": doc.doctype,
"name": doc.name,
"subject": doc.title,
"description": strip_html_tags(doc.content),
"headings": doc.headings,
"modified": doc.modified,
}
if fields:
self.add_document(id, fields)
def index_doc(self, doc):
id = f"{doc.doctype}:{doc.name}"
fields = None
if doc.doctype == "HD Ticket":
fields = {
"doctype": doc.doctype,
"name": doc.name,
"subject": doc.subject,
"team": doc.agent_group,
"modified": doc.modified,
}
if doc.doctype == "HD Article":
fields = {
"doctype": doc.doctype,
"name": doc.name,
"subject": doc.title,
"description": strip_html_tags(doc.content),
"headings": doc.headings,
"modified": doc.modified,
}
if fields:
self.add_document(id, fields)
def remove_doc(self, doc):
key = f"{doc.doctype}:{doc.name}"
self.remove_document(key)
def remove_doc(self, doc):
key = f"{doc.doctype}:{doc.name}"
self.remove_document(key)
def extract_headings(self, content: str | None) -> str:
try:
soup = BeautifulSoup(content, "html.parser")
except TypeError:
ret = []
else:
ret = []
for tag in ["h1", "h2", "h3", "h4", "h5", "h6"]:
for heading in soup.find_all(tag):
ret.append(heading.text)
return json.dumps(ret)
def extract_headings(self, content: str | None) -> str:
try:
soup = BeautifulSoup(content, "html.parser")
except TypeError:
ret = []
else:
ret = []
for tag in ["h1", "h2", "h3", "h4", "h5", "h6"]:
for heading in soup.find_all(tag):
ret.append(heading.text)
return json.dumps(ret)
def get_sections(self, content: str) -> list[tuple[str, str]]:
try:
soup = BeautifulSoup(content, "html.parser")
except TypeError:
return []
else:
sections = []
tag: PageElement
section = ""
heading = ""
for tag in soup.find_all():
if tag.name in ["p", "blockquote", "code"]:
section += tag.text + "\n"
elif tag.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
sections.append((heading, section))
section = ""
heading = tag.text
sections.append((heading, section))
return sections
def get_sections(self, content: str) -> list[tuple[str, str]]:
try:
soup = BeautifulSoup(content, "html.parser")
except TypeError:
return []
else:
sections = []
tag: PageElement
section = ""
heading = ""
for tag in soup.find_all():
if tag.name in ["p", "blockquote", "code"]:
section += tag.text + "\n"
elif tag.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
sections.append((heading, section))
section = ""
heading = tag.text
sections.append((heading, section))
return sections
def scrub(self, text: str):
# For permalink
return re.sub(r"[^a-zA-Z0-9]+", "-", text).lower()
def scrub(self, text: str):
# For permalink
return re.sub(r"[^a-zA-Z0-9]+", "-", text).lower()
def get_records(self, doctype):
records = []
for d in frappe.db.get_all(doctype, fields=self.DOCTYPE_FIELDS[doctype]):
d.doctype = doctype
if doctype == "HD Article":
for heading, section in self.get_sections(d.content):
cd = deepcopy(d)
cd.name = d.name + f"#{self.scrub(heading)}"
cd.content = section
cd.headings = heading
records.append(cd)
elif doctype == "HD Ticket":
d.headings = self.extract_headings(d.description)
records.append(d)
return records
def get_records(self, doctype):
records = []
for d in frappe.db.get_all(doctype, fields=self.DOCTYPE_FIELDS[doctype]):
d.doctype = doctype
if doctype == "HD Article":
for heading, section in self.get_sections(d.content):
cd = deepcopy(d)
cd.name = d.name + f"#{self.scrub(heading)}"
cd.content = section
cd.headings = heading
records.append(cd)
elif doctype == "HD Ticket":
d.headings = self.extract_headings(d.description)
records.append(d)
return records
@frappe.whitelist()
def search(query, only_articles=False):
search = HelpdeskSearch()
query = search.clean_query(query)
query_parts = query.split(" ")
query = " ".join(
[f"%{q}%" for q in query_parts if q not in STOPWORDS]
) # for stopwords to be ignored
result = search.search(query, start=0, highlight=True)
groups = {}
for r in result.docs:
doctype, name = r.id.split(":")
r.doctype = doctype
r.name = name
if doctype == "HD Ticket" and not only_articles:
if not is_agent():
r = []
groups.setdefault("Tickets", []).append(r)
if doctype == "HD Article":
groups.setdefault("Articles", []).append(r)
search = HelpdeskSearch()
query = search.clean_query(query)
query_parts = query.split(" ")
query = " ".join(
[f"%{q}%" for q in query_parts if q not in STOPWORDS]
) # for stopwords to be ignored
result = search.search(query, start=0, highlight=True)
groups = {}
for r in result.docs:
doctype, name = r.id.split(":")
r.doctype = doctype
r.name = name
if doctype == "HD Ticket" and not only_articles:
if not is_agent():
r = []
groups.setdefault("Tickets", []).append(r)
if doctype == "HD Article":
groups.setdefault("Articles", []).append(r)
out = []
for key in groups:
out.append({"title": key, "items": groups[key]})
return out
out = []
for key in groups:
out.append({"title": key, "items": groups[key]})
return out
@filelock("helpdesk_search_indexing", timeout=60)
def build_index():
frappe.cache().set_value("helpdesk_search_indexing_in_progress", True)
search = HelpdeskSearch()
search.build_index()
frappe.cache().set_value("helpdesk_search_indexing_in_progress", False)
frappe.cache().set_value("helpdesk_search_indexing_in_progress", True)
search = HelpdeskSearch()
search.build_index()
frappe.cache().set_value("helpdesk_search_indexing_in_progress", False)
def build_index_in_background():
if not frappe.cache().get_value("helpdesk_search_indexing_in_progress"):
frappe.enqueue(build_index, queue="long")
if not frappe.cache().get_value("helpdesk_search_indexing_in_progress"):
frappe.enqueue(build_index, queue="long")
def build_index_if_not_exists():
search = HelpdeskSearch()
if not search.index_exists():
build_index()
search = HelpdeskSearch()
if not search.index_exists():
build_index()
@filelock("helpdesk_corpus_download", timeout=60)
def download_corpus():
from nltk import download, data
try:
data.find("taggers/averaged_perceptron_tagger_eng.zip")
data.find("tokenizers/punkt_tab.zip")
except LookupError:
download("averaged_perceptron_tagger_eng")
download("punkt_tab")
from nltk import data, download
try:
data.find("taggers/averaged_perceptron_tagger_eng.zip")
data.find("tokenizers/punkt_tab.zip")
except LookupError:
download("averaged_perceptron_tagger_eng")
download("punkt_tab")

View File

@@ -11,277 +11,278 @@ from .welcome_ticket import create_welcome_ticket
def before_install():
add_support_redirect_to_tickets()
add_support_redirect_to_tickets()
def after_install():
add_default_categories_and_articles()
add_default_ticket_priorities()
add_default_sla()
add_default_agent_groups()
update_agent_role_permissions()
add_default_assignment_rule()
add_system_preset_filters()
create_default_template()
create_fallback_ticket_type()
create_helpdesk_folder()
create_ootb_ticket_types()
create_welcome_ticket()
create_ticket_feedback_options()
add_property_setter()
add_default_categories_and_articles()
add_default_ticket_priorities()
add_default_sla()
add_default_agent_groups()
update_agent_role_permissions()
add_default_assignment_rule()
add_system_preset_filters()
create_default_template()
create_fallback_ticket_type()
create_helpdesk_folder()
create_ootb_ticket_types()
create_welcome_ticket()
create_ticket_feedback_options()
add_property_setter()
def add_support_redirect_to_tickets():
website_settings = frappe.get_doc("Website Settings")
website_settings = frappe.get_doc("Website Settings")
for route_redirects in website_settings.route_redirects:
if route_redirects.source == "support":
return
for route_redirects in website_settings.route_redirects:
if route_redirects.source == "support":
return
website_settings.append(
"route_redirects",
{
"source": "support",
"target": "support/tickets",
"redirect_http_status": 301,
},
)
website_settings.save()
website_settings.append(
"route_redirects",
{
"source": "support",
"target": "support/tickets",
"redirect_http_status": 301,
},
)
website_settings.save()
def add_default_categories_and_articles():
category = frappe.get_doc(
{
"doctype": "HD Article Category",
"category_name": "Getting Started",
"description": "Content for your Category",
}
).insert()
category = frappe.get_doc(
{
"doctype": "HD Article Category",
"category_name": "Getting Started",
"description": "Content for your Category",
}
).insert()
frappe.get_doc(
{
"doctype": "HD Article",
"title": "Introduction",
"content": "Content for your Article",
"category": category.name,
"published": False,
}
).insert()
frappe.get_doc(
{
"doctype": "HD Article",
"title": "Introduction",
"content": "Content for your Article",
"category": category.name,
"published": False,
}
).insert()
def add_default_sla():
add_default_ticket_priorities()
add_default_holidy_list()
enable_track_service_level_agreement_in_support_settings()
add_default_ticket_priorities()
add_default_holidy_list()
enable_track_service_level_agreement_in_support_settings()
sla_doc = frappe.new_doc("HD Service Level Agreement")
sla_doc = frappe.new_doc("HD Service Level Agreement")
sla_doc.service_level = "Default"
sla_doc.document_type = "HD Ticket"
sla_doc.default_sla = 1
sla_doc.enabled = 1
sla_doc.service_level = "Default"
sla_doc.document_type = "HD Ticket"
sla_doc.default_sla = 1
sla_doc.enabled = 1
low_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 0,
"priority": "Low",
"response_time": 60 * 60 * 24,
"resolution_time": 60 * 60 * 72,
}
)
low_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 0,
"priority": "Low",
"response_time": 60 * 60 * 24,
"resolution_time": 60 * 60 * 72,
}
)
medium_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 1,
"priority": "Medium",
"response_time": 60 * 60 * 8,
"resolution_time": 60 * 60 * 24,
}
)
medium_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 1,
"priority": "Medium",
"response_time": 60 * 60 * 8,
"resolution_time": 60 * 60 * 24,
}
)
high_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 0,
"priority": "High",
"response_time": 60 * 60 * 1,
"resolution_time": 60 * 60 * 4,
}
)
high_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 0,
"priority": "High",
"response_time": 60 * 60 * 1,
"resolution_time": 60 * 60 * 4,
}
)
urgent_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 0,
"priority": "Urgent",
"response_time": 60 * 30,
"resolution_time": 60 * 60 * 2,
}
)
urgent_priority = frappe.get_doc(
{
"doctype": "HD Service Level Priority",
"default_priority": 0,
"priority": "Urgent",
"response_time": 60 * 30,
"resolution_time": 60 * 60 * 2,
}
)
sla_doc.append("priorities", low_priority)
sla_doc.append("priorities", medium_priority)
sla_doc.append("priorities", high_priority)
sla_doc.append("priorities", urgent_priority)
sla_doc.append("priorities", low_priority)
sla_doc.append("priorities", medium_priority)
sla_doc.append("priorities", high_priority)
sla_doc.append("priorities", urgent_priority)
sla_fullfilled_on_resolved = frappe.get_doc(
{
"doctype": "HD Service Level Agreement Fulfilled On Status",
"status": "Resolved",
}
)
sla_fullfilled_on_resolved = frappe.get_doc(
{
"doctype": "HD Service Level Agreement Fulfilled On Status",
"status": "Resolved",
}
)
sla_fullfilled_on_closed = frappe.get_doc(
{
"doctype": "HD Service Level Agreement Fulfilled On Status",
"status": "Closed",
}
)
sla_fullfilled_on_closed = frappe.get_doc(
{
"doctype": "HD Service Level Agreement Fulfilled On Status",
"status": "Closed",
}
)
sla_doc.append("sla_fulfilled_on", sla_fullfilled_on_resolved)
sla_doc.append("sla_fulfilled_on", sla_fullfilled_on_closed)
sla_doc.append("sla_fulfilled_on", sla_fullfilled_on_resolved)
sla_doc.append("sla_fulfilled_on", sla_fullfilled_on_closed)
sla_paused_on_replied = frappe.get_doc(
{"doctype": "HD Pause Service Level Agreement On Status", "status": "Replied"}
)
sla_paused_on_replied = frappe.get_doc(
{"doctype": "HD Pause Service Level Agreement On Status", "status": "Replied"}
)
sla_doc.append("pause_sla_on", sla_paused_on_replied)
sla_doc.append("pause_sla_on", sla_paused_on_replied)
sla_doc.holiday_list = "Default"
sla_doc.holiday_list = "Default"
for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]:
service_day = frappe.get_doc(
{
"doctype": "HD Service Day",
"workday": day,
"start_time": "10:00:00",
"end_time": "18:00:00",
}
)
sla_doc.append("support_and_resolution", service_day)
for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]:
service_day = frappe.get_doc(
{
"doctype": "HD Service Day",
"workday": day,
"start_time": "10:00:00",
"end_time": "18:00:00",
}
)
sla_doc.append("support_and_resolution", service_day)
sla_doc.insert()
sla_doc.insert()
def add_default_holidy_list():
frappe.get_doc(
{
"doctype": "HD Service Holiday List",
"holiday_list_name": "Default",
"from_date": datetime.strptime(f"Jan 1 {datetime.now().year}", "%b %d %Y"),
"to_date": datetime.strptime(
f"Jan 1 {datetime.now().year + 1}", "%b %d %Y"
),
}
).insert()
frappe.get_doc(
{
"doctype": "HD Service Holiday List",
"holiday_list_name": "Default",
"from_date": datetime.strptime(f"Jan 1 {datetime.now().year}", "%b %d %Y"),
"to_date": datetime.strptime(
f"Jan 1 {datetime.now().year + 1}", "%b %d %Y"
),
}
).insert()
frappe.db.commit()
frappe.db.commit()
def enable_track_service_level_agreement_in_support_settings():
support_settings = frappe.get_doc("HD Settings")
support_settings.track_service_level_agreement = True
support_settings.save()
frappe.db.commit()
support_settings = frappe.get_doc("HD Settings")
support_settings.track_service_level_agreement = True
support_settings.save()
frappe.db.commit()
def add_default_ticket_priorities():
ticket_priorities = {
"Urgent": 100,
"High": 200,
"Medium": 300,
"Low": 400,
}
ticket_priorities = {
"Urgent": 100,
"High": 200,
"Medium": 300,
"Low": 400,
}
for priority in ticket_priorities:
if frappe.db.exists("HD Ticket Priority", priority):
continue
for priority in ticket_priorities:
if frappe.db.exists("HD Ticket Priority", priority):
continue
doc = frappe.new_doc("HD Ticket Priority")
doc.name = priority
doc.integer_value = ticket_priorities[priority]
doc.insert()
doc = frappe.new_doc("HD Ticket Priority")
doc.name = priority
doc.integer_value = ticket_priorities[priority]
doc.insert()
def add_default_agent_groups():
agent_groups = ["Billing", "Product Experts"]
agent_groups = ["Billing", "Product Experts"]
for agent_group in agent_groups:
if not frappe.db.exists("HD Team", agent_group):
agent_group_doc = frappe.new_doc("HD Team")
agent_group_doc.team_name = agent_group
agent_group_doc.insert()
for agent_group in agent_groups:
if not frappe.db.exists("HD Team", agent_group):
agent_group_doc = frappe.new_doc("HD Team")
agent_group_doc.team_name = agent_group
agent_group_doc.insert()
def update_agent_role_permissions():
if frappe.db.exists("Role", "Agent"):
agent_role_doc = frappe.get_doc("Role", "Agent")
agent_role_doc.search_bar = True
agent_role_doc.notifications = True
agent_role_doc.list_sidebar = True
agent_role_doc.bulk_actions = True
agent_role_doc.view_switcher = True
agent_role_doc.form_sidebar = True
agent_role_doc.form_sidebar = True
agent_role_doc.timeline = True
agent_role_doc.dashboard = True
agent_role_doc.save()
if frappe.db.exists("Role", "Agent"):
agent_role_doc = frappe.get_doc("Role", "Agent")
agent_role_doc.search_bar = True
agent_role_doc.notifications = True
agent_role_doc.list_sidebar = True
agent_role_doc.bulk_actions = True
agent_role_doc.view_switcher = True
agent_role_doc.form_sidebar = True
agent_role_doc.form_sidebar = True
agent_role_doc.timeline = True
agent_role_doc.dashboard = True
agent_role_doc.save()
add_permission("File", "Agent", 0)
add_permission("Contact", "Agent", 0)
add_permission("Email Account", "Agent", 0)
add_permission("File", "Agent", 0)
add_permission("Contact", "Agent", 0)
add_permission("Email Account", "Agent", 0)
def add_default_assignment_rule():
support_settings = frappe.get_doc("HD Settings")
support_settings.create_base_support_rotation()
support_settings = frappe.get_doc("HD Settings")
support_settings.create_base_support_rotation()
def add_system_preset_filters():
preset_filters = []
for status in ["Closed", "Resolved", "Replied", "Open"]:
preset_filters.append(
{
"doctype": "HD Preset Filter",
"title": f"My {status} Tickets",
"reference_doctype": "HD Ticket",
"filters": [
{
"label": "Assigned To",
"fieldname": "_assign",
"filter_type": "is",
"value": "@me",
},
{
"label": "Status",
"fieldname": "status",
"filter_type": "is",
"value": status,
},
],
}
)
preset_filters.append(
{
"doctype": "HD Preset Filter",
"title": "All Tickets",
"reference_doctype": "HD Ticket",
"filters": [],
}
)
for preset in preset_filters:
preset_filter_doc = frappe.get_doc(preset)
preset_filter_doc.insert()
preset_filters = []
for status in ["Closed", "Resolved", "Replied", "Open"]:
preset_filters.append(
{
"doctype": "HD Preset Filter",
"title": f"My {status} Tickets",
"reference_doctype": "HD Ticket",
"filters": [
{
"label": "Assigned To",
"fieldname": "_assign",
"filter_type": "is",
"value": "@me",
},
{
"label": "Status",
"fieldname": "status",
"filter_type": "is",
"value": status,
},
],
}
)
preset_filters.append(
{
"doctype": "HD Preset Filter",
"title": "All Tickets",
"reference_doctype": "HD Ticket",
"filters": [],
}
)
for preset in preset_filters:
preset_filter_doc = frappe.get_doc(preset)
preset_filter_doc.insert()
def add_property_setter():
if not frappe.db.exists("Property Setter", {"name": "Contact-main-search_fields"}):
doc = frappe.new_doc("Property Setter")
doc.doctype_or_field = "DocType"
doc.doc_type = "Contact"
doc.property = "search_fields"
doc.property_type = "Data"
doc.value = "email_id"
doc.insert()
if not frappe.db.exists("Property Setter", {"name": "Contact-main-search_fields"}):
doc = frappe.new_doc("Property Setter")
doc.doctype_or_field = "DocType"
doc.doc_type = "Contact"
doc.property = "search_fields"
doc.property_type = "Data"
doc.value = "email_id"
doc.insert()