Merge pull request #1002 from frappe/mergify/bp/main-hotfix/pr-984
This commit is contained in:
commit
9f6832a5b6
@ -14,6 +14,8 @@
|
||||
"column_break_ijan",
|
||||
"status",
|
||||
"deal_owner",
|
||||
"lost_reason",
|
||||
"lost_notes",
|
||||
"section_break_jgpm",
|
||||
"probability",
|
||||
"deal_value",
|
||||
@ -391,12 +393,25 @@
|
||||
{
|
||||
"fieldname": "column_break_kpxa",
|
||||
"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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-16 11:42:49.413483",
|
||||
"modified": "2025-07-02 11:07:50.192089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Deal",
|
||||
|
||||
@ -25,6 +25,7 @@ class CRMDeal(Document):
|
||||
if self.has_value_changed("status"):
|
||||
add_status_change_log(self)
|
||||
self.validate_forcasting_fields()
|
||||
self.validate_lost_reason()
|
||||
|
||||
def after_insert(self):
|
||||
if self.deal_owner:
|
||||
@ -141,14 +142,32 @@ class CRMDeal(Document):
|
||||
if self.status == "Won" and not self.close_date:
|
||||
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):
|
||||
self.update_close_date()
|
||||
self.update_default_probability()
|
||||
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
||||
if not self.deal_value or self.deal_value == 0:
|
||||
frappe.throw(_("Deal Value is required."), frappe.MandatoryError)
|
||||
if not self.close_date:
|
||||
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
|
||||
def default_list_data():
|
||||
columns = [
|
||||
|
||||
@ -27,9 +27,10 @@
|
||||
"label": "Details"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-02 22:13:30.498404",
|
||||
"modified": "2025-06-30 16:53:51.721752",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Lead Source",
|
||||
@ -44,7 +45,7 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
@ -60,6 +61,15 @@
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
@ -71,7 +81,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"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
|
||||
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@ -162,7 +162,7 @@ declare module 'vue' {
|
||||
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
|
||||
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
|
||||
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
||||
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
||||
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
|
||||
LucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
|
||||
LucidePlus: typeof import('~icons/lucide/plus')['default']
|
||||
|
||||
@ -368,6 +368,7 @@
|
||||
<DataFields
|
||||
:doctype="doctype"
|
||||
:docname="doc.data.name"
|
||||
@beforeSave="(data) => emit('beforeSave', data)"
|
||||
@afterSave="(data) => emit('afterSave', data)"
|
||||
/>
|
||||
</div>
|
||||
@ -518,7 +519,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['afterSave'])
|
||||
const emit = defineEmits(['beforeSave', 'afterSave'])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { useDocument } from '@/data/document'
|
||||
import { isMobileView } from '@/composables/settings'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, getCurrentInstance } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
@ -79,10 +79,13 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['afterSave'])
|
||||
const emit = defineEmits(['beforeSave', 'afterSave'])
|
||||
|
||||
const { isManager } = usersStore()
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const attrs = instance?.vnode?.props ?? {}
|
||||
|
||||
const showDataFieldsModal = ref(false)
|
||||
|
||||
const { document } = useDocument(props.doctype, props.docname)
|
||||
@ -107,9 +110,15 @@ function saveChanges() {
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
document.save.submit(null, {
|
||||
onSuccess: () => emit('afterSave', changes),
|
||||
})
|
||||
const hasListener = attrs['onBeforeSave'] !== undefined
|
||||
|
||||
if (hasListener) {
|
||||
emit('beforeSave', changes)
|
||||
} else {
|
||||
document.save.submit(null, {
|
||||
onSuccess: () => emit('afterSave', changes),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@ -98,11 +98,7 @@ const show = defineModel()
|
||||
const router = useRouter()
|
||||
const error = ref(null)
|
||||
|
||||
const {
|
||||
document: deal,
|
||||
triggerOnChange,
|
||||
triggerOnBeforeCreate,
|
||||
} = useDocument('CRM Deal')
|
||||
const { document: deal, triggerOnBeforeCreate } = useDocument('CRM Deal')
|
||||
|
||||
const hasOrganizationSections = ref(true)
|
||||
const hasContactSections = ref(true)
|
||||
@ -172,7 +168,7 @@ const tabs = createResource({
|
||||
})
|
||||
|
||||
const dealStatuses = computed(() => {
|
||||
let statuses = statusOptions('deal', null, [], triggerOnChange)
|
||||
let statuses = statusOptions('deal')
|
||||
if (!deal.doc.status) {
|
||||
deal.doc.status = statuses[0].value
|
||||
}
|
||||
|
||||
@ -74,14 +74,10 @@ const router = useRouter()
|
||||
const error = ref(null)
|
||||
const isLeadCreating = ref(false)
|
||||
|
||||
const {
|
||||
document: lead,
|
||||
triggerOnChange,
|
||||
triggerOnBeforeCreate,
|
||||
} = useDocument('CRM Lead')
|
||||
const { document: lead, triggerOnBeforeCreate } = useDocument('CRM Lead')
|
||||
|
||||
const leadStatuses = computed(() => {
|
||||
let statuses = statusOptions('lead', null, [], triggerOnChange)
|
||||
let statuses = statusOptions('lead')
|
||||
if (!lead.doc.status) {
|
||||
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 { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
|
||||
import { useDocument } from '@/data/document'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
sections: {
|
||||
@ -424,7 +424,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['afterFieldChange', 'reload'])
|
||||
const emit = defineEmits(['beforeFieldChange', 'afterFieldChange', 'reload'])
|
||||
|
||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||
getMeta(props.doctype)
|
||||
@ -496,18 +496,23 @@ function parsedField(field) {
|
||||
return _field
|
||||
}
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const attrs = instance?.vnode?.props ?? {}
|
||||
|
||||
async function fieldChange(value, df) {
|
||||
if (props.preview) return
|
||||
|
||||
await triggerOnChange(df.fieldname, value)
|
||||
|
||||
document.save.submit(null, {
|
||||
onSuccess: () => {
|
||||
emit('afterFieldChange', {
|
||||
[df.fieldname]: value,
|
||||
})
|
||||
},
|
||||
})
|
||||
const hasListener = attrs['onBeforeFieldChange'] !== undefined
|
||||
|
||||
if (hasListener) {
|
||||
emit('beforeFieldChange', { [df.fieldname]: value })
|
||||
} else {
|
||||
document.save.submit(null, {
|
||||
onSuccess: () => emit('afterFieldChange', { [df.fieldname]: value }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parsedSection(section, editButtonAdded) {
|
||||
|
||||
@ -26,9 +26,10 @@
|
||||
:options="
|
||||
statusOptions(
|
||||
'deal',
|
||||
document,
|
||||
deal.data._customStatuses,
|
||||
triggerOnChange,
|
||||
document.statuses?.length
|
||||
? document.statuses
|
||||
: deal.data._customStatuses,
|
||||
triggerStatusChange,
|
||||
)
|
||||
"
|
||||
>
|
||||
@ -60,6 +61,7 @@
|
||||
v-model:reload="reload"
|
||||
v-model:tabIndex="tabIndex"
|
||||
v-model="deal"
|
||||
@beforeSave="beforeStatusChange"
|
||||
@afterSave="reloadAssignees"
|
||||
/>
|
||||
</template>
|
||||
@ -147,6 +149,7 @@
|
||||
doctype="CRM Deal"
|
||||
:docname="deal.data.name"
|
||||
@reload="sections.reload"
|
||||
@beforeFieldChange="beforeStatusChange"
|
||||
@afterFieldChange="reloadAssignees"
|
||||
>
|
||||
<template #actions="{ section }">
|
||||
@ -326,6 +329,11 @@
|
||||
:docname="props.dealId"
|
||||
name="Deals"
|
||||
/>
|
||||
<LostReasonModal
|
||||
v-if="showLostReasonModal"
|
||||
v-model="showLostReasonModal"
|
||||
:deal="document"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import ErrorPage from '@/components/ErrorPage.vue'
|
||||
@ -349,6 +357,7 @@ import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import Activities from '@/components/Activities/Activities.vue'
|
||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||
import LostReasonModal from '@/components/Modals/LostReasonModal.vue'
|
||||
import AssignTo from '@/components/AssignTo.vue'
|
||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
@ -755,6 +764,36 @@ const { assignees, document, triggerOnChange } = useDocument(
|
||||
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) {
|
||||
if (data?.hasOwnProperty('deal_owner')) {
|
||||
assignees.reload()
|
||||
|
||||
@ -26,9 +26,10 @@
|
||||
:options="
|
||||
statusOptions(
|
||||
'lead',
|
||||
document,
|
||||
lead.data._customStatuses,
|
||||
triggerOnChange,
|
||||
document.statuses?.length
|
||||
? document.statuses
|
||||
: lead.data._customStatuses,
|
||||
triggerStatusChange,
|
||||
)
|
||||
"
|
||||
>
|
||||
@ -320,6 +321,11 @@ const { triggerOnChange, assignees, document } = useDocument(
|
||||
props.leadId,
|
||||
)
|
||||
|
||||
async function triggerStatusChange(value) {
|
||||
await triggerOnChange('status', value)
|
||||
document.save.submit()
|
||||
}
|
||||
|
||||
const lead = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
||||
params: { name: props.leadId },
|
||||
|
||||
@ -14,9 +14,10 @@
|
||||
:options="
|
||||
statusOptions(
|
||||
'deal',
|
||||
document,
|
||||
deal.data._customStatuses,
|
||||
triggerOnChange,
|
||||
document.statuses?.length
|
||||
? document.statuses
|
||||
: deal.data._customStatuses,
|
||||
triggerStatusChange,
|
||||
)
|
||||
"
|
||||
>
|
||||
@ -78,6 +79,7 @@
|
||||
doctype="CRM Deal"
|
||||
:docname="deal.data.name"
|
||||
@reload="sections.reload"
|
||||
@beforeFieldChange="beforeStatusChange"
|
||||
@afterFieldChange="reloadAssignees"
|
||||
>
|
||||
<template #actions="{ section }">
|
||||
@ -222,6 +224,8 @@
|
||||
v-model:reload="reload"
|
||||
v-model:tabIndex="tabIndex"
|
||||
v-model="deal"
|
||||
@beforeSave="beforeStatusChange"
|
||||
@afterSave="reloadAssignees"
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@ -244,6 +248,11 @@
|
||||
afterInsert: (doc) => addContact(doc.name),
|
||||
}"
|
||||
/>
|
||||
<LostReasonModal
|
||||
v-if="showLostReasonModal"
|
||||
v-model="showLostReasonModal"
|
||||
:deal="document"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import Icon from '@/components/Icon.vue'
|
||||
@ -264,6 +273,7 @@ import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import Activities from '@/components/Activities/Activities.vue'
|
||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||
import LostReasonModal from '@/components/Modals/LostReasonModal.vue'
|
||||
import AssignTo from '@/components/AssignTo.vue'
|
||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
@ -624,6 +634,36 @@ const { assignees, document, triggerOnChange } = useDocument(
|
||||
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) {
|
||||
if (data?.hasOwnProperty('deal_owner')) {
|
||||
assignees.reload()
|
||||
|
||||
@ -14,9 +14,10 @@
|
||||
:options="
|
||||
statusOptions(
|
||||
'lead',
|
||||
document,
|
||||
lead.data._customStatuses,
|
||||
triggerOnChange,
|
||||
document.statuses?.length
|
||||
? document.statuses
|
||||
: lead.data._customStatuses,
|
||||
triggerStatusChange,
|
||||
)
|
||||
"
|
||||
>
|
||||
@ -473,6 +474,11 @@ const { assignees, document, triggerOnChange } = useDocument(
|
||||
props.leadId,
|
||||
)
|
||||
|
||||
async function triggerStatusChange(value) {
|
||||
await triggerOnChange('status', value)
|
||||
document.save.submit()
|
||||
}
|
||||
|
||||
function reloadAssignees(data) {
|
||||
if (data?.hasOwnProperty('lead_owner')) {
|
||||
assignees.reload()
|
||||
|
||||
@ -77,19 +77,10 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
||||
return communicationStatuses[name]
|
||||
}
|
||||
|
||||
function statusOptions(
|
||||
doctype,
|
||||
document,
|
||||
statuses = [],
|
||||
triggerOnChange = null,
|
||||
) {
|
||||
function statusOptions(doctype, statuses = [], triggerStatusChange = null) {
|
||||
let statusesByName =
|
||||
doctype == 'deal' ? dealStatusesByName : leadStatusesByName
|
||||
|
||||
if (document?.statuses?.length) {
|
||||
statuses = document.statuses
|
||||
}
|
||||
|
||||
if (statuses?.length) {
|
||||
statusesByName = statuses.reduce((acc, status) => {
|
||||
acc[status] = statusesByName[status]
|
||||
@ -104,11 +95,8 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
||||
value: statusesByName[status]?.name,
|
||||
icon: () => h(IndicatorIcon, { class: statusesByName[status]?.color }),
|
||||
onClick: async () => {
|
||||
await triggerStatusChange?.(statusesByName[status]?.name)
|
||||
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