fix(formatting): run new pre commit on the codebase
This commit is contained in:
37
.flake8
Normal file
37
.flake8
Normal 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
73
.github/helpers/.flake8_strict
vendored
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -113,7 +113,7 @@ function finish() {
|
||||
}
|
||||
|
||||
function redirect() {
|
||||
router.replace({ path: '/' });
|
||||
router.replace({ path: "/" });
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Agent", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Article Category", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Article Feedback", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Canned Response", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Customer", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Notification", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Organization", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Preset Filter", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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."
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
],
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Settings", {
|
||||
refresh: function (frm) {
|
||||
//
|
||||
},
|
||||
})
|
||||
refresh: function (frm) {
|
||||
//
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Team", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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
@@ -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_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_agent_flow(self):
|
||||
ticket = frappe.get_doc(get_ticket_obj())
|
||||
ticket.insert()
|
||||
|
||||
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.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()
|
||||
ticket.status = "Replied"
|
||||
ticket.save()
|
||||
|
||||
ticket.status = "Open"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
ticket.status = "Open"
|
||||
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)
|
||||
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 = "Resolved"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
ticket.status = "Resolved"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
|
||||
ticket.status = "Closed"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
ticket.status = "Closed"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
|
||||
def test_non_agent_flow(self):
|
||||
ticket = frappe.get_doc(get_ticket_obj())
|
||||
ticket.insert()
|
||||
def test_non_agent_flow(self):
|
||||
ticket = frappe.get_doc(get_ticket_obj())
|
||||
ticket.insert()
|
||||
|
||||
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.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 = "Replied"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
|
||||
ticket.status = "Open"
|
||||
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)
|
||||
ticket.status = "Resolved"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
|
||||
ticket.status = "Closed"
|
||||
ticket.save()
|
||||
self.assertTrue(ticket)
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Ticket Activity", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Ticket Comment", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Ticket Priority", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
})
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Ticket Template", {
|
||||
setup: function (frm) {},
|
||||
})
|
||||
setup: function (frm) {},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("HD Ticket Type", {
|
||||
refresh: function (frm) {},
|
||||
})
|
||||
refresh: function (frm) {},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
frappe.ready(function () {
|
||||
// bind events here
|
||||
})
|
||||
// bind events here
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1 +1 @@
|
||||
import "./utils"
|
||||
import "./utils";
|
||||
|
||||
@@ -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="" 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());
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user