Merge pull request #898 from frappe/main-hotfix

This commit is contained in:
Shariq Ansari 2025-06-06 17:36:08 +05:30 committed by GitHub
commit 10148641c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 264 additions and 51 deletions

View File

@ -3,6 +3,7 @@ import json
import frappe
from frappe import _
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.document import get_controller
from frappe.utils import make_filter_tuple
@ -658,6 +659,24 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_re
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):
assigned_users = frappe.get_all(
"ToDo",

View File

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

View File

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

View File

@ -365,7 +365,11 @@
</div>
</div>
<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
v-else
@ -514,6 +518,8 @@ const props = defineProps({
},
})
const emit = defineEmits(['afterSave'])
const route = useRoute()
const doc = defineModel()

View File

@ -76,6 +76,9 @@ const props = defineProps({
required: true,
},
})
const emit = defineEmits(['afterSave'])
const { isManager } = usersStore()
const showDataFieldsModal = ref(false)
@ -90,7 +93,21 @@ const tabs = createResource({
})
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(

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { getScript } from '@/data/script'
import { runSequentially } from '@/utils'
import { createDocumentResource, toast } from 'frappe-ui'
import { runSequentially, parseAssignees } from '@/utils'
import { createDocumentResource, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue'
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() {
if (
controllersCache[doctype] &&
@ -64,6 +75,8 @@ export function useDocument(doctype, docname) {
organizedControllers[controllerKey].push(controller)
}
controllersCache[doctype][docname || ''] = organizedControllers
triggerOnload()
}
function getControllers(row = null) {
@ -82,9 +95,16 @@ export function useDocument(doctype, docname) {
return []
}
async function triggerOnload() {
const handler = async function () {
await this.onload?.()
}
await trigger(handler)
}
async function triggerOnRefresh() {
const handler = async function () {
await this.refresh()
await this.refresh?.()
}
await trigger(handler)
}
@ -177,6 +197,9 @@ export function useDocument(doctype, docname) {
return {
document: documentsCache[doctype][docname || ''],
assignees,
getControllers,
triggerOnload,
triggerOnChange,
triggerOnRowAdd,
triggerOnRowRemove,

View File

@ -116,8 +116,13 @@ export function getScript(doctype, view = 'Form') {
parentInstance = null,
isChildDoctype = false,
) {
document.actions = document.actions || []
document.statuses = document.statuses || []
let instance = new FormClass()
// Store the original document context to be used by properties like 'actions'
instance._originalDocumentContext = document
instance._isChildDoctype = isChildDoctype
for (const key in document) {
@ -199,6 +204,76 @@ export function getScript(doctype, view = 'Form') {
return createDocProxy(row, this)
}
}
if (!Object.prototype.hasOwnProperty.call(FormClass.prototype, 'actions')) {
Object.defineProperty(FormClass.prototype, 'actions', {
configurable: true,
enumerable: true,
get() {
if (!this._originalDocumentContext) {
console.warn(
'CRM Script: _originalDocumentContext not found on instance for actions getter.',
)
return []
}
return this._originalDocumentContext.actions
},
set(newValue) {
if (!this._originalDocumentContext) {
console.warn(
'CRM Script: _originalDocumentContext not found on instance for actions setter.',
)
return
}
if (!Array.isArray(newValue)) {
console.warn(
'CRM Script: "actions" property must be an array. Value was not set.',
newValue,
)
this._originalDocumentContext.actions = []
return
}
this._originalDocumentContext.actions = newValue
},
})
}
if (
!Object.prototype.hasOwnProperty.call(FormClass.prototype, 'statuses')
) {
Object.defineProperty(FormClass.prototype, 'statuses', {
configurable: true,
enumerable: true,
get() {
if (!this._originalDocumentContext) {
console.warn(
'CRM Script: _originalDocumentContext not found on instance for statuses getter.',
)
return []
}
return this._originalDocumentContext.statuses
},
set(newValue) {
if (!this._originalDocumentContext) {
console.warn(
'CRM Script: _originalDocumentContext not found on instance for statuses setter.',
)
return
}
if (!Array.isArray(newValue)) {
console.warn(
'CRM Script: "statuses" property must be an array. Value was not set.',
newValue,
)
this._originalDocumentContext.statuses = []
return
}
this._originalDocumentContext.statuses = newValue
},
})
}
}
// utility function to setup a form controller

View File

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

View File

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

View File

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

View File

@ -11,7 +11,13 @@
<div class="absolute right-0">
<Dropdown
:options="
statusOptions('lead', updateField, lead.data._customStatuses)
statusOptions(
'lead',
updateField,
document.statuses?.length
? document.statuses
: lead.data._customStatuses,
)
"
>
<template #default="{ open }">
@ -36,8 +42,8 @@
class="flex h-12 items-center justify-between gap-2 border-b px-3 py-2.5"
>
<AssignTo
v-model="lead.data._assignedTo"
:data="lead.data"
v-model="assignees.data"
:data="document.doc"
doctype="CRM Lead"
/>
<div class="flex items-center gap-2">
@ -45,6 +51,10 @@
v-if="lead.data._customActions?.length"
:actions="lead.data._customActions"
/>
<CustomActions
v-if="document.actions?.length"
:actions="document.actions"
/>
<Button
:label="__('Convert')"
variant="solid"
@ -71,6 +81,7 @@
doctype="CRM Lead"
:docname="lead.data.name"
@reload="sections.reload"
@afterFieldChange="reloadAssignees"
/>
</div>
</div>
@ -173,12 +184,13 @@ import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue'
import { setupAssignees, setupCustomizations } from '@/utils'
import { setupCustomizations } from '@/utils'
import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global'
import { statusesStore } from '@/stores/statuses'
import { getMeta } from '@/stores/meta'
import { useDocument } from '@/data/document'
import {
whatsappEnabled,
callEnabled,
@ -220,7 +232,6 @@ const lead = createResource({
params: { name: props.leadId },
cache: ['lead', props.leadId],
onSuccess: (data) => {
setupAssignees(lead)
setupCustomizations(lead, {
doc: data,
$dialog,
@ -454,4 +465,12 @@ async function convertToDeal() {
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>

View File

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