Merge pull request #894 from shariquerik/refactor-assignees

This commit is contained in:
Shariq Ansari 2025-06-06 14:58:47 +05:30 committed by GitHub
commit bb6a90058b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 129 additions and 46 deletions

View File

@ -3,6 +3,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.form.assign_to import set_status
from frappe.model import no_value_fields from frappe.model import no_value_fields
from frappe.model.document import get_controller from frappe.model.document import get_controller
from frappe.utils import make_filter_tuple from frappe.utils import make_filter_tuple
@ -659,6 +660,24 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_re
return fields_meta return fields_meta
@frappe.whitelist()
def remove_assignments(doctype, name, assignees, ignore_permissions=False):
assignees = json.loads(assignees)
if not assignees:
return
for assign_to in assignees:
set_status(
doctype,
name,
todo=None,
assign_to=assign_to,
status="Cancelled",
ignore_permissions=ignore_permissions,
)
@frappe.whitelist()
def get_assigned_users(doctype, name, default_assigned_to=None): def get_assigned_users(doctype, name, default_assigned_to=None):
assigned_users = frappe.get_all( assigned_users = frappe.get_all(
"ToDo", "ToDo",

View File

@ -13,7 +13,6 @@ def get_deal(name):
deal["fields_meta"] = get_fields_meta("CRM Deal") deal["fields_meta"] = get_fields_meta("CRM Deal")
deal["_form_script"] = get_form_script("CRM Deal") deal["_form_script"] = get_form_script("CRM Deal")
deal["_assign"] = get_assigned_users("CRM Deal", deal.name)
return deal return deal

View File

@ -13,5 +13,4 @@ def get_lead(name):
lead["fields_meta"] = get_fields_meta("CRM Lead") lead["fields_meta"] = get_fields_meta("CRM Lead")
lead["_form_script"] = get_form_script("CRM Lead") lead["_form_script"] = get_form_script("CRM Lead")
lead["_assign"] = get_assigned_users("CRM Lead", lead.name)
return lead return lead

View File

@ -365,7 +365,11 @@
</div> </div>
</div> </div>
<div v-else-if="title == 'Data'" class="h-full flex flex-col px-3 sm:px-10"> <div v-else-if="title == 'Data'" class="h-full flex flex-col px-3 sm:px-10">
<DataFields :doctype="doctype" :docname="doc.data.name" /> <DataFields
:doctype="doctype"
:docname="doc.data.name"
@afterSave="(data) => emit('afterSave', data)"
/>
</div> </div>
<div <div
v-else v-else
@ -514,6 +518,8 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['afterSave'])
const route = useRoute() const route = useRoute()
const doc = defineModel() const doc = defineModel()

View File

@ -76,6 +76,9 @@ const props = defineProps({
required: true, required: true,
}, },
}) })
const emit = defineEmits(['afterSave'])
const { isManager } = usersStore() const { isManager } = usersStore()
const showDataFieldsModal = ref(false) const showDataFieldsModal = ref(false)
@ -90,7 +93,21 @@ const tabs = createResource({
}) })
function saveChanges() { function saveChanges() {
document.save.submit() if (!document.isDirty) return
const updatedDoc = { ...document.doc }
const oldDoc = { ...document.originalDoc }
const changes = Object.keys(updatedDoc).reduce((acc, key) => {
if (JSON.stringify(updatedDoc[key]) !== JSON.stringify(oldDoc[key])) {
acc[key] = updatedDoc[key]
}
return acc
}, {})
document.save.submit(null, {
onSuccess: () => emit('afterSave', changes),
})
} }
watch( watch(

View File

@ -145,13 +145,11 @@ function updateAssignees() {
.map((assignee) => assignee.name) .map((assignee) => assignee.name)
if (removedAssignees.length) { if (removedAssignees.length) {
for (let a of removedAssignees) { call('crm.api.doc.remove_assignments', {
call('frappe.desk.form.assign_to.remove', { doctype: props.doctype,
doctype: props.doctype, name: props.doc.name,
name: props.doc.name, assignees: JSON.stringify(removedAssignees),
assign_to: a, })
})
}
} }
if (addedAssignees.length) { if (addedAssignees.length) {

View File

@ -417,13 +417,13 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['afterFieldChange', 'reload'])
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } = const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
getMeta(props.doctype) getMeta(props.doctype)
const { isManager, getUser } = usersStore() const { isManager, getUser } = usersStore()
const emit = defineEmits(['reload'])
const showSidePanelModal = ref(false) const showSidePanelModal = ref(false)
let document = { doc: {} } let document = { doc: {} }
@ -493,7 +493,13 @@ async function fieldChange(value, df) {
await triggerOnChange(df.fieldname) await triggerOnChange(df.fieldname)
document.save.submit() document.save.submit(null, {
onSuccess: () => {
emit('afterFieldChange', {
[df.fieldname]: value,
})
},
})
} }
function parsedSection(section, editButtonAdded) { function parsedSection(section, editButtonAdded) {

View File

@ -1,6 +1,6 @@
import { getScript } from '@/data/script' import { getScript } from '@/data/script'
import { runSequentially } from '@/utils' import { runSequentially, parseAssignees } from '@/utils'
import { createDocumentResource, toast } from 'frappe-ui' import { createDocumentResource, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
const documentsCache = {} const documentsCache = {}
@ -35,6 +35,17 @@ export function useDocument(doctype, docname) {
} }
} }
const assignees = createResource({
url: 'crm.api.doc.get_assigned_users',
cache: `assignees:${doctype}:${docname}`,
auto: true,
params: {
doctype: doctype,
name: docname,
},
transform: (data) => parseAssignees(data),
})
async function setupFormScript() { async function setupFormScript() {
if ( if (
controllersCache[doctype] && controllersCache[doctype] &&
@ -177,6 +188,7 @@ export function useDocument(doctype, docname) {
return { return {
document: documentsCache[doctype][docname || ''], document: documentsCache[doctype][docname || ''],
assignees,
triggerOnChange, triggerOnChange,
triggerOnRowAdd, triggerOnRowAdd,
triggerOnRowRemove, triggerOnRowRemove,

View File

@ -13,8 +13,8 @@
:actions="deal.data._customActions" :actions="deal.data._customActions"
/> />
<AssignTo <AssignTo
v-model="deal.data._assignedTo" v-model="assignees.data"
:data="deal.data" :data="document.doc"
doctype="CRM Deal" doctype="CRM Deal"
/> />
<Dropdown <Dropdown
@ -46,6 +46,7 @@
v-model:reload="reload" v-model:reload="reload"
v-model:tabIndex="tabIndex" v-model:tabIndex="tabIndex"
v-model="deal" v-model="deal"
@afterSave="reloadAssignees"
/> />
</template> </template>
</Tabs> </Tabs>
@ -134,6 +135,7 @@
doctype="CRM Deal" doctype="CRM Deal"
:docname="deal.data.name" :docname="deal.data.name"
@reload="sections.reload" @reload="sections.reload"
@afterFieldChange="reloadAssignees"
> >
<template #actions="{ section }"> <template #actions="{ section }">
<div v-if="section.name == 'contacts_section'" class="pr-2"> <div v-if="section.name == 'contacts_section'" class="pr-2">
@ -332,17 +334,13 @@ import Section from '@/components/Section.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { import { openWebsite, setupCustomizations, copyToClipboard } from '@/utils'
openWebsite,
setupAssignees,
setupCustomizations,
copyToClipboard,
} from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { useDocument } from '@/data/document'
import { whatsappEnabled, callEnabled } from '@/composables/settings' import { whatsappEnabled, callEnabled } from '@/composables/settings'
import { import {
createResource, createResource,
@ -396,7 +394,6 @@ const deal = createResource({
organization.fetch() organization.fetch()
} }
setupAssignees(deal)
setupCustomizations(deal, { setupCustomizations(deal, {
doc: data, doc: data,
$dialog, $dialog,
@ -721,4 +718,12 @@ const activities = ref(null)
function openEmailBox() { function openEmailBox() {
activities.value.emailBox.show = true activities.value.emailBox.show = true
} }
const { assignees, document } = useDocument('CRM Deal', props.dealId)
function reloadAssignees(data) {
if (data?.hasOwnProperty('deal_owner')) {
assignees.reload()
}
}
</script> </script>

View File

@ -13,8 +13,8 @@
:actions="lead.data._customActions" :actions="lead.data._customActions"
/> />
<AssignTo <AssignTo
v-model="lead.data._assignedTo" v-model="assignees.data"
:data="lead.data" :data="document.doc"
doctype="CRM Lead" doctype="CRM Lead"
/> />
<Dropdown <Dropdown
@ -51,6 +51,7 @@
v-model:reload="reload" v-model:reload="reload"
v-model:tabIndex="tabIndex" v-model:tabIndex="tabIndex"
v-model="lead" v-model="lead"
@afterSave="reloadAssignees"
/> />
</template> </template>
</Tabs> </Tabs>
@ -186,6 +187,7 @@
doctype="CRM Lead" doctype="CRM Lead"
:docname="lead.data.name" :docname="lead.data.name"
@reload="sections.reload" @reload="sections.reload"
@afterFieldChange="reloadAssignees"
/> />
</div> </div>
</Resizer> </Resizer>
@ -335,12 +337,7 @@ import SidePanelLayout from '@/components/SidePanelLayout.vue'
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue' import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { import { openWebsite, setupCustomizations, copyToClipboard } from '@/utils'
openWebsite,
setupAssignees,
setupCustomizations,
copyToClipboard,
} from '@/utils'
import { showQuickEntryModal, quickEntryProps } from '@/composables/modals' import { showQuickEntryModal, quickEntryProps } from '@/composables/modals'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
@ -403,7 +400,6 @@ const lead = createResource({
onSuccess: (data) => { onSuccess: (data) => {
errorTitle.value = '' errorTitle.value = ''
errorMessage.value = '' errorMessage.value = ''
setupAssignees(lead)
setupCustomizations(lead, { setupCustomizations(lead, {
doc: data, doc: data,
$dialog, $dialog,
@ -609,7 +605,10 @@ const existingOrganizationChecked = ref(false)
const existingContact = ref('') const existingContact = ref('')
const existingOrganization = ref('') const existingOrganization = ref('')
const { triggerConvertToDeal } = useDocument('CRM Lead', props.leadId) const { triggerConvertToDeal, assignees, document } = useDocument(
'CRM Lead',
props.leadId,
)
async function convertToDeal() { async function convertToDeal() {
if (existingContactChecked.value && !existingContact.value) { if (existingContactChecked.value && !existingContact.value) {
@ -711,4 +710,10 @@ function openQuickEntryModal() {
} }
showConvertToDealModal.value = false showConvertToDealModal.value = false
} }
function reloadAssignees(data) {
if (data?.hasOwnProperty('lead_owner')) {
assignees.reload()
}
}
</script> </script>

View File

@ -36,8 +36,8 @@
class="flex h-12 items-center justify-between gap-2 border-b px-3 py-2.5" class="flex h-12 items-center justify-between gap-2 border-b px-3 py-2.5"
> >
<AssignTo <AssignTo
v-model="deal.data._assignedTo" v-model="assignees.data"
:data="deal.data" :data="document.doc"
doctype="CRM Deal" doctype="CRM Deal"
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -66,6 +66,7 @@
doctype="CRM Deal" doctype="CRM Deal"
:docname="deal.data.name" :docname="deal.data.name"
@reload="sections.reload" @reload="sections.reload"
@afterFieldChange="reloadAssignees"
> >
<template #actions="{ section }"> <template #actions="{ section }">
<div v-if="section.name == 'contacts_section'" class="pr-2"> <div v-if="section.name == 'contacts_section'" class="pr-2">
@ -258,12 +259,13 @@ import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { setupAssignees, setupCustomizations } from '@/utils' import { setupCustomizations } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { useDocument } from '@/data/document'
import { import {
whatsappEnabled, whatsappEnabled,
callEnabled, callEnabled,
@ -311,7 +313,6 @@ const deal = createResource({
organization.fetch() organization.fetch()
} }
setupAssignees(deal)
setupCustomizations(deal, { setupCustomizations(deal, {
doc: data, doc: data,
$dialog, $dialog,
@ -605,4 +606,12 @@ async function deleteDeal(name) {
}) })
router.push({ name: 'Deals' }) router.push({ name: 'Deals' })
} }
const { assignees, document } = useDocument('CRM Deal', props.dealId)
function reloadAssignees(data) {
if (data?.hasOwnProperty('deal_owner')) {
assignees.reload()
}
}
</script> </script>

View File

@ -36,8 +36,8 @@
class="flex h-12 items-center justify-between gap-2 border-b px-3 py-2.5" class="flex h-12 items-center justify-between gap-2 border-b px-3 py-2.5"
> >
<AssignTo <AssignTo
v-model="lead.data._assignedTo" v-model="assignees.data"
:data="lead.data" :data="document.doc"
doctype="CRM Lead" doctype="CRM Lead"
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -71,6 +71,7 @@
doctype="CRM Lead" doctype="CRM Lead"
:docname="lead.data.name" :docname="lead.data.name"
@reload="sections.reload" @reload="sections.reload"
@afterFieldChange="reloadAssignees"
/> />
</div> </div>
</div> </div>
@ -173,12 +174,13 @@ import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { setupAssignees, setupCustomizations } from '@/utils' import { setupCustomizations } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { useDocument } from '@/data/document'
import { import {
whatsappEnabled, whatsappEnabled,
callEnabled, callEnabled,
@ -220,7 +222,6 @@ const lead = createResource({
params: { name: props.leadId }, params: { name: props.leadId },
cache: ['lead', props.leadId], cache: ['lead', props.leadId],
onSuccess: (data) => { onSuccess: (data) => {
setupAssignees(lead)
setupCustomizations(lead, { setupCustomizations(lead, {
doc: data, doc: data,
$dialog, $dialog,
@ -454,4 +455,12 @@ async function convertToDeal() {
router.push({ name: 'Deal', params: { dealId: deal } }) router.push({ name: 'Deal', params: { dealId: deal } })
} }
} }
const { assignees, document } = useDocument('CRM Lead', props.leadId)
function reloadAssignees(data) {
if (data?.hasOwnProperty('lead_owner')) {
assignees.reload()
}
}
</script> </script>

View File

@ -211,10 +211,9 @@ export function validateEmail(email) {
return regExp.test(email) return regExp.test(email)
} }
export function setupAssignees(doc) { export function parseAssignees(assignees) {
let { getUser } = usersStore() let { getUser } = usersStore()
let assignees = doc.data?._assign || [] return assignees.map((user) => ({
doc.data._assignedTo = assignees.map((user) => ({
name: user, name: user,
image: getUser(user).user_image, image: getUser(user).user_image,
label: getUser(user).full_name, label: getUser(user).full_name,