Merge pull request #984 from shariquerik/lost-reasons
This commit is contained in:
commit
a3a54aef94
@ -14,6 +14,8 @@
|
|||||||
"column_break_ijan",
|
"column_break_ijan",
|
||||||
"status",
|
"status",
|
||||||
"deal_owner",
|
"deal_owner",
|
||||||
|
"lost_reason",
|
||||||
|
"lost_notes",
|
||||||
"section_break_jgpm",
|
"section_break_jgpm",
|
||||||
"probability",
|
"probability",
|
||||||
"deal_value",
|
"deal_value",
|
||||||
@ -391,12 +393,25 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_kpxa",
|
"fieldname": "column_break_kpxa",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "lost_reason",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Lost Reason",
|
||||||
|
"mandatory_depends_on": "eval: doc.status == \"Lost\"",
|
||||||
|
"options": "CRM Lost Reason"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "lost_notes",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"label": "Lost Notes",
|
||||||
|
"mandatory_depends_on": "eval: doc.lost_reason == \"Other\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-16 11:42:49.413483",
|
"modified": "2025-07-02 11:07:50.192089",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class CRMDeal(Document):
|
|||||||
if self.has_value_changed("status"):
|
if self.has_value_changed("status"):
|
||||||
add_status_change_log(self)
|
add_status_change_log(self)
|
||||||
self.validate_forcasting_fields()
|
self.validate_forcasting_fields()
|
||||||
|
self.validate_lost_reason()
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
if self.deal_owner:
|
if self.deal_owner:
|
||||||
@ -141,14 +142,32 @@ class CRMDeal(Document):
|
|||||||
if self.status == "Won" and not self.close_date:
|
if self.status == "Won" and not self.close_date:
|
||||||
self.close_date = frappe.utils.nowdate()
|
self.close_date = frappe.utils.nowdate()
|
||||||
|
|
||||||
|
def update_default_probability(self):
|
||||||
|
"""
|
||||||
|
Update the default probability based on the status.
|
||||||
|
"""
|
||||||
|
if not self.probability or self.probability == 0:
|
||||||
|
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
|
||||||
|
|
||||||
def validate_forcasting_fields(self):
|
def validate_forcasting_fields(self):
|
||||||
self.update_close_date()
|
self.update_close_date()
|
||||||
|
self.update_default_probability()
|
||||||
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
||||||
if not self.deal_value or self.deal_value == 0:
|
if not self.deal_value or self.deal_value == 0:
|
||||||
frappe.throw(_("Deal Value is required."), frappe.MandatoryError)
|
frappe.throw(_("Deal Value is required."), frappe.MandatoryError)
|
||||||
if not self.close_date:
|
if not self.close_date:
|
||||||
frappe.throw(_("Close Date is required."), frappe.MandatoryError)
|
frappe.throw(_("Close Date is required."), frappe.MandatoryError)
|
||||||
|
|
||||||
|
def validate_lost_reason(self):
|
||||||
|
"""
|
||||||
|
Validate the lost reason if the status is set to "Lost".
|
||||||
|
"""
|
||||||
|
if self.status == "Lost":
|
||||||
|
if not self.lost_reason:
|
||||||
|
frappe.throw(_("Please specify a reason for losing the deal."), frappe.ValidationError)
|
||||||
|
elif self.lost_reason == "Other" and not self.lost_notes:
|
||||||
|
frappe.throw(_("Please specify the reason for losing the deal."), frappe.ValidationError)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_list_data():
|
def default_list_data():
|
||||||
columns = [
|
columns = [
|
||||||
|
|||||||
@ -27,9 +27,10 @@
|
|||||||
"label": "Details"
|
"label": "Details"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-02 22:13:30.498404",
|
"modified": "2025-06-30 16:53:51.721752",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Lead Source",
|
"name": "CRM Lead Source",
|
||||||
@ -44,7 +45,7 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Sales User",
|
"role": "System Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
@ -60,6 +61,15 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales User",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
@ -71,7 +81,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
0
crm/fcrm/doctype/crm_lost_reason/__init__.py
Normal file
0
crm/fcrm/doctype/crm_lost_reason/__init__.py
Normal file
8
crm/fcrm/doctype/crm_lost_reason/crm_lost_reason.js
Normal file
8
crm/fcrm/doctype/crm_lost_reason/crm_lost_reason.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("CRM Lost Reason", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
79
crm/fcrm/doctype/crm_lost_reason/crm_lost_reason.json
Normal file
79
crm/fcrm/doctype/crm_lost_reason/crm_lost_reason.json
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:lost_reason",
|
||||||
|
"creation": "2025-06-30 16:51:31.082360",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"lost_reason",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "lost_reason",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Lost Reason",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Description"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-06-30 16:59:15.094049",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "CRM Lost Reason",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
9
crm/fcrm/doctype/crm_lost_reason/crm_lost_reason.py
Normal file
9
crm/fcrm/doctype/crm_lost_reason/crm_lost_reason.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMLostReason(Document):
|
||||||
|
pass
|
||||||
30
crm/fcrm/doctype/crm_lost_reason/test_crm_lost_reason.py
Normal file
30
crm/fcrm/doctype/crm_lost_reason/test_crm_lost_reason.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestCRMLostReason(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for CRMLostReason.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestCRMLostReason(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for CRMLostReason.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -162,6 +162,7 @@ declare module 'vue' {
|
|||||||
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
|
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
|
||||||
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
|
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
|
||||||
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
||||||
|
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
|
||||||
LucideInfo: typeof import('~icons/lucide/info')['default']
|
LucideInfo: typeof import('~icons/lucide/info')['default']
|
||||||
LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
||||||
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
||||||
|
|||||||
@ -368,6 +368,7 @@
|
|||||||
<DataFields
|
<DataFields
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:docname="doc.data.name"
|
:docname="doc.data.name"
|
||||||
|
@beforeSave="(data) => emit('beforeSave', data)"
|
||||||
@afterSave="(data) => emit('afterSave', data)"
|
@afterSave="(data) => emit('afterSave', data)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -518,7 +519,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['afterSave'])
|
const emit = defineEmits(['beforeSave', 'afterSave'])
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
|||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { useDocument } from '@/data/document'
|
import { useDocument } from '@/data/document'
|
||||||
import { isMobileView } from '@/composables/settings'
|
import { isMobileView } from '@/composables/settings'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
doctype: {
|
doctype: {
|
||||||
@ -79,10 +79,13 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['afterSave'])
|
const emit = defineEmits(['beforeSave', 'afterSave'])
|
||||||
|
|
||||||
const { isManager } = usersStore()
|
const { isManager } = usersStore()
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
const attrs = instance?.vnode?.props ?? {}
|
||||||
|
|
||||||
const showDataFieldsModal = ref(false)
|
const showDataFieldsModal = ref(false)
|
||||||
|
|
||||||
const { document } = useDocument(props.doctype, props.docname)
|
const { document } = useDocument(props.doctype, props.docname)
|
||||||
@ -107,9 +110,15 @@ function saveChanges() {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
document.save.submit(null, {
|
const hasListener = attrs['onBeforeSave'] !== undefined
|
||||||
onSuccess: () => emit('afterSave', changes),
|
|
||||||
})
|
if (hasListener) {
|
||||||
|
emit('beforeSave', changes)
|
||||||
|
} else {
|
||||||
|
document.save.submit(null, {
|
||||||
|
onSuccess: () => emit('afterSave', changes),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@ -98,11 +98,7 @@ const show = defineModel()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
const {
|
const { document: deal, triggerOnBeforeCreate } = useDocument('CRM Deal')
|
||||||
document: deal,
|
|
||||||
triggerOnChange,
|
|
||||||
triggerOnBeforeCreate,
|
|
||||||
} = useDocument('CRM Deal')
|
|
||||||
|
|
||||||
const hasOrganizationSections = ref(true)
|
const hasOrganizationSections = ref(true)
|
||||||
const hasContactSections = ref(true)
|
const hasContactSections = ref(true)
|
||||||
@ -172,7 +168,7 @@ const tabs = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dealStatuses = computed(() => {
|
const dealStatuses = computed(() => {
|
||||||
let statuses = statusOptions('deal', null, [], triggerOnChange)
|
let statuses = statusOptions('deal')
|
||||||
if (!deal.doc.status) {
|
if (!deal.doc.status) {
|
||||||
deal.doc.status = statuses[0].value
|
deal.doc.status = statuses[0].value
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,14 +74,10 @@ const router = useRouter()
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const isLeadCreating = ref(false)
|
const isLeadCreating = ref(false)
|
||||||
|
|
||||||
const {
|
const { document: lead, triggerOnBeforeCreate } = useDocument('CRM Lead')
|
||||||
document: lead,
|
|
||||||
triggerOnChange,
|
|
||||||
triggerOnBeforeCreate,
|
|
||||||
} = useDocument('CRM Lead')
|
|
||||||
|
|
||||||
const leadStatuses = computed(() => {
|
const leadStatuses = computed(() => {
|
||||||
let statuses = statusOptions('lead', null, [], triggerOnChange)
|
let statuses = statusOptions('lead')
|
||||||
if (!lead.doc.status) {
|
if (!lead.doc.status) {
|
||||||
lead.doc.status = statuses?.[0]?.value
|
lead.doc.status = statuses?.[0]?.value
|
||||||
}
|
}
|
||||||
|
|||||||
98
frontend/src/components/Modals/LostReasonModal.vue
Normal file
98
frontend/src/components/Modals/LostReasonModal.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{ title: __('Lost reason') }"
|
||||||
|
@close="cancel"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="-mt-3 mb-4 text-p-base text-ink-gray-7">
|
||||||
|
{{ __('Please provide a reason for marking this deal as lost') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-sm text-ink-gray-5">
|
||||||
|
{{ __('Lost reason') }}
|
||||||
|
<span class="text-ink-red-2">*</span>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
class="form-control flex-1 truncate"
|
||||||
|
:value="lostReason"
|
||||||
|
doctype="CRM Lost Reason"
|
||||||
|
@change="(v) => (lostReason = v)"
|
||||||
|
:onCreate="onCreate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-sm text-ink-gray-5">
|
||||||
|
{{ __('Lost notes') }}
|
||||||
|
<span v-if="lostReason == 'Other'" class="text-ink-red-2">*</span>
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
class="form-control flex-1 truncate"
|
||||||
|
type="textarea"
|
||||||
|
:value="lostNotes"
|
||||||
|
@change="(e) => (lostNotes = e.target.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex justify-between items-center gap-2">
|
||||||
|
<div><ErrorMessage :message="error" /></div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button :label="__('Cancel')" @click="cancel" />
|
||||||
|
<Button variant="solid" :label="__('Save')" @click="save" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { createDocument } from '@/composables/document'
|
||||||
|
import { Dialog } from 'frappe-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
deal: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
|
||||||
|
const lostReason = ref(props.deal.doc.lost_reason || '')
|
||||||
|
const lostNotes = ref(props.deal.doc.lost_notes || '')
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
show.value = false
|
||||||
|
error.value = ''
|
||||||
|
lostReason.value = ''
|
||||||
|
lostNotes.value = ''
|
||||||
|
props.deal.doc.status = props.deal.originalDoc.status
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!lostReason.value) {
|
||||||
|
error.value = __('Lost reason is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (lostReason.value === 'Other' && !lostNotes.value) {
|
||||||
|
error.value = __('Lost notes are required when lost reason is "Other"')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
error.value = ''
|
||||||
|
show.value = false
|
||||||
|
|
||||||
|
props.deal.doc.lost_reason = lostReason.value
|
||||||
|
props.deal.doc.lost_notes = lostNotes.value
|
||||||
|
props.deal.save.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreate(value, close) {
|
||||||
|
createDocument('CRM Lost Reason', value, close)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -400,7 +400,7 @@ import { getFormat, evaluateDependsOnValue } from '@/utils'
|
|||||||
import { flt } from '@/utils/numberFormat.js'
|
import { flt } from '@/utils/numberFormat.js'
|
||||||
import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
|
import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
|
||||||
import { useDocument } from '@/data/document'
|
import { useDocument } from '@/data/document'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
sections: {
|
sections: {
|
||||||
@ -424,7 +424,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['afterFieldChange', 'reload'])
|
const emit = defineEmits(['beforeFieldChange', 'afterFieldChange', 'reload'])
|
||||||
|
|
||||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||||
getMeta(props.doctype)
|
getMeta(props.doctype)
|
||||||
@ -496,18 +496,23 @@ function parsedField(field) {
|
|||||||
return _field
|
return _field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
const attrs = instance?.vnode?.props ?? {}
|
||||||
|
|
||||||
async function fieldChange(value, df) {
|
async function fieldChange(value, df) {
|
||||||
if (props.preview) return
|
if (props.preview) return
|
||||||
|
|
||||||
await triggerOnChange(df.fieldname, value)
|
await triggerOnChange(df.fieldname, value)
|
||||||
|
|
||||||
document.save.submit(null, {
|
const hasListener = attrs['onBeforeFieldChange'] !== undefined
|
||||||
onSuccess: () => {
|
|
||||||
emit('afterFieldChange', {
|
if (hasListener) {
|
||||||
[df.fieldname]: value,
|
emit('beforeFieldChange', { [df.fieldname]: value })
|
||||||
})
|
} else {
|
||||||
},
|
document.save.submit(null, {
|
||||||
})
|
onSuccess: () => emit('afterFieldChange', { [df.fieldname]: value }),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsedSection(section, editButtonAdded) {
|
function parsedSection(section, editButtonAdded) {
|
||||||
|
|||||||
@ -26,9 +26,10 @@
|
|||||||
:options="
|
:options="
|
||||||
statusOptions(
|
statusOptions(
|
||||||
'deal',
|
'deal',
|
||||||
document,
|
document.statuses?.length
|
||||||
deal.data._customStatuses,
|
? document.statuses
|
||||||
triggerOnChange,
|
: deal.data._customStatuses,
|
||||||
|
triggerStatusChange,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -60,6 +61,7 @@
|
|||||||
v-model:reload="reload"
|
v-model:reload="reload"
|
||||||
v-model:tabIndex="tabIndex"
|
v-model:tabIndex="tabIndex"
|
||||||
v-model="deal"
|
v-model="deal"
|
||||||
|
@beforeSave="beforeStatusChange"
|
||||||
@afterSave="reloadAssignees"
|
@afterSave="reloadAssignees"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -157,6 +159,7 @@
|
|||||||
doctype="CRM Deal"
|
doctype="CRM Deal"
|
||||||
:docname="deal.data.name"
|
:docname="deal.data.name"
|
||||||
@reload="sections.reload"
|
@reload="sections.reload"
|
||||||
|
@beforeFieldChange="beforeStatusChange"
|
||||||
@afterFieldChange="reloadAssignees"
|
@afterFieldChange="reloadAssignees"
|
||||||
>
|
>
|
||||||
<template #actions="{ section }">
|
<template #actions="{ section }">
|
||||||
@ -336,6 +339,11 @@
|
|||||||
:docname="props.dealId"
|
:docname="props.dealId"
|
||||||
name="Deals"
|
name="Deals"
|
||||||
/>
|
/>
|
||||||
|
<LostReasonModal
|
||||||
|
v-if="showLostReasonModal"
|
||||||
|
v-model="showLostReasonModal"
|
||||||
|
:deal="document"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ErrorPage from '@/components/ErrorPage.vue'
|
import ErrorPage from '@/components/ErrorPage.vue'
|
||||||
@ -359,6 +367,7 @@ import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
|||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Activities from '@/components/Activities/Activities.vue'
|
import Activities from '@/components/Activities/Activities.vue'
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
|
import LostReasonModal from '@/components/Modals/LostReasonModal.vue'
|
||||||
import AssignTo from '@/components/AssignTo.vue'
|
import AssignTo from '@/components/AssignTo.vue'
|
||||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
@ -765,6 +774,36 @@ const { assignees, document, triggerOnChange } = useDocument(
|
|||||||
props.dealId,
|
props.dealId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async function triggerStatusChange(value) {
|
||||||
|
await triggerOnChange('status', value)
|
||||||
|
setLostReason()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showLostReasonModal = ref(false)
|
||||||
|
|
||||||
|
function setLostReason() {
|
||||||
|
if (
|
||||||
|
document.doc.status !== 'Lost' ||
|
||||||
|
(document.doc.lost_reason && document.doc.lost_reason !== 'Other') ||
|
||||||
|
(document.doc.lost_reason === 'Other' && document.doc.lost_notes)
|
||||||
|
) {
|
||||||
|
document.save.submit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLostReasonModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeStatusChange(data) {
|
||||||
|
if (data?.hasOwnProperty('status') && data.status == 'Lost') {
|
||||||
|
setLostReason()
|
||||||
|
} else {
|
||||||
|
document.save.submit(null, {
|
||||||
|
onSuccess: () => reloadAssignees(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reloadAssignees(data) {
|
function reloadAssignees(data) {
|
||||||
if (data?.hasOwnProperty('deal_owner')) {
|
if (data?.hasOwnProperty('deal_owner')) {
|
||||||
assignees.reload()
|
assignees.reload()
|
||||||
|
|||||||
@ -26,9 +26,10 @@
|
|||||||
:options="
|
:options="
|
||||||
statusOptions(
|
statusOptions(
|
||||||
'lead',
|
'lead',
|
||||||
document,
|
document.statuses?.length
|
||||||
lead.data._customStatuses,
|
? document.statuses
|
||||||
triggerOnChange,
|
: lead.data._customStatuses,
|
||||||
|
triggerStatusChange,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -330,6 +331,11 @@ const { triggerOnChange, assignees, document } = useDocument(
|
|||||||
props.leadId,
|
props.leadId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async function triggerStatusChange(value) {
|
||||||
|
await triggerOnChange('status', value)
|
||||||
|
document.save.submit()
|
||||||
|
}
|
||||||
|
|
||||||
const lead = createResource({
|
const lead = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
||||||
params: { name: props.leadId },
|
params: { name: props.leadId },
|
||||||
|
|||||||
@ -14,9 +14,10 @@
|
|||||||
:options="
|
:options="
|
||||||
statusOptions(
|
statusOptions(
|
||||||
'deal',
|
'deal',
|
||||||
document,
|
document.statuses?.length
|
||||||
deal.data._customStatuses,
|
? document.statuses
|
||||||
triggerOnChange,
|
: deal.data._customStatuses,
|
||||||
|
triggerStatusChange,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -78,6 +79,7 @@
|
|||||||
doctype="CRM Deal"
|
doctype="CRM Deal"
|
||||||
:docname="deal.data.name"
|
:docname="deal.data.name"
|
||||||
@reload="sections.reload"
|
@reload="sections.reload"
|
||||||
|
@beforeFieldChange="beforeStatusChange"
|
||||||
@afterFieldChange="reloadAssignees"
|
@afterFieldChange="reloadAssignees"
|
||||||
>
|
>
|
||||||
<template #actions="{ section }">
|
<template #actions="{ section }">
|
||||||
@ -222,6 +224,8 @@
|
|||||||
v-model:reload="reload"
|
v-model:reload="reload"
|
||||||
v-model:tabIndex="tabIndex"
|
v-model:tabIndex="tabIndex"
|
||||||
v-model="deal"
|
v-model="deal"
|
||||||
|
@beforeSave="beforeStatusChange"
|
||||||
|
@afterSave="reloadAssignees"
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@ -244,6 +248,11 @@
|
|||||||
afterInsert: (doc) => addContact(doc.name),
|
afterInsert: (doc) => addContact(doc.name),
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<LostReasonModal
|
||||||
|
v-if="showLostReasonModal"
|
||||||
|
v-model="showLostReasonModal"
|
||||||
|
:deal="document"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
@ -264,6 +273,7 @@ import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
|||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Activities from '@/components/Activities/Activities.vue'
|
import Activities from '@/components/Activities/Activities.vue'
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
|
import LostReasonModal from '@/components/Modals/LostReasonModal.vue'
|
||||||
import AssignTo from '@/components/AssignTo.vue'
|
import AssignTo from '@/components/AssignTo.vue'
|
||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
@ -624,6 +634,36 @@ const { assignees, document, triggerOnChange } = useDocument(
|
|||||||
props.dealId,
|
props.dealId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async function triggerStatusChange(value) {
|
||||||
|
await triggerOnChange('status', value)
|
||||||
|
setLostReason()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showLostReasonModal = ref(false)
|
||||||
|
|
||||||
|
function setLostReason() {
|
||||||
|
if (
|
||||||
|
document.doc.status !== 'Lost' ||
|
||||||
|
(document.doc.lost_reason && document.doc.lost_reason !== 'Other') ||
|
||||||
|
(document.doc.lost_reason === 'Other' && document.doc.lost_notes)
|
||||||
|
) {
|
||||||
|
document.save.submit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLostReasonModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeStatusChange(data) {
|
||||||
|
if (data?.hasOwnProperty('status') && data.status == 'Lost') {
|
||||||
|
setLostReason()
|
||||||
|
} else {
|
||||||
|
document.save.submit(null, {
|
||||||
|
onSuccess: () => reloadAssignees(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reloadAssignees(data) {
|
function reloadAssignees(data) {
|
||||||
if (data?.hasOwnProperty('deal_owner')) {
|
if (data?.hasOwnProperty('deal_owner')) {
|
||||||
assignees.reload()
|
assignees.reload()
|
||||||
|
|||||||
@ -14,9 +14,10 @@
|
|||||||
:options="
|
:options="
|
||||||
statusOptions(
|
statusOptions(
|
||||||
'lead',
|
'lead',
|
||||||
document,
|
document.statuses?.length
|
||||||
lead.data._customStatuses,
|
? document.statuses
|
||||||
triggerOnChange,
|
: lead.data._customStatuses,
|
||||||
|
triggerStatusChange,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@ -473,6 +474,11 @@ const { assignees, document, triggerOnChange } = useDocument(
|
|||||||
props.leadId,
|
props.leadId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async function triggerStatusChange(value) {
|
||||||
|
await triggerOnChange('status', value)
|
||||||
|
document.save.submit()
|
||||||
|
}
|
||||||
|
|
||||||
function reloadAssignees(data) {
|
function reloadAssignees(data) {
|
||||||
if (data?.hasOwnProperty('lead_owner')) {
|
if (data?.hasOwnProperty('lead_owner')) {
|
||||||
assignees.reload()
|
assignees.reload()
|
||||||
|
|||||||
@ -77,19 +77,10 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
|||||||
return communicationStatuses[name]
|
return communicationStatuses[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusOptions(
|
function statusOptions(doctype, statuses = [], triggerStatusChange = null) {
|
||||||
doctype,
|
|
||||||
document,
|
|
||||||
statuses = [],
|
|
||||||
triggerOnChange = null,
|
|
||||||
) {
|
|
||||||
let statusesByName =
|
let statusesByName =
|
||||||
doctype == 'deal' ? dealStatusesByName : leadStatusesByName
|
doctype == 'deal' ? dealStatusesByName : leadStatusesByName
|
||||||
|
|
||||||
if (document?.statuses?.length) {
|
|
||||||
statuses = document.statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statuses?.length) {
|
if (statuses?.length) {
|
||||||
statusesByName = statuses.reduce((acc, status) => {
|
statusesByName = statuses.reduce((acc, status) => {
|
||||||
acc[status] = statusesByName[status]
|
acc[status] = statusesByName[status]
|
||||||
@ -104,11 +95,8 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
|||||||
value: statusesByName[status]?.name,
|
value: statusesByName[status]?.name,
|
||||||
icon: () => h(IndicatorIcon, { class: statusesByName[status]?.color }),
|
icon: () => h(IndicatorIcon, { class: statusesByName[status]?.color }),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
|
await triggerStatusChange?.(statusesByName[status]?.name)
|
||||||
capture('status_changed', { doctype, status })
|
capture('status_changed', { doctype, status })
|
||||||
if (document) {
|
|
||||||
await triggerOnChange?.('status', statusesByName[status]?.name)
|
|
||||||
document.save.submit()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user