1
0
forked from test/crm

Merge pull request #1239 from frappe/main-hotfix

This commit is contained in:
Shariq Ansari 2025-09-08 19:49:39 +05:30 committed by GitHub
commit 8d7a155d78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 43136 additions and 14553 deletions

View File

@ -10,8 +10,15 @@ from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
def validate(doc, method): def validate(doc, method):
if doc.type == "Incoming" and doc.get("from"): if doc.type == "Incoming" and doc.get("from"):
name, doctype = get_lead_or_deal_from_number(doc.get("from")) name, doctype = get_lead_or_deal_from_number(doc.get("from"))
doc.reference_doctype = doctype if name != None:
doc.reference_name = name doc.reference_doctype = doctype
doc.reference_name = name
if doc.type == "Outgoing" and doc.get("to"):
name, doctype = get_lead_or_deal_from_number(doc.get("to"))
if name != None:
doc.reference_doctype = doctype
doc.reference_name = name
def on_update(doc, method): def on_update(doc, method):
@ -29,7 +36,7 @@ def on_update(doc, method):
def notify_agent(doc): def notify_agent(doc):
if doc.type == "Incoming": if doc.type == "Incoming":
doctype = doc.reference_doctype doctype = doc.reference_doctype
if doctype.startswith("CRM "): if doctype and doctype.startswith("CRM "):
doctype = doctype[4:].lower() doctype = doctype[4:].lower()
notification_text = f""" notification_text = f"""
<div class="mb-2 leading-5 text-ink-gray-5"> <div class="mb-2 leading-5 text-ink-gray-5">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

6329
crm/locale/da.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.16.0\n" "Generated-By: Babel 2.16.0\n"
#: frontend/src/components/ViewControls.vue:1217 #: frontend/src/components/ViewControls.vue:1172
msgid " (New)" msgid " (New)"
msgstr "" msgstr ""
@ -230,7 +230,7 @@ msgstr ""
#: frontend/src/components/CustomActions.vue:69 #: frontend/src/components/CustomActions.vue:69
#: frontend/src/components/ViewControls.vue:683 #: frontend/src/components/ViewControls.vue:683
#: frontend/src/components/ViewControls.vue:1109 #: frontend/src/components/ViewControls.vue:1064
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
@ -1796,8 +1796,8 @@ msgstr ""
msgid "Delete Task" msgid "Delete Task"
msgstr "" msgstr ""
#: frontend/src/components/ViewControls.vue:1157 #: frontend/src/components/ViewControls.vue:1112
#: frontend/src/components/ViewControls.vue:1165 #: frontend/src/components/ViewControls.vue:1120
msgid "Delete View" msgid "Delete View"
msgstr "" msgstr ""
@ -1986,7 +1986,7 @@ msgstr ""
#: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:225 #: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:225
#: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:228 #: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:228
#: frontend/src/components/Settings/EmailTemplate/NewEmailTemplate.vue:19 #: frontend/src/components/Settings/EmailTemplate/NewEmailTemplate.vue:19
#: frontend/src/components/ViewControls.vue:1113 #: frontend/src/components/ViewControls.vue:1068
msgid "Duplicate" msgid "Duplicate"
msgstr "" msgstr ""
@ -3429,11 +3429,11 @@ msgstr ""
msgid "Make Call" msgid "Make Call"
msgstr "" msgstr ""
#: frontend/src/components/ViewControls.vue:1146 #: frontend/src/components/ViewControls.vue:1101
msgid "Make Private" msgid "Make Private"
msgstr "" msgstr ""
#: frontend/src/components/ViewControls.vue:1146 #: frontend/src/components/ViewControls.vue:1101
msgid "Make Public" msgid "Make Public"
msgstr "" msgstr ""
@ -4244,7 +4244,7 @@ msgstr ""
msgid "Phone Numbers" msgid "Phone Numbers"
msgstr "" msgstr ""
#: frontend/src/components/ViewControls.vue:1138 #: frontend/src/components/ViewControls.vue:1093
msgid "Pin View" msgid "Pin View"
msgstr "" msgstr ""
@ -4963,7 +4963,7 @@ msgstr ""
msgid "Set as Primary Contact" msgid "Set as Primary Contact"
msgstr "" msgstr ""
#: frontend/src/components/ViewControls.vue:1123 #: frontend/src/components/ViewControls.vue:1078
msgid "Set as default" msgid "Set as default"
msgstr "" msgstr ""
@ -5653,7 +5653,7 @@ msgstr ""
msgid "Unlink {0} item(s)" msgid "Unlink {0} item(s)"
msgstr "" msgstr ""
#: frontend/src/components/ViewControls.vue:1138 #: frontend/src/components/ViewControls.vue:1093
msgid "Unpin View" msgid "Unpin View"
msgstr "" msgstr ""
@ -5688,7 +5688,6 @@ msgstr ""
#: frontend/src/components/Settings/SettingsPage.vue:20 #: frontend/src/components/Settings/SettingsPage.vue:20
#: frontend/src/components/Settings/TelephonySettings.vue:23 #: frontend/src/components/Settings/TelephonySettings.vue:23
#: frontend/src/components/Telephony/ExotelCallUI.vue:210 #: frontend/src/components/Telephony/ExotelCallUI.vue:210
#: frontend/src/components/ViewControls.vue:980
msgid "Update" msgid "Update"
msgstr "" msgstr ""
@ -5917,10 +5916,6 @@ msgstr ""
msgid "You do not have permission to access this document" msgid "You do not have permission to access this document"
msgstr "" msgstr ""
#: frontend/src/components/ViewControls.vue:976
msgid "You have unsaved changes. Do you want to save them?"
msgstr ""
#: crm/fcrm/doctype/crm_form_script/crm_form_script.py:24 #: crm/fcrm/doctype/crm_form_script/crm_form_script.py:24
msgid "You need to be in developer mode to edit a Standard Form Script" msgid "You need to be in developer mode to edit a Standard Form Script"
msgstr "" msgstr ""

6329
crm/locale/nb.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -85,7 +85,6 @@ declare module 'vue' {
DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default'] DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default']
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default'] DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default']
Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default'] Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default']
DropdownItem: typeof import('./src/components/DropdownItem.vue')['default']
DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default'] DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default']
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default'] DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default'] EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
@ -202,6 +201,8 @@ declare module 'vue' {
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default'] PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default'] PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default'] Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
PrimaryDropdown: typeof import('./src/components/PrimaryDropdown.vue')['default']
PrimaryDropdownItem: typeof import('./src/components/PrimaryDropdownItem.vue')['default']
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default'] ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default'] QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default'] QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']

View File

@ -106,6 +106,8 @@ function convertToDeal(selections, unselectAll) {
} }
function deleteValues(selections, unselectAll) { function deleteValues(selections, unselectAll) {
unselectAllAction.value = unselectAll
const selectedDocs = Array.from(selections) const selectedDocs = Array.from(selections)
if (selectedDocs.length == 1) { if (selectedDocs.length == 1) {
showDeleteDocModal.value = { showDeleteDocModal.value = {
@ -217,6 +219,12 @@ function bulkActions(selections, unselectAll) {
} }
function reload(unselectAll) { function reload(unselectAll) {
showDeleteDocModal.value = {
showLinkedDocsModal: false,
showDeleteModal: false,
docname: null,
}
unselectAllAction.value?.() unselectAllAction.value?.()
unselectAll?.() unselectAll?.()
list.value?.reload() list.value?.reload()

View File

@ -175,6 +175,5 @@ function openAddressModal(_address) {
doctype: 'Address', doctype: 'Address',
address: _address, address: _address,
} }
nextTick(() => (show.value = false))
} }
</script> </script>

View File

@ -0,0 +1,69 @@
<template>
<Popover>
<template #target="{ isOpen, togglePopover }">
<Button
:label="value"
class="dropdown-button flex items-center justify-between bg-surface-white !px-2.5 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:bg-surface-white focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0"
@click="togglePopover"
>
<div v-if="value" class="truncate">{{ value }}</div>
<div v-else class="text-base leading-5 text-ink-gray-4 truncate">
{{ placeholder }}
</div>
<template #suffix>
<FeatherIcon
:name="isOpen ? 'chevron-up' : 'chevron-down'"
class="h-4 text-ink-gray-5"
/>
</template>
</Button>
</template>
<template #body>
<div
class="my-2 p-1.5 min-w-40 space-y-1.5 divide-y divide-outline-gray-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div>
<PrimaryDropdownItem
v-for="option in options"
:key="option.name || option.value"
:option="option"
/>
<div v-if="!options?.length">
<div class="p-1.5 pl-3 pr-4 text-base text-ink-gray-4">
{{ __('No {0} Available', [label]) }}
</div>
</div>
</div>
<div class="pt-1.5">
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
iconLeft="plus"
@click="create && create()"
/>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import PrimaryDropdownItem from '@/components/PrimaryDropdownItem.vue'
import { Popover } from 'frappe-ui'
const props = defineProps({
value: { type: [String, Number], default: '' },
placeholder: { type: String, default: '' },
options: { type: Array, default: [] },
create: { type: Function },
label: { type: String, default: '' },
})
</script>
<style scoped>
.dropdown-button {
border-color: transparent;
background: transparent;
}
</style>

View File

@ -56,7 +56,7 @@
<script setup> <script setup>
import SuccessIcon from '@/components/Icons/SuccessIcon.vue' import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import { TextInput, Tooltip } from 'frappe-ui' import { TextInput } from 'frappe-ui'
import { nextTick, ref, onMounted } from 'vue' import { nextTick, ref, onMounted } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -79,70 +79,14 @@
<div>{{ doc[field.fieldname] }}</div> <div>{{ doc[field.fieldname] }}</div>
</Tooltip> </Tooltip>
</div> </div>
<div v-else-if="field.fieldtype === 'Dropdown'"> <PrimaryDropdown
<Popover> v-else-if="field.fieldtype === 'Dropdown'"
<template #target="{ isOpen, togglePopover }"> :value="doc[field.fieldname]"
<Button :placeholder="field.placeholder"
:label="doc[field.fieldname]" :options="field.options"
class="dropdown-button flex items-center justify-between bg-surface-white !px-2.5 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:bg-surface-white focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0" :create="field.create"
@click="togglePopover" :label="field.label"
> />
<div
v-if="doc[field.fieldname]"
class="truncate"
>
{{ doc[field.fieldname] }}
</div>
<div
v-else
class="text-base leading-5 text-ink-gray-4 truncate"
>
{{ field.placeholder }}
</div>
<template #suffix>
<FeatherIcon
:name="
isOpen ? 'chevron-up' : 'chevron-down'
"
class="h-4 text-ink-gray-5"
/>
</template>
</Button>
</template>
<template #body>
<div
class="my-2 p-1.5 min-w-40 space-y-1.5 divide-y divide-outline-gray-1 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div>
<DropdownItem
v-if="field.options?.length"
v-for="option in field.options"
:key="option.name"
:option="option"
/>
<div v-else>
<div
class="p-1.5 pl-3 pr-4 text-base text-ink-gray-4"
>
{{
__('No {0} Available', [field.label])
}}
</div>
</div>
</div>
<div class="pt-1.5">
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
iconLeft="plus"
@click="field.create()"
/>
</div>
</div>
</template>
</Popover>
</div>
<FormControl <FormControl
v-else-if="field.fieldtype == 'Check'" v-else-if="field.fieldtype == 'Check'"
class="form-control" class="form-control"
@ -366,7 +310,7 @@
import Password from '@/components/Controls/Password.vue' import Password from '@/components/Controls/Password.vue'
import FormattedInput from '@/components/Controls/FormattedInput.vue' import FormattedInput from '@/components/Controls/FormattedInput.vue'
import Section from '@/components/Section.vue' import Section from '@/components/Section.vue'
import DropdownItem from '@/components/DropdownItem.vue' import PrimaryDropdown from '@/components/PrimaryDropdown.vue'
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue' import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue' import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
@ -378,7 +322,7 @@ import { usersStore } from '@/stores/users'
import { isMobileView } from '@/composables/settings' import { isMobileView } from '@/composables/settings'
import { getFormat, evaluateDependsOnValue } from '@/utils' import { getFormat, evaluateDependsOnValue } from '@/utils'
import { flt } from '@/utils/numberFormat.js' import { flt } from '@/utils/numberFormat.js'
import { Tooltip, DateTimePicker, DatePicker, Popover } from 'frappe-ui' import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
import { useDocument } from '@/data/document' import { useDocument } from '@/data/document'
import { ref, computed, getCurrentInstance } from 'vue' import { ref, computed, getCurrentInstance } from 'vue'

View File

@ -113,7 +113,7 @@
class="flex flex-1 flex-col justify-between overflow-hidden" class="flex flex-1 flex-col justify-between overflow-hidden"
> >
<SidePanelLayout <SidePanelLayout
:sections="sections.data" :sections="parsedSections"
doctype="Contact" doctype="Contact"
:docname="contact.doc.name" :docname="contact.doc.name"
@reload="sections.reload" @reload="sections.reload"
@ -293,9 +293,7 @@ const tabs = [
const deals = createResource({ const deals = createResource({
url: 'crm.api.contact.get_linked_deals', url: 'crm.api.contact.get_linked_deals',
cache: ['deals', props.contactId], cache: ['deals', props.contactId],
params: { params: { contact: props.contactId },
contact: props.contactId,
},
auto: true, auto: true,
}) })
@ -310,118 +308,110 @@ const sections = createResource({
cache: ['sidePanelSections', 'Contact'], cache: ['sidePanelSections', 'Contact'],
params: { doctype: 'Contact' }, params: { doctype: 'Contact' },
auto: true, auto: true,
transform: (data) => computed(() => getParsedSections(data)),
}) })
function getParsedSections(_sections) { const parsedSections = computed(() => {
return _sections.map((section) => { if (!sections.data) return []
section.columns = section.columns.map((column) => { return sections.data.map((section) => ({
column.fields = column.fields.map((field) => { ...section,
columns: section.columns.map((column) => ({
...column,
fields: column.fields.map((field) => {
if (field.fieldname === 'email_id') { if (field.fieldname === 'email_id') {
return { return {
...field, ...field,
read_only: false, read_only: false,
fieldtype: 'Dropdown', fieldtype: 'Dropdown',
options: options: (contact.doc?.email_ids || []).map((email) => ({
contact.doc?.email_ids?.map((email) => { name: email.name,
return { value: email.email_id,
name: email.name, selected: email.email_id === contact.doc.email_id,
value: email.email_id, placeholder: 'john@doe.com',
selected: email.email_id === contact.doc.email_id, onClick: () => setAsPrimary('email', email.email_id),
placeholder: 'john@doe.com', onSave: (option, isNew) =>
onClick: () => { isNew
setAsPrimary('email', email.email_id) ? createNew('email', option.value)
}, : editOption(
onSave: (option, isNew) => { 'Contact Email',
if (isNew) { option.name,
createNew('email', option.value) 'email_id',
} else { option.value,
editOption( ),
'Contact Email', onDelete: async (option, isNew) => {
option.name, contact.doc.email_ids = contact.doc.email_ids.filter(
'email_id', (e) => e.name !== option.name,
option.value )
) if (!isNew) await deleteOption('Contact Email', option.name)
} },
}, })),
onDelete: async (option, isNew) => {
contact.doc.email_ids = contact.doc.email_ids.filter(
(email) => email.name !== option.name,
)
!isNew && (await deleteOption('Contact Email', option.name))
},
}
}) || [],
create: () => { create: () => {
contact.doc?.email_ids?.push({ // Add a temporary new option locally (mirrors original behavior)
name: 'new-1', contact.doc.email_ids = [
value: '', ...(contact.doc.email_ids || []),
selected: false, {
isNew: true, name: 'new-1',
}) value: '',
selected: false,
isNew: true,
},
]
}, },
} }
} else if (field.fieldname === 'mobile_no') { }
if (field.fieldname === 'mobile_no') {
return { return {
...field, ...field,
read_only: false, read_only: false,
fieldtype: 'Dropdown', fieldtype: 'Dropdown',
options: options: (contact.doc?.phone_nos || []).map((phone) => ({
contact.doc?.phone_nos?.map((phone) => { name: phone.name,
return { value: phone.phone,
name: phone.name, selected: phone.phone === contact.doc.mobile_no,
value: phone.phone, onClick: () => setAsPrimary('mobile_no', phone.phone),
selected: phone.phone === contact.doc.mobile_no, onSave: (option, isNew) =>
onClick: () => { isNew
setAsPrimary('mobile_no', phone.phone) ? createNew('phone', option.value)
}, : editOption(
onSave: (option, isNew) => { 'Contact Phone',
if (isNew) { option.name,
createNew('phone', option.value) 'phone',
} else { option.value,
editOption( ),
'Contact Phone', onDelete: async (option, isNew) => {
option.name, contact.doc.phone_nos = contact.doc.phone_nos.filter(
'phone', (p) => p.name !== option.name,
option.value )
) if (!isNew) await deleteOption('Contact Phone', option.name)
} },
}, })),
onDelete: async (option, isNew) => {
contact.doc.phone_nos = contact.doc.phone_nos.filter(
(phone) => phone.name !== option.name,
)
!isNew && (await deleteOption('Contact Phone', option.name))
},
}
}) || [],
create: () => { create: () => {
contact.doc?.phone_nos?.push({ contact.doc.phone_nos = [
name: 'new-1', ...(contact.doc.phone_nos || []),
value: '', {
selected: false, name: 'new-1',
isNew: true, value: '',
}) selected: false,
isNew: true,
},
]
}, },
} }
} else if (field.fieldname === 'address') { }
if (field.fieldname === 'address') {
return { return {
...field, ...field,
create: (value, close) => { create: (_value, close) => {
openAddressModal() openAddressModal()
close() close && close()
}, },
edit: (address) => openAddressModal(address), edit: (address) => openAddressModal(address),
} }
} else {
return field
} }
}) return field
return column }),
}) })),
return section }))
}) })
}
async function setAsPrimary(field, value) { async function setAsPrimary(field, value) {
let d = await call('crm.api.contact.set_as_primary', { let d = await call('crm.api.contact.set_as_primary', {