1
0
forked from test/crm

Merge pull request #1287 from frappe/main-hotfix

This commit is contained in:
Shariq Ansari 2025-09-25 22:02:44 +05:30 committed by GitHub
commit 3e9fdb8d20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 8111 additions and 6324 deletions

View File

@ -750,7 +750,11 @@ def getCounts(d, doctype):
@frappe.whitelist() @frappe.whitelist()
def get_linked_docs_of_document(doctype, docname): def get_linked_docs_of_document(doctype, docname):
doc = frappe.get_doc(doctype, docname) try:
doc = frappe.get_doc(doctype, docname)
except frappe.DoesNotExistError:
return []
linked_docs = get_linked_docs(doc) linked_docs = get_linked_docs(doc)
dynamic_linked_docs = get_dynamic_linked_docs(doc) dynamic_linked_docs = get_dynamic_linked_docs(doc)
@ -759,7 +763,14 @@ def get_linked_docs_of_document(doctype, docname):
docs_data = [] docs_data = []
for doc in linked_docs: for doc in linked_docs:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"]) if not doc.get("reference_doctype") or not doc.get("reference_docname"):
continue
try:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
continue
title = data.get("title") title = data.get("title")
if data.doctype == "CRM Call Log": if data.doctype == "CRM Call Log":
title = f"Call from {data.get('from')} to {data.get('to')}" title = f"Call from {data.get('from')} to {data.get('to')}"
@ -767,6 +778,9 @@ def get_linked_docs_of_document(doctype, docname):
if data.doctype == "CRM Deal": if data.doctype == "CRM Deal":
title = data.get("organization") title = data.get("organization")
if data.doctype == "CRM Notification":
title = data.get("message")
docs_data.append( docs_data.append(
{ {
"doc": data.doctype, "doc": data.doctype,
@ -779,25 +793,51 @@ def get_linked_docs_of_document(doctype, docname):
def remove_doc_link(doctype, docname): def remove_doc_link(doctype, docname):
linked_doc_data = frappe.get_doc(doctype, docname) if not doctype or not docname:
linked_doc_data.update( return
{
"reference_doctype": None, try:
"reference_docname": None, linked_doc_data = frappe.get_doc(doctype, docname)
} if doctype == "CRM Notification":
) delete_notification_type = {
linked_doc_data.save(ignore_permissions=True) "notification_type_doctype": "",
"notification_type_doc": "",
}
delete_references = {
"reference_doctype": "",
"reference_name": "",
}
if linked_doc_data.get("notification_type_doctype") == linked_doc_data.get("reference_doctype"):
delete_references.update(delete_notification_type)
linked_doc_data.update(delete_references)
else:
linked_doc_data.update(
{
"reference_doctype": "",
"reference_docname": "",
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
def remove_contact_link(doctype, docname): def remove_contact_link(doctype, docname):
linked_doc_data = frappe.get_doc(doctype, docname) if not doctype or not docname:
linked_doc_data.update( return
{
"contact": None, try:
"contacts": [], linked_doc_data = frappe.get_doc(doctype, docname)
} linked_doc_data.update(
) {
linked_doc_data.save(ignore_permissions=True) "contact": None,
"contacts": [],
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
@frappe.whitelist() @frappe.whitelist()
@ -806,13 +846,19 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
items = frappe.parse_json(items) items = frappe.parse_json(items)
for item in items: for item in items:
if remove_contact: if not item.get("doctype") or not item.get("docname"):
remove_contact_link(item["doctype"], item["docname"]) continue
else:
remove_doc_link(item["doctype"], item["docname"])
if delete: try:
frappe.delete_doc(item["doctype"], item["docname"]) if remove_contact:
remove_contact_link(item["doctype"], item["docname"])
else:
remove_doc_link(item["doctype"], item["docname"])
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
# Skip if document doesn't exist or has validation errors
continue
return "success" return "success"
@ -821,19 +867,40 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
def delete_bulk_docs(doctype, items, delete_linked=False): def delete_bulk_docs(doctype, items, delete_linked=False):
from frappe.desk.reportview import delete_bulk from frappe.desk.reportview import delete_bulk
if not doctype:
frappe.throw("Doctype is required")
if not items:
frappe.throw("Items are required")
items = frappe.parse_json(items) items = frappe.parse_json(items)
if not isinstance(items, list):
frappe.throw("Items must be a list")
for doc in items: for doc in items:
linked_docs = get_linked_docs_of_document(doctype, doc) try:
for linked_doc in linked_docs: if not frappe.db.exists(doctype, doc):
remove_linked_doc_reference( frappe.log_error(f"Document {doctype} {doc} does not exist", "Bulk Delete Error")
[ continue
{
"doctype": linked_doc["reference_doctype"], linked_docs = get_linked_docs_of_document(doctype, doc)
"docname": linked_doc["reference_docname"], for linked_doc in linked_docs:
} if not linked_doc.get("reference_doctype") or not linked_doc.get("reference_docname"):
], continue
remove_contact=doctype == "Contact",
delete=delete_linked, remove_linked_doc_reference(
[
{
"doctype": linked_doc["reference_doctype"],
"docname": linked_doc["reference_docname"],
}
],
remove_contact=doctype == "Contact",
delete=delete_linked,
)
except Exception as e:
frappe.log_error(
f"Error processing linked docs for {doctype} {doc}: {str(e)}", "Bulk Delete Error"
) )
if len(items) > 10: if len(items) > 10:

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

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

@ -13,7 +13,7 @@
"@tiptap/extension-paragraph": "^2.12.0", "@tiptap/extension-paragraph": "^2.12.0",
"@twilio/voice-sdk": "^2.10.2", "@twilio/voice-sdk": "^2.10.2",
"@vueuse/integrations": "^10.3.0", "@vueuse/integrations": "^10.3.0",
"frappe-ui": "^0.1.200", "frappe-ui": "^0.1.201",
"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

@ -13,7 +13,7 @@
</div> </div>
</div> </div>
<div> <div>
<div class="text-ink-gray-5"> <div class="text-ink-gray-5 text-base">
{{ {{
__('Are you sure you want to delete {0} items?', [ __('Are you sure you want to delete {0} items?', [
props.items?.length, props.items?.length,
@ -53,7 +53,7 @@
</div> </div>
</div> </div>
<div> <div>
<div class="text-ink-gray-5"> <div class="text-ink-gray-5 text-base">
{{ {{
confirmDeleteInfo.delete confirmDeleteInfo.delete
? __( ? __(

View File

@ -2,7 +2,7 @@
<Dialog v-model="show" :options="{ size: 'xl' }"> <Dialog v-model="show" :options="{ size: 'xl' }">
<template #body v-if="!confirmDeleteInfo.show"> <template #body v-if="!confirmDeleteInfo.show">
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6"> <div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-4 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<div> <div>
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold"> <h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
{{ {{
@ -32,11 +32,12 @@
{ {
label: 'Document', label: 'Document',
key: 'title', key: 'title',
width: '19rem',
}, },
{ {
label: 'Master', label: 'Master',
key: 'reference_doctype', key: 'reference_doctype',
width: '30%', width: '12rem',
}, },
]" ]"
@selectionsChanged=" @selectionsChanged="

View File

@ -26,13 +26,14 @@
<ListRowItem <ListRowItem
:item="item" :item="item"
@click="listViewRef.toggleRow(row['reference_docname'])" @click="listViewRef.toggleRow(row['reference_docname'])"
class="!w-full"
> >
<template #default="{ label }"> <template #default="{ label }">
<div <div
v-if="column.key === 'title'" v-if="column.key === 'title'"
class="truncate text-base flex gap-2" class="truncate text-base flex gap-2 w-full"
> >
<span> <span class="max-w-[90%] truncate">
{{ label }} {{ label }}
</span> </span>
<FeatherIcon <FeatherIcon
@ -102,6 +103,7 @@ const listViewRef = ref(null)
const viewLinkedDoc = (doc) => { const viewLinkedDoc = (doc) => {
let page = '' let page = ''
let id = '' let id = ''
let openDesk = false
switch (doc.reference_doctype) { switch (doc.reference_doctype) {
case 'CRM Lead': case 'CRM Lead':
page = 'leads' page = 'leads'
@ -123,6 +125,11 @@ const viewLinkedDoc = (doc) => {
page = 'organizations' page = 'organizations'
id = doc.reference_docname id = doc.reference_docname
break break
case 'CRM Notification':
page = 'crm-notification'
id = doc.reference_docname
openDesk = true
break
case 'FCRM Note': case 'FCRM Note':
page = 'notes' page = 'notes'
id = `view?open=${doc.reference_docname}` id = `view?open=${doc.reference_docname}`
@ -130,7 +137,11 @@ const viewLinkedDoc = (doc) => {
default: default:
break break
} }
window.open(`/crm/${page}/${id}`) let base = '/crm'
if (openDesk) {
base = '/app'
}
window.open(`${base}/${page}/${id}`)
} }
const getDoctypeName = (doctype) => { const getDoctypeName = (doctype) => {

View File

@ -1,7 +1,8 @@
import { getScript } from '@/data/script' import { getScript } from '@/data/script'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { getMeta } from '@/stores/meta'
import { showSettings, activeSettingsPage } from '@/composables/settings' import { showSettings, activeSettingsPage } from '@/composables/settings'
import { runSequentially, parseAssignees } from '@/utils' import { runSequentially, parseAssignees, evaluateExpression } from '@/utils'
import { createDocumentResource, createResource, toast } from 'frappe-ui' import { createDocumentResource, createResource, toast } from 'frappe-ui'
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
@ -11,6 +12,7 @@ const assigneesCache = {}
export function useDocument(doctype, docname) { export function useDocument(doctype, docname) {
const { setupScript, scripts } = getScript(doctype) const { setupScript, scripts } = getScript(doctype)
const meta = getMeta(doctype)
documentsCache[doctype] = documentsCache[doctype] || {} documentsCache[doctype] = documentsCache[doctype] || {}
@ -37,6 +39,7 @@ export function useDocument(doctype, docname) {
} }
}, },
setValue: { setValue: {
validate,
onSuccess: () => { onSuccess: () => {
triggerOnSave() triggerOnSave()
toast.success(__('Document updated successfully')) toast.success(__('Document updated successfully'))
@ -152,6 +155,42 @@ export function useDocument(doctype, docname) {
return [] return []
} }
function validate(d) {
checkMandatory(d.doc || d.fieldname)
}
function checkMandatory(doc) {
let fields = meta?.getFields() || []
if (!fields || fields.length === 0) return
let missingFields = []
fields.forEach((df) => {
let parent = meta?.doctypeMeta?.[df.parent] || null
if (evaluateExpression(df.mandatory_depends_on, doc, parent)) {
const value = doc[df.fieldname]
if (
value === undefined ||
value === null ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0)
) {
missingFields.push(df.label || df.fieldname)
}
}
})
if (missingFields.length > 0) {
toast.error(
__('Mandatory fields required: {0}', [missingFields.join(', ')]),
)
throw new Error(
__('Mandatory fields required: {0}', [missingFields.join(', ')]),
)
}
}
async function triggerOnLoad() { async function triggerOnLoad() {
const handler = async function () { const handler = async function () {
await (this.onLoad?.() || this.on_load?.() || this.onload?.()) await (this.onLoad?.() || this.on_load?.() || this.onload?.())
@ -280,6 +319,7 @@ export function useDocument(doctype, docname) {
assignees: assigneesCache[doctype][docname || ''], assignees: assigneesCache[doctype][docname || ''],
scripts, scripts,
error, error,
validate,
getControllers, getControllers,
triggerOnLoad, triggerOnLoad,
triggerOnBeforeCreate, triggerOnBeforeCreate,

View File

@ -421,6 +421,36 @@ export function evaluateDependsOnValue(expression, doc) {
return out return out
} }
export function evaluateExpression(expression, doc, parent) {
if (!expression) return false
if (!doc) return false
let out = null
if (typeof expression === 'boolean') {
out = expression
} else if (typeof expression === 'function') {
out = expression(doc)
} else if (expression.substr(0, 5) == 'eval:') {
try {
out = _eval(expression.substr(5), { doc, parent })
if (parent && parent.istable && expression.includes('is_submittable')) {
out = true
}
} catch (e) {
out = true
}
} else {
let value = doc[expression]
if (Array.isArray(value)) {
out = !!value.length
} else {
out = !!value
}
}
return out
}
export function convertSize(size) { export function convertSize(size) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'] const units = ['B', 'KB', 'MB', 'GB', 'TB']
let unitIndex = 0 let unitIndex = 0

View File

@ -2572,10 +2572,10 @@ 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.200: frappe-ui@^0.1.201:
version "0.1.200" version "0.1.201"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.200.tgz#2cdaa24708f0dbe98b0dd6c2536b5974418fdfef" resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.201.tgz#12a18f08b97489facd4a170b4cbfc8adbbf1f109"
integrity sha512-mlkGS5oZxKYcpAit6qehjmoYqo6NvrWmSrbhvkpYDXecEBWU3IgJkMzt13753Z3QTbWbJv7y3iadlVge8u+FMw== integrity sha512-kVz9K3W22ZuxGScSCct2kUFexZ1uZPM6FCc5BWAep3UcEzHRSbHzmLyEKCu0FKzftXtDHwNG8AoUDyHrDPkvCw==
dependencies: dependencies:
"@floating-ui/vue" "^1.1.6" "@floating-ui/vue" "^1.1.6"
"@headlessui/vue" "^1.7.14" "@headlessui/vue" "^1.7.14"