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):
if doc.type == "Incoming" and doc.get("from"):
name, doctype = get_lead_or_deal_from_number(doc.get("from"))
doc.reference_doctype = doctype
doc.reference_name = name
if name != None:
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):
@ -29,7 +36,7 @@ def on_update(doc, method):
def notify_agent(doc):
if doc.type == "Incoming":
doctype = doc.reference_doctype
if doctype.startswith("CRM "):
if doctype and doctype.startswith("CRM "):
doctype = doctype[4:].lower()
notification_text = f"""
<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"
"Generated-By: Babel 2.16.0\n"
#: frontend/src/components/ViewControls.vue:1217
#: frontend/src/components/ViewControls.vue:1172
msgid " (New)"
msgstr ""
@ -230,7 +230,7 @@ msgstr ""
#: frontend/src/components/CustomActions.vue:69
#: frontend/src/components/ViewControls.vue:683
#: frontend/src/components/ViewControls.vue:1109
#: frontend/src/components/ViewControls.vue:1064
msgid "Actions"
msgstr ""
@ -1796,8 +1796,8 @@ msgstr ""
msgid "Delete Task"
msgstr ""
#: frontend/src/components/ViewControls.vue:1157
#: frontend/src/components/ViewControls.vue:1165
#: frontend/src/components/ViewControls.vue:1112
#: frontend/src/components/ViewControls.vue:1120
msgid "Delete View"
msgstr ""
@ -1986,7 +1986,7 @@ msgstr ""
#: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:225
#: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:228
#: frontend/src/components/Settings/EmailTemplate/NewEmailTemplate.vue:19
#: frontend/src/components/ViewControls.vue:1113
#: frontend/src/components/ViewControls.vue:1068
msgid "Duplicate"
msgstr ""
@ -3429,11 +3429,11 @@ msgstr ""
msgid "Make Call"
msgstr ""
#: frontend/src/components/ViewControls.vue:1146
#: frontend/src/components/ViewControls.vue:1101
msgid "Make Private"
msgstr ""
#: frontend/src/components/ViewControls.vue:1146
#: frontend/src/components/ViewControls.vue:1101
msgid "Make Public"
msgstr ""
@ -4244,7 +4244,7 @@ msgstr ""
msgid "Phone Numbers"
msgstr ""
#: frontend/src/components/ViewControls.vue:1138
#: frontend/src/components/ViewControls.vue:1093
msgid "Pin View"
msgstr ""
@ -4963,7 +4963,7 @@ msgstr ""
msgid "Set as Primary Contact"
msgstr ""
#: frontend/src/components/ViewControls.vue:1123
#: frontend/src/components/ViewControls.vue:1078
msgid "Set as default"
msgstr ""
@ -5653,7 +5653,7 @@ msgstr ""
msgid "Unlink {0} item(s)"
msgstr ""
#: frontend/src/components/ViewControls.vue:1138
#: frontend/src/components/ViewControls.vue:1093
msgid "Unpin View"
msgstr ""
@ -5688,7 +5688,6 @@ msgstr ""
#: frontend/src/components/Settings/SettingsPage.vue:20
#: frontend/src/components/Settings/TelephonySettings.vue:23
#: frontend/src/components/Telephony/ExotelCallUI.vue:210
#: frontend/src/components/ViewControls.vue:980
msgid "Update"
msgstr ""
@ -5917,10 +5916,6 @@ msgstr ""
msgid "You do not have permission to access this document"
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
msgid "You need to be in developer mode to edit a Standard Form Script"
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']
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.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']
DurationIcon: typeof import('./src/components/Icons/DurationIcon.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']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.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']
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']

View File

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

View File

@ -175,6 +175,5 @@ function openAddressModal(_address) {
doctype: 'Address',
address: _address,
}
nextTick(() => (show.value = false))
}
</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>
import SuccessIcon from '@/components/Icons/SuccessIcon.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'
const props = defineProps({

View File

@ -79,70 +79,14 @@
<div>{{ doc[field.fieldname] }}</div>
</Tooltip>
</div>
<div v-else-if="field.fieldtype === 'Dropdown'">
<Popover>
<template #target="{ isOpen, togglePopover }">
<Button
:label="doc[field.fieldname]"
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="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>
<PrimaryDropdown
v-else-if="field.fieldtype === 'Dropdown'"
:value="doc[field.fieldname]"
:placeholder="field.placeholder"
:options="field.options"
:create="field.create"
:label="field.label"
/>
<FormControl
v-else-if="field.fieldtype == 'Check'"
class="form-control"
@ -366,7 +310,7 @@
import Password from '@/components/Controls/Password.vue'
import FormattedInput from '@/components/Controls/FormattedInput.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 ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
@ -378,7 +322,7 @@ import { usersStore } from '@/stores/users'
import { isMobileView } from '@/composables/settings'
import { getFormat, evaluateDependsOnValue } from '@/utils'
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 { ref, computed, getCurrentInstance } from 'vue'

View File

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