Merge branch 'develop' into trial-banner-1

This commit is contained in:
Shariq Ansari 2025-02-21 18:13:41 +05:30 committed by GitHub
commit 93a61f9468
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 296 additions and 174 deletions

View File

@ -276,7 +276,7 @@ def get_data(
default_view_filters = { default_view_filters = {
"dt": doctype, "dt": doctype,
"type": view_type or "list", "type": view_type or "list",
"is_default": 1, "is_standard": 1,
"user": frappe.session.user, "user": frappe.session.user,
} }
@ -537,7 +537,7 @@ def get_records_based_on_order(doctype, rows, filters, page_length, order):
@frappe.whitelist() @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 = [ not_allowed_fieldtypes = [
"Tab Break", "Tab Break",
"Section 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: if not restricted_fieldtypes or field.get("fieldtype") not in restricted_fieldtypes:
fields.append(field) fields.append(field)
if only_required:
fields = [field for field in fields if field.get("reqd")]
if as_array: if as_array:
return fields return fields

View File

@ -14,8 +14,3 @@ def get_views(doctype):
query = query.where(View.dt == doctype) query = query.where(View.dt == doctype)
views = query.run(as_dict=True) views = query.run(as_dict=True)
return views return views
@frappe.whitelist()
def get_default_view():
return frappe.db.get_single_value("FCRM Settings", "default_view") or None

View File

@ -27,7 +27,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Type", "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", "fieldname": "section_break_ttpm",
@ -46,7 +46,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-01-02 22:12:51.663011", "modified": "2025-02-21 13:09:49.573515",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Fields Layout", "name": "CRM Fields Layout",

View File

@ -24,7 +24,7 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
if layout and layout.layout: if layout and layout.layout:
tabs = json.loads(layout.layout) tabs = json.loads(layout.layout)
if not tabs: if not tabs and type != "Required Fields":
tabs = get_default_layout(doctype) tabs = get_default_layout(doctype)
has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False

View File

@ -197,8 +197,8 @@ class CRMLead(Document):
return False return False
def create_deal(self, contact, organization): def create_deal(self, contact, organization, deal=None):
deal = frappe.new_doc("CRM Deal") new_deal = frappe.new_doc("CRM Deal")
lead_deal_map = { lead_deal_map = {
"lead_owner": "deal_owner", "lead_owner": "deal_owner",
@ -245,13 +245,13 @@ class CRMLead(Document):
if field.fieldname in lead_deal_map: if field.fieldname in lead_deal_map:
fieldname = lead_deal_map[field.fieldname] fieldname = lead_deal_map[field.fieldname]
if hasattr(deal, fieldname): if hasattr(new_deal, fieldname):
if fieldname == "organization": if fieldname == "organization":
deal.update({fieldname: organization}) new_deal.update({fieldname: organization})
else: else:
deal.update({fieldname: self.get(field.fieldname)}) new_deal.update({fieldname: self.get(field.fieldname)})
deal.update( new_deal.update(
{ {
"lead": self.name, "lead": self.name,
"contacts": [{"contact": contact}], "contacts": [{"contact": contact}],
@ -259,7 +259,7 @@ class CRMLead(Document):
) )
if self.first_responded_on: if self.first_responded_on:
deal.update( new_deal.update(
{ {
"sla_creation": self.sla_creation, "sla_creation": self.sla_creation,
"response_by": self.response_by, "response_by": self.response_by,
@ -270,8 +270,11 @@ class CRMLead(Document):
} }
) )
deal.insert(ignore_permissions=True) if deal:
return deal.name new_deal.update(deal)
new_deal.insert(ignore_permissions=True)
return new_deal.name
def set_sla(self): def set_sla(self):
""" """
@ -297,8 +300,8 @@ class CRMLead(Document):
if sla: if sla:
sla.apply(self) sla.apply(self)
def convert_to_deal(self): def convert_to_deal(self, deal=None):
return convert_to_deal(lead=self.name, doc=self) return convert_to_deal(lead=self.name, doc=self, deal=deal)
@staticmethod @staticmethod
def get_non_filterable_fields(): def get_non_filterable_fields():
@ -380,7 +383,7 @@ class CRMLead(Document):
@frappe.whitelist() @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( if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission(
"CRM Lead", "write", lead "CRM Lead", "write", lead
): ):
@ -394,5 +397,5 @@ def convert_to_deal(lead, doc=None):
lead.db_set("communication_status", "Replied") lead.db_set("communication_status", "Replied")
contact = lead.create_contact(False) contact = lead.create_contact(False)
organization = lead.create_organization() organization = lead.create_organization()
deal = lead.create_deal(contact, organization) _deal = lead.create_deal(contact, organization, deal)
return deal return _deal

View File

@ -8,7 +8,8 @@ from frappe.model.document import Document
class CRMNotification(Document): class CRMNotification(Document):
def on_update(self): def on_update(self):
frappe.publish_realtime("crm_notification") if self.to_user:
frappe.publish_realtime("crm_notification", user= self.to_user)
def notify_user(args): def notify_user(args):
""" """

View File

@ -8,6 +8,7 @@
"label", "label",
"icon", "icon",
"user", "user",
"is_standard",
"is_default", "is_default",
"column_break_zacm", "column_break_zacm",
"type", "type",
@ -112,12 +113,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Public" "label": "Public"
}, },
{
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
},
{ {
"fieldname": "icon", "fieldname": "icon",
"fieldtype": "Data", "fieldtype": "Data",
@ -174,15 +169,26 @@
"label": "Kanban Fields" "label": "Kanban Fields"
}, },
{ {
"default": "name",
"fieldname": "title_field", "fieldname": "title_field",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Title Field" "label": "Title Field"
},
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard"
},
{
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-06-25 19:40:12.067788", "modified": "2025-02-20 15:36:55.059065",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM View Settings", "name": "CRM View Settings",

View File

@ -37,7 +37,7 @@ def create(view):
doc.icon = view.icon doc.icon = view.icon
doc.dt = view.doctype doc.dt = view.doctype
doc.user = frappe.session.user doc.user = frappe.session.user
doc.route_name = view.route_name or "" doc.route_name = view.route_name or get_route_name(view.doctype)
doc.load_default_columns = view.load_default_columns or False doc.load_default_columns = view.load_default_columns or False
doc.filters = json.dumps(view.filters) doc.filters = json.dumps(view.filters)
doc.order_by = view.order_by doc.order_by = view.order_by
@ -70,7 +70,7 @@ def update(view):
doc.label = view.label doc.label = view.label
doc.type = view.type or "list" doc.type = view.type or "list"
doc.icon = view.icon doc.icon = view.icon
doc.route_name = view.route_name or "" doc.route_name = view.route_name or get_route_name(view.doctype)
doc.load_default_columns = view.load_default_columns or False doc.load_default_columns = view.load_default_columns or False
doc.filters = json.dumps(filters) doc.filters = json.dumps(filters)
doc.order_by = view.order_by doc.order_by = view.order_by
@ -147,13 +147,23 @@ def sync_default_columns(view):
@frappe.whitelist() @frappe.whitelist()
def set_as_default(name=None, type=None, doctype=None): def set_as_default(name=None, type=None, doctype=None):
if not name: if name:
name = type + "_" + doctype frappe.db.set_value("CRM View Settings", name, "is_default", 1)
frappe.db.set_single_value("FCRM Settings", "default_view", name) else:
doc = create_or_update_standard_view({"type": type, "doctype": doctype, "is_default": 1})
name = doc.name
# remove default from other views of same user
frappe.db.set_value(
"CRM View Settings",
{"name": ("!=", name), "user": frappe.session.user, "is_default": 1},
"is_default",
0,
)
@frappe.whitelist() @frappe.whitelist()
def create_or_update_default_view(view): def create_or_update_standard_view(view):
view = frappe._dict(view) view = frappe._dict(view)
filters = parse_json(view.filters) or {} filters = parse_json(view.filters) or {}
@ -161,6 +171,7 @@ def create_or_update_default_view(view):
rows = parse_json(view.rows or "[]") rows = parse_json(view.rows or "[]")
kanban_columns = parse_json(view.kanban_columns or "[]") kanban_columns = parse_json(view.kanban_columns or "[]")
kanban_fields = parse_json(view.kanban_fields or "[]") kanban_fields = parse_json(view.kanban_fields or "[]")
view.column_field = view.column_field or "status"
default_rows = sync_default_rows(view.doctype, view.type) default_rows = sync_default_rows(view.doctype, view.type)
rows = rows + default_rows if default_rows else rows rows = rows + default_rows if default_rows else rows
@ -173,42 +184,63 @@ def create_or_update_default_view(view):
doc = frappe.db.exists( doc = frappe.db.exists(
"CRM View Settings", "CRM View Settings",
{"dt": view.doctype, "type": view.type or "list", "is_default": True, "user": frappe.session.user}, {"dt": view.doctype, "type": view.type or "list", "is_standard": True, "user": frappe.session.user},
) )
if doc: if doc:
doc = frappe.get_doc("CRM View Settings", doc) doc = frappe.get_doc("CRM View Settings", doc)
doc.label = view.label doc.label = view.label
doc.type = view.type or "list" doc.type = view.type or "list"
doc.route_name = view.route_name or "" doc.route_name = view.route_name or get_route_name(view.doctype)
doc.load_default_columns = view.load_default_columns or False doc.load_default_columns = view.load_default_columns or False
doc.filters = json.dumps(filters) doc.filters = json.dumps(filters)
doc.order_by = view.order_by doc.order_by = view.order_by or "modified desc"
doc.group_by_field = view.group_by_field doc.group_by_field = view.group_by_field or "owner"
doc.column_field = view.column_field doc.column_field = view.column_field
doc.title_field = view.title_field doc.title_field = view.title_field
doc.kanban_columns = json.dumps(kanban_columns) doc.kanban_columns = json.dumps(kanban_columns)
doc.kanban_fields = json.dumps(kanban_fields) doc.kanban_fields = json.dumps(kanban_fields)
doc.columns = json.dumps(columns) doc.columns = json.dumps(columns)
doc.rows = json.dumps(rows) doc.rows = json.dumps(rows)
doc.is_default = view.is_default or False
doc.save() doc.save()
else: else:
doc = frappe.new_doc("CRM View Settings") doc = frappe.new_doc("CRM View Settings")
label = "Group By View" if view.type == "group_by" else "List View"
label = "List"
if view.type == "group_by":
label = "Group By"
elif view.type == "kanban":
label = "Kanban"
doc.name = view.label or label doc.name = view.label or label
doc.label = view.label or label doc.label = view.label or label
doc.type = view.type or "list" doc.type = view.type or "list"
doc.dt = view.doctype doc.dt = view.doctype
doc.user = frappe.session.user doc.user = frappe.session.user
doc.route_name = view.route_name or "" doc.route_name = view.route_name or get_route_name(view.doctype)
doc.load_default_columns = view.load_default_columns or False doc.load_default_columns = view.load_default_columns or False
doc.filters = json.dumps(filters) doc.filters = json.dumps(filters)
doc.order_by = view.order_by doc.order_by = view.order_by or "modified desc"
doc.group_by_field = view.group_by_field doc.group_by_field = view.group_by_field or "owner"
doc.column_field = view.column_field doc.column_field = view.column_field
doc.title_field = view.title_field doc.title_field = view.title_field
doc.kanban_columns = json.dumps(kanban_columns) doc.kanban_columns = json.dumps(kanban_columns)
doc.kanban_fields = json.dumps(kanban_fields) doc.kanban_fields = json.dumps(kanban_fields)
doc.columns = json.dumps(columns) doc.columns = json.dumps(columns)
doc.rows = json.dumps(rows) doc.rows = json.dumps(rows)
doc.is_default = True doc.is_standard = True
doc.is_default = view.is_default or False
doc.insert() doc.insert()
return doc
def get_route_name(doctype):
# Example: "CRM Lead" -> "Leads"
if doctype.startswith("CRM "):
doctype = doctype[4:]
if doctype[-1] != "s":
doctype += "s"
return doctype

View File

@ -6,8 +6,6 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"defaults_tab", "defaults_tab",
"default_view",
"column_break_jeeh",
"restore_defaults", "restore_defaults",
"branding_tab", "branding_tab",
"brand_name", "brand_name",
@ -58,21 +56,12 @@
"fieldname": "favicon", "fieldname": "favicon",
"fieldtype": "Attach", "fieldtype": "Attach",
"label": "Favicon" "label": "Favicon"
},
{
"fieldname": "default_view",
"fieldtype": "Data",
"label": "Default View"
},
{
"fieldname": "column_break_jeeh",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-02-18 17:05:39.440396", "modified": "2025-02-20 12:38:38.088477",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "FCRM Settings", "name": "FCRM Settings",

View File

@ -14,7 +14,7 @@
"@vueuse/core": "^10.3.0", "@vueuse/core": "^10.3.0",
"@vueuse/integrations": "^10.3.0", "@vueuse/integrations": "^10.3.0",
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
"frappe-ui": "^0.1.105", "frappe-ui": "^0.1.110",
"gemoji": "^8.1.0", "gemoji": "^8.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.1", "mime": "^4.0.1",

View File

@ -25,7 +25,19 @@
</template> </template>
<template #item-label="{ active, selected, option }"> <template #item-label="{ active, selected, option }">
<slot name="item-label" v-bind="{ active, selected, option }" /> <slot name="item-label" v-bind="{ active, selected, option }">
<div v-if="option.description" class="flex flex-col gap-1">
<div class="flex-1 font-semibold truncate text-ink-gray-7">
{{ option.label }}
</div>
<div class="flex-1 text-sm truncate text-ink-gray-5">
{{ option.description }}
</div>
</div>
<div v-else class="flex-1 truncate text-ink-gray-7">
{{ option.label }}
</div>
</slot>
</template> </template>
<template #footer="{ value, close }"> <template #footer="{ value, close }">
@ -131,8 +143,9 @@ const options = createResource({
transform: (data) => { transform: (data) => {
let allData = data.map((option) => { let allData = data.map((option) => {
return { return {
label: option.value, label: option.label || option.value,
value: option.value, value: option.value,
description: option.description,
} }
}) })
if (!props.hideMe && props.doctype == 'User') { if (!props.hideMe && props.doctype == 'User') {

View File

@ -226,6 +226,10 @@ import { ref, computed, watch } from 'vue'
const props = defineProps({ const props = defineProps({
tabs: Object, tabs: Object,
doctype: String, doctype: String,
onlyRequired: {
type: Boolean,
default: false,
},
}) })
const tabIndex = ref(0) const tabIndex = ref(0)
@ -249,6 +253,7 @@ const params = computed(() => {
doctype: props.doctype, doctype: props.doctype,
restricted_fieldtypes: restrictedFieldTypes, restricted_fieldtypes: restrictedFieldTypes,
as_array: true, as_array: true,
only_required: props.onlyRequired,
} }
}) })

View File

@ -190,6 +190,13 @@ function createDeal() {
if (deal.website && !deal.website.startsWith('http')) { if (deal.website && !deal.website.startsWith('http')) {
deal.website = 'https://' + deal.website 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({ createResource({
url: 'crm.fcrm.doctype.crm_deal.crm_deal.create_deal', url: 'crm.fcrm.doctype.crm_deal.crm_deal.create_deal',
params: { args: deal }, params: { args: deal },

View File

@ -35,6 +35,7 @@
v-if="!preview" v-if="!preview"
:tabs="tabs.data" :tabs="tabs.data"
:doctype="_doctype" :doctype="_doctype"
:onlyRequired="onlyRequired"
/> />
<FieldLayout v-else :tabs="tabs.data" :data="{}" :preview="true" /> <FieldLayout v-else :tabs="tabs.data" :data="{}" :preview="true" />
</div> </div>
@ -55,6 +56,10 @@ const props = defineProps({
type: String, type: String,
default: 'CRM Lead', default: 'CRM Lead',
}, },
onlyRequired: {
type: Boolean,
default: false,
},
}) })
const show = defineModel() const show = defineModel()
@ -64,12 +69,13 @@ const dirty = ref(false)
const preview = ref(false) const preview = ref(false)
function getParams() { 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({ const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', 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(), params: getParams(),
onSuccess(data) { onSuccess(data) {
tabs.originalData = JSON.parse(JSON.stringify(data)) tabs.originalData = JSON.parse(JSON.stringify(data))
@ -106,11 +112,12 @@ function saveChanges() {
}) })
}) })
loading.value = true loading.value = true
let type = props.onlyRequired ? 'Required Fields' : 'Quick Entry'
call( call(
'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout', 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout',
{ {
doctype: _doctype.value, doctype: _doctype.value,
type: 'Quick Entry', type: type,
layout: JSON.stringify(_tabs), layout: JSON.stringify(_tabs),
}, },
).then(() => { ).then(() => {

View File

@ -21,6 +21,7 @@
</template> </template>
<script setup> <script setup>
import LightningIcon from '@/components/Icons/LightningIcon.vue' import LightningIcon from '@/components/Icons/LightningIcon.vue'
import { capture } from '@/telemetry'
import { createResource } from 'frappe-ui' import { createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
@ -34,6 +35,7 @@ const props = defineProps({
const showBanner = ref(window.is_demo_site) const showBanner = ref(window.is_demo_site)
function signupNow() { function signupNow() {
capture('signup_from_demo_site')
window.open('https://frappecloud.com/crm/signup', '_blank') window.open('https://frappecloud.com/crm/signup', '_blank')
} }
</script> </script>

View File

@ -310,13 +310,13 @@ const currentView = computed(() => {
label: label:
_view?.label || props.options?.defaultViewName || getViewType().label, _view?.label || props.options?.defaultViewName || getViewType().label,
icon: _view?.icon || getViewType().icon, icon: _view?.icon || getViewType().icon,
is_default: !_view || _view.is_default, is_standard: !_view || _view.is_standard,
} }
}) })
usePageMeta(() => { usePageMeta(() => {
let label = currentView.value.label let label = currentView.value.label
if (currentView.value.is_default) { if (currentView.value.is_standard) {
let routeName = route.name let routeName = route.name
label = `${routeName} - ${label}` label = `${routeName} - ${label}`
} }
@ -480,11 +480,11 @@ async function exportRows() {
export_type.value = 'Excel' export_type.value = 'Excel'
} }
let defaultViews = [] let standardViews = []
let allowedViews = props.options.allowedViews || ['list'] let allowedViews = props.options.allowedViews || ['list']
if (allowedViews.includes('list')) { if (allowedViews.includes('list')) {
defaultViews.push({ standardViews.push({
name: 'list', name: 'list',
label: __(props.options?.defaultViewName) || __('List'), label: __(props.options?.defaultViewName) || __('List'),
icon: markRaw(ListIcon), icon: markRaw(ListIcon),
@ -495,7 +495,7 @@ if (allowedViews.includes('list')) {
}) })
} }
if (allowedViews.includes('kanban')) { if (allowedViews.includes('kanban')) {
defaultViews.push({ standardViews.push({
name: 'kanban', name: 'kanban',
label: __(props.options?.defaultViewName) || __('Kanban'), label: __(props.options?.defaultViewName) || __('Kanban'),
icon: markRaw(KanbanIcon), icon: markRaw(KanbanIcon),
@ -506,7 +506,7 @@ if (allowedViews.includes('kanban')) {
}) })
} }
if (allowedViews.includes('group_by')) { if (allowedViews.includes('group_by')) {
defaultViews.push({ standardViews.push({
name: 'group_by', name: 'group_by',
label: __(props.options?.defaultViewName) || __('Group By'), label: __(props.options?.defaultViewName) || __('Group By'),
icon: markRaw(GroupByIcon), icon: markRaw(GroupByIcon),
@ -531,9 +531,9 @@ function getIcon(icon, type) {
const viewsDropdownOptions = computed(() => { const viewsDropdownOptions = computed(() => {
let _views = [ let _views = [
{ {
group: __('Default Views'), group: __('Standard Views'),
hideLabel: true, hideLabel: true,
items: defaultViews, items: standardViews,
}, },
] ]
@ -558,7 +558,7 @@ const viewsDropdownOptions = computed(() => {
}) })
let publicViews = list.value.data.views.filter((v) => v.public) let publicViews = list.value.data.views.filter((v) => v.public)
let savedViews = list.value.data.views.filter( let savedViews = list.value.data.views.filter(
(v) => !v.pinned && !v.public && !v.is_default, (v) => !v.pinned && !v.public && !v.is_standard,
) )
let pinnedViews = list.value.data.views.filter((v) => v.pinned) let pinnedViews = list.value.data.views.filter((v) => v.pinned)
@ -662,7 +662,7 @@ function updateFilter(filters) {
list.value.reload() list.value.reload()
if (!route.query.view) { if (!route.query.view) {
create_or_update_default_view() createOrUpdateStandardView()
} }
} }
@ -677,7 +677,7 @@ function updateSort(order_by) {
list.value.reload() list.value.reload()
if (!route.query.view) { if (!route.query.view) {
create_or_update_default_view() createOrUpdateStandardView()
} }
} }
@ -692,7 +692,7 @@ function updateGroupBy(group_by_field) {
list.value.reload() list.value.reload()
if (!route.query.view) { if (!route.query.view) {
create_or_update_default_view() createOrUpdateStandardView()
} }
} }
@ -726,7 +726,7 @@ function updateColumns(obj) {
viewUpdated.value = true viewUpdated.value = true
if (!route.query.view) { if (!route.query.view) {
create_or_update_default_view() createOrUpdateStandardView()
} }
} }
@ -768,7 +768,7 @@ async function updateKanbanSettings(data) {
list.value.reload() list.value.reload()
if (!route.query.view) { if (!route.query.view) {
create_or_update_default_view() createOrUpdateStandardView()
} else if (!data.column_field) { } else if (!data.column_field) {
if (isDirty) { if (isDirty) {
$dialog({ $dialog({
@ -780,14 +780,14 @@ async function updateKanbanSettings(data) {
label: __('Update'), label: __('Update'),
variant: 'solid', variant: 'solid',
onClick: (close) => { onClick: (close) => {
update_custom_view() updateCustomView()
close() close()
}, },
}, },
], ],
}) })
} else { } else {
update_custom_view() updateCustomView()
} }
} }
} }
@ -811,11 +811,11 @@ function loadMoreKanban(columnName) {
list.value.reload() list.value.reload()
} }
function create_or_update_default_view() { function createOrUpdateStandardView() {
if (route.query.view) return if (route.query.view) return
view.value.doctype = props.doctype view.value.doctype = props.doctype
call( call(
'crm.fcrm.doctype.crm_view_settings.crm_view_settings.create_or_update_default_view', 'crm.fcrm.doctype.crm_view_settings.crm_view_settings.create_or_update_standard_view',
{ {
view: view.value, view: view.value,
}, },
@ -842,7 +842,7 @@ function create_or_update_default_view() {
}) })
} }
function update_custom_view() { function updateCustomView() {
viewUpdated.value = false viewUpdated.value = false
view.value = { view.value = {
doctype: props.doctype, doctype: props.doctype,
@ -897,6 +897,7 @@ const viewActions = (view) => {
if (!_view) { if (!_view) {
_view = { _view = {
label: view.label,
type: view.name, type: view.name,
dt: props.doctype, dt: props.doctype,
} }
@ -904,7 +905,7 @@ const viewActions = (view) => {
let actions = [ let actions = [
{ {
group: __('Default Views'), group: __('Actions'),
hideLabel: true, hideLabel: true,
items: [ items: [
{ {
@ -916,7 +917,7 @@ const viewActions = (view) => {
}, },
] ]
if (!isStandardView(_view, isStandard)) { if (!isDefaultView(_view, isStandard)) {
actions[0].items.unshift({ actions[0].items.unshift({
label: __('Set as default'), label: __('Set as default'),
icon: () => h(CheckIcon, { class: 'h-4 w-4' }), icon: () => h(CheckIcon, { class: 'h-4 w-4' }),
@ -981,16 +982,12 @@ const viewActions = (view) => {
return actions return actions
} }
function isStandardView(v, isStandard) { function isDefaultView(v, isStandard) {
let defaultView = getDefaultView() let defaultView = getDefaultView()
if (!defaultView) return false if (!defaultView || (isStandard && !v.name)) return false
if (isStandard && !v.name) { return defaultView.name == v.name
return defaultView == v.type + '_' + v.dt
}
return defaultView == v.name
} }
const viewModalObj = ref({}) const viewModalObj = ref({})

View File

@ -186,7 +186,6 @@
<Dialog <Dialog
v-model="showConvertToDealModal" v-model="showConvertToDealModal"
:options="{ :options="{
title: __('Convert to Deal'),
size: 'xl', size: 'xl',
actions: [ 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> <template #body-content>
<div class="mb-4 flex items-center gap-2 text-ink-gray-5"> <div class="mb-4 flex items-center gap-2 text-ink-gray-5">
<OrganizationsIcon class="h-4 w-4" /> <OrganizationsIcon class="h-4 w-4" />
<label class="block text-base">{{ __('Organization') }}</label> <label class="block text-base">{{ __('Organization') }}</label>
</div> </div>
<div class="ml-6"> <div class="ml-6 text-ink-gray-9">
<div class="flex items-center justify-between text-base"> <div class="flex items-center justify-between text-base">
<div>{{ __('Choose Existing') }}</div> <div>{{ __('Choose Existing') }}</div>
<Switch v-model="existingOrganizationChecked" /> <Switch v-model="existingOrganizationChecked" />
@ -210,7 +235,6 @@
<Link <Link
v-if="existingOrganizationChecked" v-if="existingOrganizationChecked"
class="form-control mt-2.5" class="form-control mt-2.5"
variant="outline"
size="md" size="md"
:value="existingOrganization" :value="existingOrganization"
doctype="CRM Organization" doctype="CRM Organization"
@ -229,7 +253,7 @@
<ContactsIcon class="h-4 w-4" /> <ContactsIcon class="h-4 w-4" />
<label class="block text-base">{{ __('Contact') }}</label> <label class="block text-base">{{ __('Contact') }}</label>
</div> </div>
<div class="ml-6"> <div class="ml-6 text-ink-gray-9">
<div class="flex items-center justify-between text-base"> <div class="flex items-center justify-between text-base">
<div>{{ __('Choose Existing') }}</div> <div>{{ __('Choose Existing') }}</div>
<Switch v-model="existingContactChecked" /> <Switch v-model="existingContactChecked" />
@ -237,7 +261,6 @@
<Link <Link
v-if="existingContactChecked" v-if="existingContactChecked"
class="form-control mt-2.5" class="form-control mt-2.5"
variant="outline"
size="md" size="md"
:value="existingContact" :value="existingContact"
doctype="Contact" doctype="Contact"
@ -247,8 +270,23 @@
{{ __("New contact will be created based on the person's details") }} {{ __("New contact will be created based on the person's details") }}
</div> </div>
</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> </template>
</Dialog> </Dialog>
<QuickEntryModal
v-if="showQuickEntryModal"
v-model="showQuickEntryModal"
doctype="CRM Deal"
:onlyRequired="true"
/>
<FilesUploader <FilesUploader
v-if="lead.data?.name" v-if="lead.data?.name"
v-model="showFilesUploader" v-model="showFilesUploader"
@ -280,12 +318,15 @@ import LinkIcon from '@/components/Icons/LinkIcon.vue'
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue' import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue' import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue' import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import LayoutHeader from '@/components/LayoutHeader.vue' import LayoutHeader from '@/components/LayoutHeader.vue'
import Activities from '@/components/Activities/Activities.vue' import Activities from '@/components/Activities/Activities.vue'
import AssignTo from '@/components/AssignTo.vue' import AssignTo from '@/components/AssignTo.vue'
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue' import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.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 SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
import { import {
@ -298,10 +339,15 @@ import {
} from '@/utils' } from '@/utils'
import { getView } from '@/utils/view' import { getView } from '@/utils/view'
import { getSettings } from '@/stores/settings' import { getSettings } from '@/stores/settings'
import { usersStore } from '@/stores/users'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { contactsStore } from '@/stores/contacts' import { contactsStore } from '@/stores/contacts'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { whatsappEnabled, callEnabled } from '@/composables/settings' import {
whatsappEnabled,
callEnabled,
isMobileView,
} from '@/composables/settings'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { import {
createResource, createResource,
@ -315,14 +361,15 @@ import {
call, call,
usePageMeta, usePageMeta,
} from 'frappe-ui' } 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 { useRouter, useRoute } from 'vue-router'
import { useActiveTabManager } from '@/composables/useActiveTabManager' import { useActiveTabManager } from '@/composables/useActiveTabManager'
const { brand } = getSettings() const { brand } = getSettings()
const { isManager } = usersStore()
const { $dialog, $socket, makeCall } = globalStore() const { $dialog, $socket, makeCall } = globalStore()
const { getContactByName, contacts } = contactsStore() const { getContactByName, contacts } = contactsStore()
const { statusOptions, getLeadStatus } = statusesStore() const { statusOptions, getLeadStatus, getDealStatus } = statusesStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -599,18 +646,23 @@ async function convertToDeal(updated) {
) )
showConvertToDealModal.value = false showConvertToDealModal.value = false
} else { } else {
let deal = await call( let _deal = await call(
'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', 'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal',
{ { lead: lead.data.name, deal },
lead: lead.data.name, ).catch((err) => {
}, createToast({
) title: __('Error converting to deal'),
if (deal) { text: __(err.messages?.[0]),
icon: 'x',
iconClasses: 'text-ink-red-4',
})
})
if (_deal) {
capture('convert_lead_to_deal') capture('convert_lead_to_deal')
if (updated) { if (updated) {
await contacts.reload() 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() { function openEmailBox() {
activities.value.emailBox.show = true 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> </script>

View File

@ -113,23 +113,22 @@ router.beforeEach(async (to, from, next) => {
isLoggedIn && (await userResource.promise) isLoggedIn && (await userResource.promise)
if (to.name === 'Home' && isLoggedIn) { if (to.name === 'Home' && isLoggedIn) {
const { getDefaultView, defaultView } = viewsStore() const { views, getDefaultView } = viewsStore()
await defaultView.promise await views.promise
let _defaultView = getDefaultView(true) let defaultView = getDefaultView()
if (!defaultView) {
if (!_defaultView) {
next({ name: 'Leads' }) next({ name: 'Leads' })
return return
} }
let { name, type, view } = _defaultView let { route_name, type, name, is_standard } = defaultView
name = name || 'Leads' route_name = route_name || 'Leads'
if (view) { if (name && !is_standard) {
next({ name, params: { viewType: type }, query: { view } }) next({ name: route_name, params: { viewType: type }, query: { name } })
} else { } else {
next({ name, params: { viewType: type } }) next({ name: route_name, params: { viewType: type } })
} }
} else if (!isLoggedIn) { } else if (!isLoggedIn) {
window.location.href = '/login?redirect-to=/crm' window.location.href = '/login?redirect-to=/crm'

View File

@ -7,13 +7,7 @@ export const viewsStore = defineStore('crm-views', (doctype) => {
let pinnedViews = ref([]) let pinnedViews = ref([])
let publicViews = ref([]) let publicViews = ref([])
let standardViews = ref({}) let standardViews = ref({})
const defaultView = ref(null)
// Default view
const defaultView = createResource({
url: 'crm.api.views.get_default_view',
cache: 'crm-default-view',
auto: true,
})
// Views // Views
const views = createResource({ const views = createResource({
@ -34,48 +28,19 @@ export const viewsStore = defineStore('crm-views', (doctype) => {
if (view.public) { if (view.public) {
publicViews.value?.push(view) publicViews.value?.push(view)
} }
if (view.is_default && view.dt) { if (view.is_standard && view.dt) {
standardViews.value[view.dt + ' ' + view.type] = view standardViews.value[view.dt + ' ' + view.type] = view
} }
if (view.is_default) {
defaultView.value = view
}
} }
return views return views
}, },
}) })
function getDefaultView(routeName = false) { function getDefaultView() {
let view = defaultView.data return defaultView.value
if (!view) return null
if (typeof view === 'string' && !isNaN(view)) {
view = parseInt(view)
}
if (routeName) {
let viewObj = getView(view) || {
type: view.split('_')[0],
dt: view.split('_')[1],
}
let routeName = viewObj.dt
if (routeName.startsWith('CRM ')) {
routeName = routeName.slice(4)
}
if (!routeName.endsWith('s')) {
routeName += 's'
}
let viewName = viewObj.is_default ? null : viewObj.name
return {
name: routeName,
type: viewObj.type,
view: viewName,
}
}
return view
} }
function getView(view, type, doctype = null) { function getView(view, type, doctype = null) {

View File

@ -6,7 +6,7 @@ import { markRaw } from 'vue'
const { getView: getViewDetails } = viewsStore() const { getView: getViewDetails } = viewsStore()
function defaultView(type) { function standardView(type) {
let types = { let types = {
list: { list: {
label: __('List'), label: __('List'),
@ -29,7 +29,7 @@ export function getView(view, type, doctype) {
let viewType = type || 'list' let viewType = type || 'list'
let viewDetails = getViewDetails(view, viewType, doctype) let viewDetails = getViewDetails(view, viewType, doctype)
if (viewDetails && !viewDetails.icon) { if (viewDetails && !viewDetails.icon) {
viewDetails.icon = defaultView(viewType).icon viewDetails.icon = standardView(viewType).icon
} }
return viewDetails || defaultView(viewType) return viewDetails || standardView(viewType)
} }

View File

@ -1136,10 +1136,10 @@
dependencies: dependencies:
mini-svg-data-uri "^1.2.3" mini-svg-data-uri "^1.2.3"
"@tailwindcss/typography@^0.5.0": "@tailwindcss/typography@^0.5.16":
version "0.5.15" version "0.5.16"
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.15.tgz#007ab9870c86082a1c76e5b3feda9392c7c8d648" resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.16.tgz#a926c8f44d5c439b2915e231cad80058850047c6"
integrity sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA== integrity sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==
dependencies: dependencies:
lodash.castarray "^4.4.0" lodash.castarray "^4.4.0"
lodash.isplainobject "^4.0.6" 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" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
frappe-ui@^0.1.105: frappe-ui@^0.1.110:
version "0.1.105" version "0.1.110"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.105.tgz#3bdf3c458ba27f27ff2f2a28cf7eb6f9ed872367" resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.110.tgz#dbe02d294cb0aeb0a4c1b3000682093c309450ae"
integrity sha512-9bZ/hj/HhQ9vp7DxE8aOKS8HqwETZrKT3IhSzjpYOk21efK8QwdbQ9sp0t4m3UII+HaUTSOTHnFzF7y9EhRZxg== integrity sha512-kFah6SoPauULXaeSbljNUq595/82VmY4k4+KA8zi4sXxpn4sXYi12qUl/1I8GOBhsCQQizmoh46DO7e/uU2M1A==
dependencies: dependencies:
"@headlessui/vue" "^1.7.14" "@headlessui/vue" "^1.7.14"
"@popperjs/core" "^2.11.2" "@popperjs/core" "^2.11.2"
"@tailwindcss/forms" "^0.5.3" "@tailwindcss/forms" "^0.5.3"
"@tailwindcss/typography" "^0.5.0" "@tailwindcss/typography" "^0.5.16"
"@tiptap/extension-color" "^2.0.3" "@tiptap/extension-color" "^2.0.3"
"@tiptap/extension-highlight" "^2.0.3" "@tiptap/extension-highlight" "^2.0.3"
"@tiptap/extension-image" "^2.0.3" "@tiptap/extension-image" "^2.0.3"