Merge pull request #606 from frappe/develop
This commit is contained in:
commit
0d1e5b687e
@ -537,7 +537,7 @@ def get_records_based_on_order(doctype, rows, filters, page_length, order):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_required=False):
|
||||
not_allowed_fieldtypes = [
|
||||
"Tab Break",
|
||||
"Section Break",
|
||||
@ -572,6 +572,9 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
if not restricted_fieldtypes or field.get("fieldtype") not in restricted_fieldtypes:
|
||||
fields.append(field)
|
||||
|
||||
if only_required:
|
||||
fields = [field for field in fields if field.get("reqd")]
|
||||
|
||||
if as_array:
|
||||
return fields
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Type",
|
||||
"options": "Quick Entry\nSide Panel\nData Fields\nGrid Row"
|
||||
"options": "Quick Entry\nSide Panel\nData Fields\nGrid Row\nRequired Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ttpm",
|
||||
@ -46,7 +46,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-02 22:12:51.663011",
|
||||
"modified": "2025-02-21 13:09:49.573515",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Fields Layout",
|
||||
|
||||
@ -24,7 +24,7 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
|
||||
if layout and layout.layout:
|
||||
tabs = json.loads(layout.layout)
|
||||
|
||||
if not tabs:
|
||||
if not tabs and type != "Required Fields":
|
||||
tabs = get_default_layout(doctype)
|
||||
|
||||
has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False
|
||||
|
||||
@ -197,8 +197,8 @@ class CRMLead(Document):
|
||||
|
||||
return False
|
||||
|
||||
def create_deal(self, contact, organization):
|
||||
deal = frappe.new_doc("CRM Deal")
|
||||
def create_deal(self, contact, organization, deal=None):
|
||||
new_deal = frappe.new_doc("CRM Deal")
|
||||
|
||||
lead_deal_map = {
|
||||
"lead_owner": "deal_owner",
|
||||
@ -245,13 +245,13 @@ class CRMLead(Document):
|
||||
if field.fieldname in lead_deal_map:
|
||||
fieldname = lead_deal_map[field.fieldname]
|
||||
|
||||
if hasattr(deal, fieldname):
|
||||
if hasattr(new_deal, fieldname):
|
||||
if fieldname == "organization":
|
||||
deal.update({fieldname: organization})
|
||||
new_deal.update({fieldname: organization})
|
||||
else:
|
||||
deal.update({fieldname: self.get(field.fieldname)})
|
||||
new_deal.update({fieldname: self.get(field.fieldname)})
|
||||
|
||||
deal.update(
|
||||
new_deal.update(
|
||||
{
|
||||
"lead": self.name,
|
||||
"contacts": [{"contact": contact}],
|
||||
@ -259,7 +259,7 @@ class CRMLead(Document):
|
||||
)
|
||||
|
||||
if self.first_responded_on:
|
||||
deal.update(
|
||||
new_deal.update(
|
||||
{
|
||||
"sla_creation": self.sla_creation,
|
||||
"response_by": self.response_by,
|
||||
@ -270,8 +270,11 @@ class CRMLead(Document):
|
||||
}
|
||||
)
|
||||
|
||||
deal.insert(ignore_permissions=True)
|
||||
return deal.name
|
||||
if deal:
|
||||
new_deal.update(deal)
|
||||
|
||||
new_deal.insert(ignore_permissions=True)
|
||||
return new_deal.name
|
||||
|
||||
def set_sla(self):
|
||||
"""
|
||||
@ -297,8 +300,8 @@ class CRMLead(Document):
|
||||
if sla:
|
||||
sla.apply(self)
|
||||
|
||||
def convert_to_deal(self):
|
||||
return convert_to_deal(lead=self.name, doc=self)
|
||||
def convert_to_deal(self, deal=None):
|
||||
return convert_to_deal(lead=self.name, doc=self, deal=deal)
|
||||
|
||||
@staticmethod
|
||||
def get_non_filterable_fields():
|
||||
@ -380,7 +383,7 @@ class CRMLead(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_to_deal(lead, doc=None):
|
||||
def convert_to_deal(lead, doc=None, deal=None):
|
||||
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission(
|
||||
"CRM Lead", "write", lead
|
||||
):
|
||||
@ -394,5 +397,5 @@ def convert_to_deal(lead, doc=None):
|
||||
lead.db_set("communication_status", "Replied")
|
||||
contact = lead.create_contact(False)
|
||||
organization = lead.create_organization()
|
||||
deal = lead.create_deal(contact, organization)
|
||||
return deal
|
||||
_deal = lead.create_deal(contact, organization, deal)
|
||||
return _deal
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/integrations": "^10.3.0",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.105",
|
||||
"frappe-ui": "^0.1.110",
|
||||
"gemoji": "^8.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.1",
|
||||
|
||||
@ -226,6 +226,10 @@ import { ref, computed, watch } from 'vue'
|
||||
const props = defineProps({
|
||||
tabs: Object,
|
||||
doctype: String,
|
||||
onlyRequired: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const tabIndex = ref(0)
|
||||
@ -249,6 +253,7 @@ const params = computed(() => {
|
||||
doctype: props.doctype,
|
||||
restricted_fieldtypes: restrictedFieldTypes,
|
||||
as_array: true,
|
||||
only_required: props.onlyRequired,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -190,6 +190,13 @@ function createDeal() {
|
||||
if (deal.website && !deal.website.startsWith('http')) {
|
||||
deal.website = 'https://' + deal.website
|
||||
}
|
||||
if (chooseExistingContact.value) {
|
||||
deal['first_name'] = null
|
||||
deal['last_name'] = null
|
||||
deal['email'] = null
|
||||
deal['mobile_no'] = null
|
||||
} else deal['contact'] = null
|
||||
|
||||
createResource({
|
||||
url: 'crm.fcrm.doctype.crm_deal.crm_deal.create_deal',
|
||||
params: { args: deal },
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
v-if="!preview"
|
||||
:tabs="tabs.data"
|
||||
:doctype="_doctype"
|
||||
:onlyRequired="onlyRequired"
|
||||
/>
|
||||
<FieldLayout v-else :tabs="tabs.data" :data="{}" :preview="true" />
|
||||
</div>
|
||||
@ -55,6 +56,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'CRM Lead',
|
||||
},
|
||||
onlyRequired: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const show = defineModel()
|
||||
@ -64,12 +69,13 @@ const dirty = ref(false)
|
||||
const preview = ref(false)
|
||||
|
||||
function getParams() {
|
||||
return { doctype: _doctype.value, type: 'Quick Entry' }
|
||||
let type = props.onlyRequired ? 'Required Fields' : 'Quick Entry'
|
||||
return { doctype: _doctype.value, type }
|
||||
}
|
||||
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['QuickEntryModal', _doctype.value],
|
||||
cache: ['QuickEntryModal', _doctype.value, props.onlyRequired],
|
||||
params: getParams(),
|
||||
onSuccess(data) {
|
||||
tabs.originalData = JSON.parse(JSON.stringify(data))
|
||||
@ -106,11 +112,12 @@ function saveChanges() {
|
||||
})
|
||||
})
|
||||
loading.value = true
|
||||
let type = props.onlyRequired ? 'Required Fields' : 'Quick Entry'
|
||||
call(
|
||||
'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout',
|
||||
{
|
||||
doctype: _doctype.value,
|
||||
type: 'Quick Entry',
|
||||
type: type,
|
||||
layout: JSON.stringify(_tabs),
|
||||
},
|
||||
).then(() => {
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import LightningIcon from '@/components/Icons/LightningIcon.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@ -34,6 +35,7 @@ const props = defineProps({
|
||||
const showBanner = ref(window.is_demo_site)
|
||||
|
||||
function signupNow() {
|
||||
capture('signup_from_demo_site')
|
||||
window.open('https://frappecloud.com/crm/signup', '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -186,7 +186,6 @@
|
||||
<Dialog
|
||||
v-model="showConvertToDealModal"
|
||||
:options="{
|
||||
title: __('Convert to Deal'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
@ -197,12 +196,38 @@
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-header>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||
{{ __('Convert to Deal') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="showConvertToDealModal = false"
|
||||
>
|
||||
<FeatherIcon name="x" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
<div class="mb-4 flex items-center gap-2 text-ink-gray-5">
|
||||
<OrganizationsIcon class="h-4 w-4" />
|
||||
<label class="block text-base">{{ __('Organization') }}</label>
|
||||
</div>
|
||||
<div class="ml-6">
|
||||
<div class="ml-6 text-ink-gray-9">
|
||||
<div class="flex items-center justify-between text-base">
|
||||
<div>{{ __('Choose Existing') }}</div>
|
||||
<Switch v-model="existingOrganizationChecked" />
|
||||
@ -210,7 +235,6 @@
|
||||
<Link
|
||||
v-if="existingOrganizationChecked"
|
||||
class="form-control mt-2.5"
|
||||
variant="outline"
|
||||
size="md"
|
||||
:value="existingOrganization"
|
||||
doctype="CRM Organization"
|
||||
@ -229,7 +253,7 @@
|
||||
<ContactsIcon class="h-4 w-4" />
|
||||
<label class="block text-base">{{ __('Contact') }}</label>
|
||||
</div>
|
||||
<div class="ml-6">
|
||||
<div class="ml-6 text-ink-gray-9">
|
||||
<div class="flex items-center justify-between text-base">
|
||||
<div>{{ __('Choose Existing') }}</div>
|
||||
<Switch v-model="existingContactChecked" />
|
||||
@ -237,7 +261,6 @@
|
||||
<Link
|
||||
v-if="existingContactChecked"
|
||||
class="form-control mt-2.5"
|
||||
variant="outline"
|
||||
size="md"
|
||||
:value="existingContact"
|
||||
doctype="Contact"
|
||||
@ -247,8 +270,23 @@
|
||||
{{ __("New contact will be created based on the person's details") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="dealTabs.data?.length" class="h-px w-full border-t my-6" />
|
||||
|
||||
<FieldLayout
|
||||
v-if="dealTabs.data?.length"
|
||||
:tabs="dealTabs.data"
|
||||
:data="deal"
|
||||
doctype="CRM Deal"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<QuickEntryModal
|
||||
v-if="showQuickEntryModal"
|
||||
v-model="showQuickEntryModal"
|
||||
doctype="CRM Deal"
|
||||
:onlyRequired="true"
|
||||
/>
|
||||
<FilesUploader
|
||||
v-if="lead.data?.name"
|
||||
v-model="showFilesUploader"
|
||||
@ -280,12 +318,15 @@ import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import Activities from '@/components/Activities/Activities.vue'
|
||||
import AssignTo from '@/components/AssignTo.vue'
|
||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import {
|
||||
@ -298,10 +339,15 @@ import {
|
||||
} from '@/utils'
|
||||
import { getView } from '@/utils/view'
|
||||
import { getSettings } from '@/stores/settings'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||
import {
|
||||
whatsappEnabled,
|
||||
callEnabled,
|
||||
isMobileView,
|
||||
} from '@/composables/settings'
|
||||
import { capture } from '@/telemetry'
|
||||
import {
|
||||
createResource,
|
||||
@ -315,14 +361,15 @@ import {
|
||||
call,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useActiveTabManager } from '@/composables/useActiveTabManager'
|
||||
|
||||
const { brand } = getSettings()
|
||||
const { isManager } = usersStore()
|
||||
const { $dialog, $socket, makeCall } = globalStore()
|
||||
const { getContactByName, contacts } = contactsStore()
|
||||
const { statusOptions, getLeadStatus } = statusesStore()
|
||||
const { statusOptions, getLeadStatus, getDealStatus } = statusesStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@ -599,18 +646,23 @@ async function convertToDeal(updated) {
|
||||
)
|
||||
showConvertToDealModal.value = false
|
||||
} else {
|
||||
let deal = await call(
|
||||
let _deal = await call(
|
||||
'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal',
|
||||
{
|
||||
lead: lead.data.name,
|
||||
},
|
||||
)
|
||||
if (deal) {
|
||||
{ lead: lead.data.name, deal },
|
||||
).catch((err) => {
|
||||
createToast({
|
||||
title: __('Error converting to deal'),
|
||||
text: __(err.messages?.[0]),
|
||||
icon: 'x',
|
||||
iconClasses: 'text-ink-red-4',
|
||||
})
|
||||
})
|
||||
if (_deal) {
|
||||
capture('convert_lead_to_deal')
|
||||
if (updated) {
|
||||
await contacts.reload()
|
||||
}
|
||||
router.push({ name: 'Deal', params: { dealId: deal } })
|
||||
router.push({ name: 'Deal', params: { dealId: _deal } })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -620,4 +672,50 @@ const activities = ref(null)
|
||||
function openEmailBox() {
|
||||
activities.value.emailBox.show = true
|
||||
}
|
||||
|
||||
const deal = reactive({})
|
||||
|
||||
const dealStatuses = computed(() => {
|
||||
let statuses = statusOptions('deal')
|
||||
if (!deal.status) {
|
||||
deal.status = statuses[0].value
|
||||
}
|
||||
return statuses
|
||||
})
|
||||
|
||||
const dealTabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['RequiredFields', 'CRM Deal'],
|
||||
params: { doctype: 'CRM Deal', type: 'Required Fields' },
|
||||
auto: true,
|
||||
transform: (_tabs) => {
|
||||
let hasFields = false
|
||||
let parsedTabs = _tabs.forEach((tab) => {
|
||||
tab.sections.forEach((section) => {
|
||||
section.columns.forEach((column) => {
|
||||
column.fields.forEach((field) => {
|
||||
hasFields = true
|
||||
if (field.fieldname == 'status') {
|
||||
field.fieldtype = 'Select'
|
||||
field.options = dealStatuses.value
|
||||
field.prefix = getDealStatus(deal.status).color
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Table') {
|
||||
deal[field.fieldname] = []
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
return hasFields ? parsedTabs : []
|
||||
},
|
||||
})
|
||||
|
||||
const showQuickEntryModal = ref(false)
|
||||
|
||||
function openQuickEntryModal() {
|
||||
showQuickEntryModal.value = true
|
||||
showConvertToDealModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@ -1136,10 +1136,10 @@
|
||||
dependencies:
|
||||
mini-svg-data-uri "^1.2.3"
|
||||
|
||||
"@tailwindcss/typography@^0.5.0":
|
||||
version "0.5.15"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.15.tgz#007ab9870c86082a1c76e5b3feda9392c7c8d648"
|
||||
integrity sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==
|
||||
"@tailwindcss/typography@^0.5.16":
|
||||
version "0.5.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.16.tgz#a926c8f44d5c439b2915e231cad80058850047c6"
|
||||
integrity sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==
|
||||
dependencies:
|
||||
lodash.castarray "^4.4.0"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
@ -2388,15 +2388,15 @@ fraction.js@^4.3.7:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||
|
||||
frappe-ui@^0.1.105:
|
||||
version "0.1.105"
|
||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.105.tgz#3bdf3c458ba27f27ff2f2a28cf7eb6f9ed872367"
|
||||
integrity sha512-9bZ/hj/HhQ9vp7DxE8aOKS8HqwETZrKT3IhSzjpYOk21efK8QwdbQ9sp0t4m3UII+HaUTSOTHnFzF7y9EhRZxg==
|
||||
frappe-ui@^0.1.110:
|
||||
version "0.1.110"
|
||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.110.tgz#dbe02d294cb0aeb0a4c1b3000682093c309450ae"
|
||||
integrity sha512-kFah6SoPauULXaeSbljNUq595/82VmY4k4+KA8zi4sXxpn4sXYi12qUl/1I8GOBhsCQQizmoh46DO7e/uU2M1A==
|
||||
dependencies:
|
||||
"@headlessui/vue" "^1.7.14"
|
||||
"@popperjs/core" "^2.11.2"
|
||||
"@tailwindcss/forms" "^0.5.3"
|
||||
"@tailwindcss/typography" "^0.5.0"
|
||||
"@tailwindcss/typography" "^0.5.16"
|
||||
"@tiptap/extension-color" "^2.0.3"
|
||||
"@tiptap/extension-highlight" "^2.0.3"
|
||||
"@tiptap/extension-image" "^2.0.3"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user