Merge pull request #817 from frappe/main-hotfix
This commit is contained in:
commit
7396878a3b
@ -5,4 +5,68 @@ frappe.ui.form.on("CRM Deal", {
|
|||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
frm.add_web_link(`/crm/deals/${frm.doc.name}`, __("Open in Portal"));
|
frm.add_web_link(`/crm/deals/${frm.doc.name}`, __("Open in Portal"));
|
||||||
},
|
},
|
||||||
|
update_total: function (frm) {
|
||||||
|
let total = 0;
|
||||||
|
let total_qty = 0;
|
||||||
|
let net_total = 0;
|
||||||
|
frm.doc.products.forEach((d) => {
|
||||||
|
total += d.amount;
|
||||||
|
total_qty += d.qty;
|
||||||
|
net_total += d.net_amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
|
||||||
|
frappe.model.set_value(
|
||||||
|
frm.doctype,
|
||||||
|
frm.docname,
|
||||||
|
"net_total",
|
||||||
|
net_total || total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("CRM Products", {
|
||||||
|
products_add: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
products_remove: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
product_code: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
|
||||||
|
},
|
||||||
|
rate: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
qty: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
discount_percentage: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.discount_percentage && d.amount) {
|
||||||
|
discount_amount = (d.discount_percentage / 100) * d.amount;
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"discount_amount",
|
||||||
|
discount_amount
|
||||||
|
);
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"net_amount",
|
||||||
|
d.amount - discount_amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -43,6 +43,12 @@
|
|||||||
"mobile_no",
|
"mobile_no",
|
||||||
"phone",
|
"phone",
|
||||||
"gender",
|
"gender",
|
||||||
|
"products_tab",
|
||||||
|
"products",
|
||||||
|
"section_break_ccbj",
|
||||||
|
"total",
|
||||||
|
"column_break_udbq",
|
||||||
|
"net_total",
|
||||||
"sla_tab",
|
"sla_tab",
|
||||||
"sla",
|
"sla",
|
||||||
"sla_creation",
|
"sla_creation",
|
||||||
@ -334,11 +340,46 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
"options": "Currency"
|
"options": "Currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Products",
|
||||||
|
"options": "CRM Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ccbj",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_udbq",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Total after discount",
|
||||||
|
"fieldname": "net_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-11 14:31:41.058895",
|
"modified": "2025-05-12 12:30:55.415282",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
@ -370,6 +411,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
|||||||
@ -27,7 +27,9 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
|
|||||||
if not tabs and type != "Required Fields":
|
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 = False
|
||||||
|
if isinstance(tabs, list) and len(tabs) > 0 and isinstance(tabs[0], dict):
|
||||||
|
has_tabs = any("sections" in tab for tab in tabs)
|
||||||
|
|
||||||
if not has_tabs:
|
if not has_tabs:
|
||||||
tabs = [{"name": "first_tab", "sections": tabs}]
|
tabs = [{"name": "first_tab", "sections": tabs}]
|
||||||
@ -47,7 +49,10 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
|
|||||||
|
|
||||||
for tab in tabs:
|
for tab in tabs:
|
||||||
for section in tab.get("sections"):
|
for section in tab.get("sections"):
|
||||||
|
if section.get("columns"):
|
||||||
|
section["columns"] = [column for column in section.get("columns") if column]
|
||||||
for column in section.get("columns") if section.get("columns") else []:
|
for column in section.get("columns") if section.get("columns") else []:
|
||||||
|
column["fields"] = [field for field in column.get("fields") if field]
|
||||||
for field in column.get("fields") if column.get("fields") else []:
|
for field in column.get("fields") if column.get("fields") else []:
|
||||||
field = next((f for f in fields if f.fieldname == field), None)
|
field = next((f for f in fields if f.fieldname == field), None)
|
||||||
if field:
|
if field:
|
||||||
|
|||||||
@ -5,4 +5,68 @@ frappe.ui.form.on("CRM Lead", {
|
|||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
frm.add_web_link(`/crm/leads/${frm.doc.name}`, __("Open in Portal"));
|
frm.add_web_link(`/crm/leads/${frm.doc.name}`, __("Open in Portal"));
|
||||||
},
|
},
|
||||||
|
update_total: function (frm) {
|
||||||
|
let total = 0;
|
||||||
|
let total_qty = 0;
|
||||||
|
let net_total = 0;
|
||||||
|
frm.doc.products.forEach((d) => {
|
||||||
|
total += d.amount;
|
||||||
|
total_qty += d.qty;
|
||||||
|
net_total += d.net_amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
|
||||||
|
frappe.model.set_value(
|
||||||
|
frm.doctype,
|
||||||
|
frm.docname,
|
||||||
|
"net_total",
|
||||||
|
net_total || total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("CRM Products", {
|
||||||
|
products_add: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
products_remove: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
product_code: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
|
||||||
|
},
|
||||||
|
rate: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
qty: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
discount_percentage: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.discount_percentage && d.amount) {
|
||||||
|
discount_amount = (d.discount_percentage / 100) * d.amount;
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"discount_amount",
|
||||||
|
discount_amount
|
||||||
|
);
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"net_amount",
|
||||||
|
d.amount - discount_amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@ -37,6 +37,12 @@
|
|||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"image",
|
"image",
|
||||||
"converted",
|
"converted",
|
||||||
|
"products_tab",
|
||||||
|
"products",
|
||||||
|
"section_break_ggwh",
|
||||||
|
"total",
|
||||||
|
"column_break_uisv",
|
||||||
|
"net_total",
|
||||||
"sla_tab",
|
"sla_tab",
|
||||||
"sla",
|
"sla",
|
||||||
"sla_creation",
|
"sla_creation",
|
||||||
@ -285,12 +291,47 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Status Change Log",
|
"label": "Status Change Log",
|
||||||
"options": "CRM Status Change Log"
|
"options": "CRM Status Change Log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Products",
|
||||||
|
"options": "CRM Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ggwh",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_uisv",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Total after discount",
|
||||||
|
"fieldname": "net_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-02 22:14:01.991054",
|
"modified": "2025-05-14 19:51:06.184569",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Lead",
|
"name": "CRM Lead",
|
||||||
@ -331,6 +372,7 @@
|
|||||||
"share": 1
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sender_field": "email",
|
"sender_field": "email",
|
||||||
"sender_name_field": "first_name",
|
"sender_name_field": "first_name",
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
|
|||||||
0
crm/fcrm/doctype/crm_product/__init__.py
Normal file
0
crm/fcrm/doctype/crm_product/__init__.py
Normal file
9
crm/fcrm/doctype/crm_product/crm_product.js
Normal file
9
crm/fcrm/doctype/crm_product/crm_product.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on("CRM Product", {
|
||||||
|
product_code: function (frm) {
|
||||||
|
if (!frm.doc.product_name)
|
||||||
|
frm.set_value("product_name", frm.doc.product_code);
|
||||||
|
}
|
||||||
|
});
|
||||||
105
crm/fcrm/doctype/crm_product/crm_product.json
Normal file
105
crm/fcrm/doctype/crm_product/crm_product.json
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_import": 1,
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:product_code",
|
||||||
|
"creation": "2025-04-28 11:45:09.309636",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
|
"product_code",
|
||||||
|
"product_name",
|
||||||
|
"column_break_bpdj",
|
||||||
|
"disabled",
|
||||||
|
"standard_rate",
|
||||||
|
"image",
|
||||||
|
"section_break_rtwm",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Naming Series",
|
||||||
|
"options": "CRM-PROD-.YYYY.-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "product_code",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Product Code",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "product_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Product Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bpdj",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "disabled",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Disabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_rtwm",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "standard_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Standard Selling Rate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"image_field": "image",
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"make_attachments_public": 1,
|
||||||
|
"modified": "2025-04-28 12:47:25.087957",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "CRM Product",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"search_fields": "product_name,description",
|
||||||
|
"show_name_in_global_search": 1,
|
||||||
|
"show_preview_popup": 1,
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "product_name",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
16
crm/fcrm/doctype/crm_product/crm_product.py
Normal file
16
crm/fcrm/doctype/crm_product/crm_product.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMProduct(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.set_product_name()
|
||||||
|
|
||||||
|
def set_product_name(self):
|
||||||
|
if not self.product_name:
|
||||||
|
self.product_name = self.product_code
|
||||||
|
else:
|
||||||
|
self.product_name = self.product_name.strip()
|
||||||
29
crm/fcrm/doctype/crm_product/test_crm_product.py
Normal file
29
crm/fcrm/doctype/crm_product/test_crm_product.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestCRMProduct(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for CRMProduct.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestCRMProduct(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for CRMProduct.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
0
crm/fcrm/doctype/crm_products/__init__.py
Normal file
0
crm/fcrm/doctype/crm_products/__init__.py
Normal file
136
crm/fcrm/doctype/crm_products/crm_products.json
Normal file
136
crm/fcrm/doctype/crm_products/crm_products.json
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-04-28 12:50:49.812915",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"product_code",
|
||||||
|
"column_break_gvbc",
|
||||||
|
"product_name",
|
||||||
|
"section_break_fnvf",
|
||||||
|
"qty",
|
||||||
|
"column_break_ajac",
|
||||||
|
"rate",
|
||||||
|
"section_break_olqb",
|
||||||
|
"discount_percentage",
|
||||||
|
"column_break_uvra",
|
||||||
|
"discount_amount",
|
||||||
|
"section_break_cnpb",
|
||||||
|
"column_break_pozr",
|
||||||
|
"amount",
|
||||||
|
"column_break_ejqw",
|
||||||
|
"net_amount"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_gvbc",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "product_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Product Name",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_fnvf",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_olqb",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"fieldname": "discount_percentage",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"label": "Discount %"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "discount_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Discount Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_cnpb",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_pozr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"fieldname": "rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Rate",
|
||||||
|
"options": "currency",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"depends_on": "discount_percentage",
|
||||||
|
"description": "Amount after discount",
|
||||||
|
"fieldname": "net_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"columns": 5,
|
||||||
|
"fieldname": "product_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Product",
|
||||||
|
"options": "CRM Product"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Quantity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ajac",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_uvra",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ejqw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-05-14 18:52:26.183306",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "CRM Products",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
110
crm/fcrm/doctype/crm_products/crm_products.py
Normal file
110
crm/fcrm/doctype/crm_products/crm_products.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMProducts(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def create_product_details_script(doctype):
|
||||||
|
if not frappe.db.exists("CRM Form Script", "Product Details Script for " + doctype):
|
||||||
|
script = get_product_details_script(doctype)
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "CRM Form Script",
|
||||||
|
"name": "Product Details Script for " + doctype,
|
||||||
|
"dt": doctype,
|
||||||
|
"view": "Form",
|
||||||
|
"script": script,
|
||||||
|
"enabled": 1,
|
||||||
|
"is_standard": 1,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_details_script(doctype):
|
||||||
|
doctype_class = "class " + doctype.replace(" ", "")
|
||||||
|
|
||||||
|
return (
|
||||||
|
doctype_class
|
||||||
|
+ " {"
|
||||||
|
+ """
|
||||||
|
update_total() {
|
||||||
|
let total = 0
|
||||||
|
let total_qty = 0
|
||||||
|
let net_total = 0
|
||||||
|
let discount_applied = false
|
||||||
|
|
||||||
|
this.doc.products.forEach((d) => {
|
||||||
|
total += d.amount
|
||||||
|
net_total += d.net_amount
|
||||||
|
if (d.discount_percentage > 0) {
|
||||||
|
discount_applied = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.doc.total = total
|
||||||
|
this.doc.net_total = net_total || total
|
||||||
|
|
||||||
|
if (!net_total && discount_applied) {
|
||||||
|
this.doc.net_total = net_total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CRMProducts {
|
||||||
|
products_add() {
|
||||||
|
let row = this.doc.getRow('products')
|
||||||
|
row.trigger('qty')
|
||||||
|
this.doc.trigger('update_total')
|
||||||
|
}
|
||||||
|
|
||||||
|
products_remove() {
|
||||||
|
this.doc.trigger('update_total')
|
||||||
|
}
|
||||||
|
|
||||||
|
async product_code(idx) {
|
||||||
|
let row = this.doc.getRow('products', idx)
|
||||||
|
|
||||||
|
let a = await call("frappe.client.get_value", {
|
||||||
|
doctype: "CRM Product",
|
||||||
|
filters: { name: row.product_code },
|
||||||
|
fieldname: ["product_name", "standard_rate"],
|
||||||
|
})
|
||||||
|
|
||||||
|
row.product_name = a.product_name
|
||||||
|
if (a.standard_rate && !row.rate) {
|
||||||
|
row.rate = a.standard_rate
|
||||||
|
row.trigger("rate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qty(idx) {
|
||||||
|
let row = this.doc.getRow('products', idx)
|
||||||
|
row.amount = row.qty * row.rate
|
||||||
|
row.trigger('discount_percentage', idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
rate() {
|
||||||
|
let row = this.doc.getRow('products')
|
||||||
|
row.amount = row.qty * row.rate
|
||||||
|
row.trigger('discount_percentage')
|
||||||
|
}
|
||||||
|
|
||||||
|
discount_percentage(idx) {
|
||||||
|
let row = this.doc.getRow('products', idx)
|
||||||
|
if (!row.discount_percentage) {
|
||||||
|
row.net_amount = row.amount
|
||||||
|
row.discount_amount = 0
|
||||||
|
}
|
||||||
|
if (row.discount_percentage && row.amount) {
|
||||||
|
row.discount_amount = (row.discount_percentage / 100) * row.amount
|
||||||
|
row.net_amount = row.amount - row.discount_amount
|
||||||
|
}
|
||||||
|
this.doc.trigger('update_total')
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
)
|
||||||
@ -4,6 +4,8 @@ import click
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
from crm.fcrm.doctype.crm_products.crm_products import create_product_details_script
|
||||||
|
|
||||||
|
|
||||||
def before_install():
|
def before_install():
|
||||||
pass
|
pass
|
||||||
@ -19,6 +21,7 @@ def after_install(force=False):
|
|||||||
add_default_industries()
|
add_default_industries()
|
||||||
add_default_lead_sources()
|
add_default_lead_sources()
|
||||||
add_standard_dropdown_items()
|
add_standard_dropdown_items()
|
||||||
|
add_default_scripts()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@ -353,3 +356,8 @@ def add_standard_dropdown_items():
|
|||||||
crm_settings.append("dropdown_items", item)
|
crm_settings.append("dropdown_items", item)
|
||||||
|
|
||||||
crm_settings.save()
|
crm_settings.save()
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_scripts():
|
||||||
|
for doctype in ["CRM Lead", "CRM Deal"]:
|
||||||
|
create_product_details_script(doctype)
|
||||||
|
|||||||
@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Frappe CRM VERSION\n"
|
"Project-Id-Version: Frappe CRM VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: shariq@frappe.io\n"
|
"Report-Msgid-Bugs-To: shariq@frappe.io\n"
|
||||||
"POT-Creation-Date: 2025-04-20 09:35+0000\n"
|
"POT-Creation-Date: 2025-05-04 09:35+0000\n"
|
||||||
"PO-Revision-Date: 2025-04-20 09:35+0000\n"
|
"PO-Revision-Date: 2025-05-04 09:35+0000\n"
|
||||||
"Last-Translator: shariq@frappe.io\n"
|
"Last-Translator: shariq@frappe.io\n"
|
||||||
"Language-Team: shariq@frappe.io\n"
|
"Language-Team: shariq@frappe.io\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
@ -121,6 +121,10 @@ msgstr ""
|
|||||||
msgid "API Key"
|
msgid "API Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:179
|
||||||
|
msgid "API Key is required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the api_secret (Password) field in DocType 'CRM Twilio Settings'
|
#. Label of the api_secret (Password) field in DocType 'CRM Twilio Settings'
|
||||||
#. Label of the api_secret (Password) field in DocType 'ERPNext CRM Settings'
|
#. Label of the api_secret (Password) field in DocType 'ERPNext CRM Settings'
|
||||||
#: crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.json
|
#: crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.json
|
||||||
@ -151,6 +155,10 @@ msgstr ""
|
|||||||
msgid "Accepted At"
|
msgid "Accepted At"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:13
|
||||||
|
msgid "Account Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the account_sid (Data) field in DocType 'CRM Exotel Settings'
|
#. Label of the account_sid (Data) field in DocType 'CRM Exotel Settings'
|
||||||
#. Label of the account_sid (Data) field in DocType 'CRM Twilio Settings'
|
#. Label of the account_sid (Data) field in DocType 'CRM Twilio Settings'
|
||||||
#: crm/fcrm/doctype/crm_exotel_settings/crm_exotel_settings.json
|
#: crm/fcrm/doctype/crm_exotel_settings/crm_exotel_settings.json
|
||||||
@ -158,17 +166,25 @@ msgstr ""
|
|||||||
msgid "Account SID"
|
msgid "Account SID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:165
|
||||||
|
msgid "Account name is required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/CustomActions.vue:73
|
#: frontend/src/components/CustomActions.vue:73
|
||||||
#: frontend/src/components/ViewControls.vue:682
|
#: frontend/src/components/ViewControls.vue:682
|
||||||
#: frontend/src/components/ViewControls.vue:1111
|
#: frontend/src/components/ViewControls.vue:1111
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:542 frontend/src/pages/Lead.vue:528
|
#: frontend/src/pages/Deal.vue:546 frontend/src/pages/Lead.vue:532
|
||||||
#: frontend/src/pages/MobileDeal.vue:441 frontend/src/pages/MobileLead.vue:344
|
#: frontend/src/pages/MobileDeal.vue:441 frontend/src/pages/MobileLead.vue:344
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAccountList.vue:9
|
||||||
|
msgid "Add Account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/ColumnSettings.vue:65
|
#: frontend/src/components/ColumnSettings.vue:65
|
||||||
#: frontend/src/components/Kanban/KanbanView.vue:157
|
#: frontend/src/components/Kanban/KanbanView.vue:157
|
||||||
msgid "Add Column"
|
msgid "Add Column"
|
||||||
@ -266,9 +282,9 @@ msgstr ""
|
|||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:629 frontend/src/pages/MobileContact.vue:607
|
#: frontend/src/pages/Contact.vue:633 frontend/src/pages/MobileContact.vue:607
|
||||||
#: frontend/src/pages/MobileOrganization.vue:493
|
#: frontend/src/pages/MobileOrganization.vue:493
|
||||||
#: frontend/src/pages/Organization.vue:528
|
#: frontend/src/pages/Organization.vue:532
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -331,12 +347,12 @@ msgstr ""
|
|||||||
msgid "Are you sure you want to delete this attachment?"
|
msgid "Are you sure you want to delete this attachment?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:317 frontend/src/pages/MobileContact.vue:292
|
#: frontend/src/pages/Contact.vue:321 frontend/src/pages/MobileContact.vue:292
|
||||||
msgid "Are you sure you want to delete this contact?"
|
msgid "Are you sure you want to delete this contact?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/MobileOrganization.vue:286
|
#: frontend/src/pages/MobileOrganization.vue:286
|
||||||
#: frontend/src/pages/Organization.vue:324
|
#: frontend/src/pages/Organization.vue:328
|
||||||
msgid "Are you sure you want to delete this organization?"
|
msgid "Are you sure you want to delete this organization?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -397,7 +413,7 @@ msgstr ""
|
|||||||
msgid "Attach a file"
|
msgid "Attach a file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:577 frontend/src/pages/Lead.vue:563
|
#: frontend/src/pages/Deal.vue:581 frontend/src/pages/Lead.vue:567
|
||||||
#: frontend/src/pages/MobileDeal.vue:477 frontend/src/pages/MobileLead.vue:380
|
#: frontend/src/pages/MobileDeal.vue:477 frontend/src/pages/MobileLead.vue:380
|
||||||
msgid "Attachments"
|
msgid "Attachments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -414,6 +430,7 @@ msgid "BCC"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/FilesUploader/FilesUploader.vue:31
|
#: frontend/src/components/FilesUploader/FilesUploader.vue:31
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:67
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -650,7 +667,7 @@ msgstr ""
|
|||||||
msgid "Calling..."
|
msgid "Calling..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:562 frontend/src/pages/Lead.vue:548
|
#: frontend/src/pages/Deal.vue:566 frontend/src/pages/Lead.vue:552
|
||||||
#: frontend/src/pages/MobileDeal.vue:461 frontend/src/pages/MobileLead.vue:364
|
#: frontend/src/pages/MobileDeal.vue:461 frontend/src/pages/MobileLead.vue:364
|
||||||
msgid "Calls"
|
msgid "Calls"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -706,7 +723,7 @@ msgstr ""
|
|||||||
msgid "Change image"
|
msgid "Change image"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:241 frontend/src/pages/Lead.vue:267
|
#: frontend/src/pages/Lead.vue:245 frontend/src/pages/Lead.vue:271
|
||||||
#: frontend/src/pages/MobileLead.vue:110 frontend/src/pages/MobileLead.vue:137
|
#: frontend/src/pages/MobileLead.vue:110 frontend/src/pages/MobileLead.vue:137
|
||||||
msgid "Choose Existing"
|
msgid "Choose Existing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -719,6 +736,10 @@ msgstr ""
|
|||||||
msgid "Choose Existing Organization"
|
msgid "Choose Existing Organization"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAdd.vue:9
|
||||||
|
msgid "Choose the email service provider you want to configure."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Controls/Link.vue:60
|
#: frontend/src/components/Controls/Link.vue:60
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -790,7 +811,7 @@ msgstr ""
|
|||||||
msgid "Comment"
|
msgid "Comment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:552 frontend/src/pages/Lead.vue:538
|
#: frontend/src/pages/Deal.vue:556 frontend/src/pages/Lead.vue:542
|
||||||
#: frontend/src/pages/MobileDeal.vue:451 frontend/src/pages/MobileLead.vue:354
|
#: frontend/src/pages/MobileDeal.vue:451 frontend/src/pages/MobileLead.vue:354
|
||||||
msgid "Comments"
|
msgid "Comments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -840,7 +861,7 @@ msgstr ""
|
|||||||
#: crm/fcrm/doctype/crm_contacts/crm_contacts.json
|
#: crm/fcrm/doctype/crm_contacts/crm_contacts.json
|
||||||
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
||||||
#: frontend/src/components/Layouts/AppSidebar.vue:509
|
#: frontend/src/components/Layouts/AppSidebar.vue:509
|
||||||
#: frontend/src/pages/Lead.vue:263 frontend/src/pages/MobileLead.vue:133
|
#: frontend/src/pages/Lead.vue:267 frontend/src/pages/MobileLead.vue:133
|
||||||
msgid "Contact"
|
msgid "Contact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -852,11 +873,11 @@ msgstr ""
|
|||||||
msgid "Contact Us"
|
msgid "Contact Us"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:661 frontend/src/pages/MobileDeal.vue:560
|
#: frontend/src/pages/Deal.vue:665 frontend/src/pages/MobileDeal.vue:560
|
||||||
msgid "Contact added"
|
msgid "Contact added"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:647 frontend/src/pages/MobileDeal.vue:546
|
#: frontend/src/pages/Deal.vue:651 frontend/src/pages/MobileDeal.vue:546
|
||||||
msgid "Contact already added"
|
msgid "Contact already added"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -868,7 +889,7 @@ msgstr ""
|
|||||||
msgid "Contact not found"
|
msgid "Contact not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:676 frontend/src/pages/MobileDeal.vue:575
|
#: frontend/src/pages/Deal.vue:680 frontend/src/pages/MobileDeal.vue:575
|
||||||
msgid "Contact removed"
|
msgid "Contact removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -877,7 +898,7 @@ msgstr ""
|
|||||||
#. Label of a shortcut in the Frappe CRM Workspace
|
#. Label of a shortcut in the Frappe CRM Workspace
|
||||||
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
||||||
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
||||||
#: frontend/src/pages/Contact.vue:261 frontend/src/pages/MobileContact.vue:236
|
#: frontend/src/pages/Contact.vue:265 frontend/src/pages/MobileContact.vue:236
|
||||||
#: frontend/src/pages/MobileOrganization.vue:373
|
#: frontend/src/pages/MobileOrganization.vue:373
|
||||||
msgid "Contacts"
|
msgid "Contacts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -896,7 +917,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: frontend/src/components/Layouts/AppSidebar.vue:351
|
#: frontend/src/components/Layouts/AppSidebar.vue:351
|
||||||
#: frontend/src/components/ListBulkActions.vue:70
|
#: frontend/src/components/ListBulkActions.vue:70
|
||||||
#: frontend/src/pages/Lead.vue:201 frontend/src/pages/MobileLead.vue:49
|
#: frontend/src/pages/Lead.vue:205 frontend/src/pages/MobileLead.vue:49
|
||||||
#: frontend/src/pages/MobileLead.vue:96
|
#: frontend/src/pages/MobileLead.vue:96
|
||||||
msgid "Convert"
|
msgid "Convert"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -908,7 +929,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: frontend/src/components/ListBulkActions.vue:62
|
#: frontend/src/components/ListBulkActions.vue:62
|
||||||
#: frontend/src/components/ListBulkActions.vue:204
|
#: frontend/src/components/ListBulkActions.vue:204
|
||||||
#: frontend/src/pages/Lead.vue:38 frontend/src/pages/Lead.vue:212
|
#: frontend/src/pages/Lead.vue:38 frontend/src/pages/Lead.vue:216
|
||||||
#: frontend/src/pages/MobileLead.vue:92
|
#: frontend/src/pages/MobileLead.vue:92
|
||||||
msgid "Convert to Deal"
|
msgid "Convert to Deal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1032,7 +1053,7 @@ msgstr ""
|
|||||||
msgid "Custom statuses"
|
msgid "Custom statuses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:430
|
#: frontend/src/pages/Deal.vue:434
|
||||||
msgid "Customer created successfully"
|
msgid "Customer created successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1046,7 +1067,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: frontend/src/components/Activities/DataFields.vue:6
|
#: frontend/src/components/Activities/DataFields.vue:6
|
||||||
#: frontend/src/components/Layouts/AppSidebar.vue:539
|
#: frontend/src/components/Layouts/AppSidebar.vue:539
|
||||||
#: frontend/src/pages/Deal.vue:557 frontend/src/pages/Lead.vue:543
|
#: frontend/src/pages/Deal.vue:561 frontend/src/pages/Lead.vue:547
|
||||||
#: frontend/src/pages/MobileDeal.vue:456 frontend/src/pages/MobileLead.vue:359
|
#: frontend/src/pages/MobileDeal.vue:456 frontend/src/pages/MobileLead.vue:359
|
||||||
msgid "Data"
|
msgid "Data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1082,19 +1103,19 @@ msgstr ""
|
|||||||
msgid "Deal Statuses"
|
msgid "Deal Statuses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:650 frontend/src/pages/MobileContact.vue:628
|
#: frontend/src/pages/Contact.vue:654 frontend/src/pages/MobileContact.vue:628
|
||||||
#: frontend/src/pages/MobileOrganization.vue:514
|
#: frontend/src/pages/MobileOrganization.vue:514
|
||||||
#: frontend/src/pages/Organization.vue:549
|
#: frontend/src/pages/Organization.vue:553
|
||||||
msgid "Deal owner"
|
msgid "Deal owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:470 frontend/src/pages/MobileDeal.vue:363
|
#: frontend/src/pages/Deal.vue:474 frontend/src/pages/MobileDeal.vue:363
|
||||||
msgid "Deal updated"
|
msgid "Deal updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of a shortcut in the Frappe CRM Workspace
|
#. Label of a shortcut in the Frappe CRM Workspace
|
||||||
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
||||||
#: frontend/src/pages/Deal.vue:502 frontend/src/pages/MobileContact.vue:320
|
#: frontend/src/pages/Deal.vue:506 frontend/src/pages/MobileContact.vue:320
|
||||||
#: frontend/src/pages/MobileDeal.vue:395
|
#: frontend/src/pages/MobileDeal.vue:395
|
||||||
#: frontend/src/pages/MobileOrganization.vue:367
|
#: frontend/src/pages/MobileOrganization.vue:367
|
||||||
msgid "Deals"
|
msgid "Deals"
|
||||||
@ -1109,17 +1130,37 @@ msgstr ""
|
|||||||
msgid "Default"
|
msgid "Default"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAccountCard.vue:41
|
||||||
|
msgid "Default Inbox"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:44
|
||||||
|
msgid "Default Incoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the default_medium (Select) field in DocType 'CRM Telephony Agent'
|
#. Label of the default_medium (Select) field in DocType 'CRM Telephony Agent'
|
||||||
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
|
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
|
||||||
msgid "Default Medium"
|
msgid "Default Medium"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:52
|
||||||
|
msgid "Default Outgoing"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the default_priority (Check) field in DocType 'CRM Service Level
|
#. Label of the default_priority (Check) field in DocType 'CRM Service Level
|
||||||
#. Priority'
|
#. Priority'
|
||||||
#: crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json
|
#: crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json
|
||||||
msgid "Default Priority"
|
msgid "Default Priority"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAccountCard.vue:43
|
||||||
|
msgid "Default Sending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAccountCard.vue:39
|
||||||
|
msgid "Default Sending and Inbox"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py:33
|
#: crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py:33
|
||||||
msgid "Default Service Level Agreement already exists for {0}"
|
msgid "Default Service Level Agreement already exists for {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1157,14 +1198,14 @@ msgstr ""
|
|||||||
#: frontend/src/components/ListBulkActions.vue:186
|
#: frontend/src/components/ListBulkActions.vue:186
|
||||||
#: frontend/src/components/ViewControls.vue:1163
|
#: frontend/src/components/ViewControls.vue:1163
|
||||||
#: frontend/src/components/ViewControls.vue:1174
|
#: frontend/src/components/ViewControls.vue:1174
|
||||||
#: frontend/src/pages/Contact.vue:105 frontend/src/pages/Contact.vue:320
|
#: frontend/src/pages/Contact.vue:105 frontend/src/pages/Contact.vue:324
|
||||||
#: frontend/src/pages/MobileContact.vue:81
|
#: frontend/src/pages/MobileContact.vue:81
|
||||||
#: frontend/src/pages/MobileContact.vue:295
|
#: frontend/src/pages/MobileContact.vue:295
|
||||||
#: frontend/src/pages/MobileDeal.vue:526
|
#: frontend/src/pages/MobileDeal.vue:526
|
||||||
#: frontend/src/pages/MobileOrganization.vue:72
|
#: frontend/src/pages/MobileOrganization.vue:72
|
||||||
#: frontend/src/pages/MobileOrganization.vue:289
|
#: frontend/src/pages/MobileOrganization.vue:289
|
||||||
#: frontend/src/pages/Notes.vue:40 frontend/src/pages/Organization.vue:83
|
#: frontend/src/pages/Notes.vue:40 frontend/src/pages/Organization.vue:83
|
||||||
#: frontend/src/pages/Organization.vue:327 frontend/src/pages/Tasks.vue:368
|
#: frontend/src/pages/Organization.vue:331 frontend/src/pages/Tasks.vue:368
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1186,12 +1227,12 @@ msgstr ""
|
|||||||
msgid "Delete attachment"
|
msgid "Delete attachment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:316 frontend/src/pages/MobileContact.vue:291
|
#: frontend/src/pages/Contact.vue:320 frontend/src/pages/MobileContact.vue:291
|
||||||
msgid "Delete contact"
|
msgid "Delete contact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/MobileOrganization.vue:285
|
#: frontend/src/pages/MobileOrganization.vue:285
|
||||||
#: frontend/src/pages/Organization.vue:323
|
#: frontend/src/pages/Organization.vue:327
|
||||||
msgid "Delete organization"
|
msgid "Delete organization"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1357,6 +1398,10 @@ msgstr ""
|
|||||||
msgid "Edit Data Fields Layout"
|
msgid "Edit Data Fields Layout"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:6
|
||||||
|
msgid "Edit Email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Modals/SidePanelModal.vue:7
|
#: frontend/src/components/Modals/SidePanelModal.vue:7
|
||||||
msgid "Edit Field Layout"
|
msgid "Edit Field Layout"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1409,19 +1454,24 @@ msgstr ""
|
|||||||
#: crm/fcrm/doctype/crm_contacts/crm_contacts.json
|
#: crm/fcrm/doctype/crm_contacts/crm_contacts.json
|
||||||
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
||||||
#: crm/fcrm/doctype/crm_invitation/crm_invitation.json
|
#: crm/fcrm/doctype/crm_invitation/crm_invitation.json
|
||||||
#: crm/fcrm/doctype/crm_lead/crm_lead.json frontend/src/pages/Contact.vue:640
|
#: crm/fcrm/doctype/crm_lead/crm_lead.json frontend/src/pages/Contact.vue:644
|
||||||
#: frontend/src/pages/MobileContact.vue:618
|
#: frontend/src/pages/MobileContact.vue:618
|
||||||
#: frontend/src/pages/MobileOrganization.vue:504
|
#: frontend/src/pages/MobileOrganization.vue:504
|
||||||
#: frontend/src/pages/MobileOrganization.vue:532
|
#: frontend/src/pages/MobileOrganization.vue:532
|
||||||
#: frontend/src/pages/Organization.vue:539
|
#: frontend/src/pages/Organization.vue:543
|
||||||
#: frontend/src/pages/Organization.vue:567
|
#: frontend/src/pages/Organization.vue:571
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAccountList.vue:6
|
||||||
#: frontend/src/components/Settings/Settings.vue:107
|
#: frontend/src/components/Settings/Settings.vue:107
|
||||||
msgid "Email Accounts"
|
msgid "Email Accounts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:168
|
||||||
|
msgid "Email ID is required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the email_sent_at (Datetime) field in DocType 'CRM Invitation'
|
#. Label of the email_sent_at (Datetime) field in DocType 'CRM Invitation'
|
||||||
#: crm/fcrm/doctype/crm_invitation/crm_invitation.json
|
#: crm/fcrm/doctype/crm_invitation/crm_invitation.json
|
||||||
msgid "Email Sent At"
|
msgid "Email Sent At"
|
||||||
@ -1431,6 +1481,14 @@ msgstr ""
|
|||||||
msgid "Email Templates"
|
msgid "Email Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAdd.vue:143
|
||||||
|
msgid "Email account created successfully"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:214
|
||||||
|
msgid "Email account updated successfully"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Layouts/AppSidebar.vue:537
|
#: frontend/src/components/Layouts/AppSidebar.vue:537
|
||||||
msgid "Email communication"
|
msgid "Email communication"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1443,7 +1501,7 @@ msgstr ""
|
|||||||
msgid "Email template"
|
msgid "Email template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:547 frontend/src/pages/Lead.vue:533
|
#: frontend/src/pages/Deal.vue:551 frontend/src/pages/Lead.vue:537
|
||||||
#: frontend/src/pages/MobileDeal.vue:446 frontend/src/pages/MobileLead.vue:349
|
#: frontend/src/pages/MobileDeal.vue:446 frontend/src/pages/MobileLead.vue:349
|
||||||
msgid "Emails"
|
msgid "Emails"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1464,6 +1522,14 @@ msgstr ""
|
|||||||
msgid "Enable"
|
msgid "Enable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:28
|
||||||
|
msgid "Enable Incoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:36
|
||||||
|
msgid "Enable Outgoing"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the enabled (Check) field in DocType 'CRM Exotel Settings'
|
#. Label of the enabled (Check) field in DocType 'CRM Exotel Settings'
|
||||||
#. Label of the enabled (Check) field in DocType 'CRM Form Script'
|
#. Label of the enabled (Check) field in DocType 'CRM Form Script'
|
||||||
#. Label of the enabled (Check) field in DocType 'CRM Service Level Agreement'
|
#. Label of the enabled (Check) field in DocType 'CRM Service Level Agreement'
|
||||||
@ -1508,28 +1574,28 @@ msgstr ""
|
|||||||
#: frontend/src/components/Settings/SettingsPage.vue:91
|
#: frontend/src/components/Settings/SettingsPage.vue:91
|
||||||
#: frontend/src/components/Settings/TelephonySettings.vue:131
|
#: frontend/src/components/Settings/TelephonySettings.vue:131
|
||||||
#: frontend/src/components/Settings/TelephonySettings.vue:156
|
#: frontend/src/components/Settings/TelephonySettings.vue:156
|
||||||
#: frontend/src/pages/Lead.vue:629 frontend/src/pages/Lead.vue:639
|
#: frontend/src/pages/Lead.vue:633 frontend/src/pages/Lead.vue:643
|
||||||
#: frontend/src/pages/MobileLead.vue:438 frontend/src/pages/MobileLead.vue:448
|
#: frontend/src/pages/MobileLead.vue:438 frontend/src/pages/MobileLead.vue:448
|
||||||
msgid "Error"
|
msgid "Error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:491 frontend/src/pages/MobileDeal.vue:384
|
#: frontend/src/pages/Deal.vue:495 frontend/src/pages/MobileDeal.vue:384
|
||||||
msgid "Error Updating Deal"
|
msgid "Error Updating Deal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:477 frontend/src/pages/MobileLead.vue:287
|
#: frontend/src/pages/Lead.vue:481 frontend/src/pages/MobileLead.vue:287
|
||||||
msgid "Error Updating Lead"
|
msgid "Error Updating Lead"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:662
|
#: frontend/src/pages/Lead.vue:666
|
||||||
msgid "Error converting to deal"
|
msgid "Error converting to deal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:478 frontend/src/pages/MobileDeal.vue:371
|
#: frontend/src/pages/Deal.vue:482 frontend/src/pages/MobileDeal.vue:371
|
||||||
msgid "Error updating deal"
|
msgid "Error updating deal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:464 frontend/src/pages/MobileLead.vue:274
|
#: frontend/src/pages/Lead.vue:468 frontend/src/pages/MobileLead.vue:274
|
||||||
msgid "Error updating lead"
|
msgid "Error updating lead"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1632,10 +1698,18 @@ msgstr ""
|
|||||||
msgid "Failed to capture Twilio recording"
|
msgid "Failed to capture Twilio recording"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAdd.vue:150
|
||||||
|
msgid "Failed to create email account, Invalid credentials"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: crm/integrations/twilio/api.py:152
|
#: crm/integrations/twilio/api.py:152
|
||||||
msgid "Failed to update Twilio call status"
|
msgid "Failed to update Twilio call status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:222
|
||||||
|
msgid "Failed to update email account, Invalid credentials"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the favicon (Attach) field in DocType 'FCRM Settings'
|
#. Label of the favicon (Attach) field in DocType 'FCRM Settings'
|
||||||
#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
|
#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
|
||||||
#: frontend/src/components/Settings/GeneralSettings.vue:62
|
#: frontend/src/components/Settings/GeneralSettings.vue:62
|
||||||
@ -1888,6 +1962,22 @@ msgstr ""
|
|||||||
msgid "Icon"
|
msgid "Icon"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:55
|
||||||
|
msgid "If enabled, all outgoing emails will be sent from this account. Note: Only one account can be default outgoing."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:47
|
||||||
|
msgid "If enabled, all replies to your company (eg: replies@yourcomany.com) will come to this account. Note: Only one account can be default incoming."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:39
|
||||||
|
msgid "If enabled, outgoing emails can be sent from this account."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:31
|
||||||
|
msgid "If enabled, records can be created from the incoming emails on this account."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the image (Attach Image) field in DocType 'CRM Lead'
|
#. Label of the image (Attach Image) field in DocType 'CRM Lead'
|
||||||
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
||||||
msgid "Image"
|
msgid "Image"
|
||||||
@ -1916,6 +2006,10 @@ msgstr ""
|
|||||||
msgid "Inbound Call"
|
msgid "Inbound Call"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAccountCard.vue:45
|
||||||
|
msgid "Inbox"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Option for the 'Type' (Select) field in DocType 'CRM Call Log'
|
#. Option for the 'Type' (Select) field in DocType 'CRM Call Log'
|
||||||
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json
|
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json
|
||||||
msgid "Incoming"
|
msgid "Incoming"
|
||||||
@ -1984,6 +2078,10 @@ msgstr ""
|
|||||||
msgid "Invalid credentials"
|
msgid "Invalid credentials"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:172
|
||||||
|
msgid "Invalid email ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Settings/Settings.vue:101
|
#: frontend/src/components/Settings/Settings.vue:101
|
||||||
msgid "Invite Members"
|
msgid "Invite Members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2138,11 +2236,11 @@ msgstr ""
|
|||||||
msgid "Last Year"
|
msgid "Last Year"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:655 frontend/src/pages/MobileContact.vue:633
|
#: frontend/src/pages/Contact.vue:659 frontend/src/pages/MobileContact.vue:633
|
||||||
#: frontend/src/pages/MobileOrganization.vue:519
|
#: frontend/src/pages/MobileOrganization.vue:519
|
||||||
#: frontend/src/pages/MobileOrganization.vue:547
|
#: frontend/src/pages/MobileOrganization.vue:547
|
||||||
#: frontend/src/pages/Organization.vue:554
|
#: frontend/src/pages/Organization.vue:558
|
||||||
#: frontend/src/pages/Organization.vue:582
|
#: frontend/src/pages/Organization.vue:586
|
||||||
msgid "Last modified"
|
msgid "Last modified"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2188,13 +2286,13 @@ msgstr ""
|
|||||||
msgid "Lead Statuses"
|
msgid "Lead Statuses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:456 frontend/src/pages/MobileLead.vue:266
|
#: frontend/src/pages/Lead.vue:460 frontend/src/pages/MobileLead.vue:266
|
||||||
msgid "Lead updated"
|
msgid "Lead updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of a shortcut in the Frappe CRM Workspace
|
#. Label of a shortcut in the Frappe CRM Workspace
|
||||||
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
||||||
#: frontend/src/pages/Lead.vue:488 frontend/src/pages/MobileLead.vue:298
|
#: frontend/src/pages/Lead.vue:492 frontend/src/pages/MobileLead.vue:298
|
||||||
msgid "Leads"
|
msgid "Leads"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2395,9 +2493,9 @@ msgstr ""
|
|||||||
msgid "Mobile app installation"
|
msgid "Mobile app installation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:645 frontend/src/pages/MobileContact.vue:623
|
#: frontend/src/pages/Contact.vue:649 frontend/src/pages/MobileContact.vue:623
|
||||||
#: frontend/src/pages/MobileOrganization.vue:509
|
#: frontend/src/pages/MobileOrganization.vue:509
|
||||||
#: frontend/src/pages/Organization.vue:544
|
#: frontend/src/pages/Organization.vue:548
|
||||||
msgid "Mobile no"
|
msgid "Mobile no"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2435,7 +2533,7 @@ msgstr ""
|
|||||||
#: frontend/src/components/Modals/EmailTemplateModal.vue:24
|
#: frontend/src/components/Modals/EmailTemplateModal.vue:24
|
||||||
#: frontend/src/components/ViewControls.vue:781
|
#: frontend/src/components/ViewControls.vue:781
|
||||||
#: frontend/src/pages/MobileOrganization.vue:527
|
#: frontend/src/pages/MobileOrganization.vue:527
|
||||||
#: frontend/src/pages/Organization.vue:562
|
#: frontend/src/pages/Organization.vue:566
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2506,11 +2604,11 @@ msgstr ""
|
|||||||
msgid "New WhatsApp Message"
|
msgid "New WhatsApp Message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:279 frontend/src/pages/MobileLead.vue:150
|
#: frontend/src/pages/Lead.vue:283 frontend/src/pages/MobileLead.vue:150
|
||||||
msgid "New contact will be created based on the person's details"
|
msgid "New contact will be created based on the person's details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:254 frontend/src/pages/MobileLead.vue:124
|
#: frontend/src/pages/Lead.vue:258 frontend/src/pages/MobileLead.vue:124
|
||||||
msgid "New organization will be created based on the data in details section"
|
msgid "New organization will be created based on the data in details section"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2558,6 +2656,10 @@ msgstr ""
|
|||||||
msgid "No Title"
|
msgid "No Title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:152
|
||||||
|
msgid "No changes made"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Modals/SidePanelModal.vue:51
|
#: frontend/src/components/Modals/SidePanelModal.vue:51
|
||||||
#: frontend/src/pages/Deal.vue:262 frontend/src/pages/MobileDeal.vue:199
|
#: frontend/src/pages/Deal.vue:262 frontend/src/pages/MobileDeal.vue:199
|
||||||
msgid "No contacts added"
|
msgid "No contacts added"
|
||||||
@ -2571,7 +2673,7 @@ msgstr ""
|
|||||||
msgid "No label"
|
msgid "No label"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:722
|
#: frontend/src/pages/Deal.vue:726
|
||||||
msgid "No mobile number set"
|
msgid "No mobile number set"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2584,7 +2686,7 @@ msgstr ""
|
|||||||
msgid "No phone number set"
|
msgid "No phone number set"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:717
|
#: frontend/src/pages/Deal.vue:721
|
||||||
msgid "No primary contact set"
|
msgid "No primary contact set"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2675,8 +2777,8 @@ msgstr ""
|
|||||||
msgid "Not allowed to set primary contact for Deal"
|
msgid "Not allowed to set primary contact for Deal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:252 frontend/src/pages/Deal.vue:414
|
#: frontend/src/pages/Contact.vue:256 frontend/src/pages/Deal.vue:418
|
||||||
#: frontend/src/pages/Lead.vue:422 frontend/src/pages/Organization.vue:241
|
#: frontend/src/pages/Lead.vue:426 frontend/src/pages/Organization.vue:245
|
||||||
msgid "Not permitted"
|
msgid "Not permitted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2686,7 +2788,7 @@ msgstr ""
|
|||||||
msgid "Note"
|
msgid "Note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:572 frontend/src/pages/Lead.vue:558
|
#: frontend/src/pages/Deal.vue:576 frontend/src/pages/Lead.vue:562
|
||||||
#: frontend/src/pages/MobileDeal.vue:472 frontend/src/pages/MobileLead.vue:375
|
#: frontend/src/pages/MobileDeal.vue:472 frontend/src/pages/MobileLead.vue:375
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2734,10 +2836,10 @@ msgstr ""
|
|||||||
msgid "Old Parent"
|
msgid "Old Parent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Contact.vue:300 frontend/src/pages/Lead.vue:592
|
#: frontend/src/pages/Contact.vue:304 frontend/src/pages/Lead.vue:596
|
||||||
#: frontend/src/pages/MobileContact.vue:275
|
#: frontend/src/pages/MobileContact.vue:275
|
||||||
#: frontend/src/pages/MobileOrganization.vue:269
|
#: frontend/src/pages/MobileOrganization.vue:269
|
||||||
#: frontend/src/pages/Organization.vue:307
|
#: frontend/src/pages/Organization.vue:311
|
||||||
msgid "Only PNG and JPG images are allowed"
|
msgid "Only PNG and JPG images are allowed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2792,13 +2894,13 @@ msgstr ""
|
|||||||
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
#: crm/fcrm/doctype/crm_deal/crm_deal.json
|
||||||
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
||||||
#: frontend/src/components/Layouts/AppSidebar.vue:510
|
#: frontend/src/components/Layouts/AppSidebar.vue:510
|
||||||
#: frontend/src/pages/Contact.vue:624 frontend/src/pages/Lead.vue:237
|
#: frontend/src/pages/Contact.vue:628 frontend/src/pages/Lead.vue:241
|
||||||
#: frontend/src/pages/MobileContact.vue:602
|
#: frontend/src/pages/MobileContact.vue:602
|
||||||
#: frontend/src/pages/MobileLead.vue:106
|
#: frontend/src/pages/MobileLead.vue:106
|
||||||
#: frontend/src/pages/MobileOrganization.vue:488
|
#: frontend/src/pages/MobileOrganization.vue:488
|
||||||
#: frontend/src/pages/MobileOrganization.vue:542
|
#: frontend/src/pages/MobileOrganization.vue:542
|
||||||
#: frontend/src/pages/Organization.vue:523
|
#: frontend/src/pages/Organization.vue:527
|
||||||
#: frontend/src/pages/Organization.vue:577
|
#: frontend/src/pages/Organization.vue:581
|
||||||
msgid "Organization"
|
msgid "Organization"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2826,14 +2928,14 @@ msgid "Organization logo"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/MobileOrganization.vue:216
|
#: frontend/src/pages/MobileOrganization.vue:216
|
||||||
#: frontend/src/pages/Organization.vue:254
|
#: frontend/src/pages/Organization.vue:258
|
||||||
msgid "Organization updated"
|
msgid "Organization updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of a shortcut in the Frappe CRM Workspace
|
#. Label of a shortcut in the Frappe CRM Workspace
|
||||||
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
|
||||||
#: frontend/src/pages/MobileOrganization.vue:223
|
#: frontend/src/pages/MobileOrganization.vue:223
|
||||||
#: frontend/src/pages/Organization.vue:261
|
#: frontend/src/pages/Organization.vue:265
|
||||||
msgid "Organizations"
|
msgid "Organizations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2865,10 +2967,18 @@ msgstr ""
|
|||||||
msgid "Parent CRM Territory"
|
msgid "Parent CRM Territory"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:64
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: crm/api/demo.py:21 crm/api/demo.py:29
|
#: crm/api/demo.py:21 crm/api/demo.py:29
|
||||||
msgid "Password cannot be reset by Demo User {}"
|
msgid "Password cannot be reset by Demo User {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:175
|
||||||
|
msgid "Password is required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Modals/EmailTemplateModal.vue:23
|
#: frontend/src/components/Modals/EmailTemplateModal.vue:23
|
||||||
#: frontend/src/components/Modals/EmailTemplateSelectorModal.vue:11
|
#: frontend/src/components/Modals/EmailTemplateSelectorModal.vue:11
|
||||||
msgid "Payment Reminder"
|
msgid "Payment Reminder"
|
||||||
@ -2903,7 +3013,7 @@ msgstr ""
|
|||||||
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
||||||
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
|
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
|
||||||
#: frontend/src/pages/MobileOrganization.vue:537
|
#: frontend/src/pages/MobileOrganization.vue:537
|
||||||
#: frontend/src/pages/Organization.vue:572
|
#: frontend/src/pages/Organization.vue:576
|
||||||
msgid "Phone"
|
msgid "Phone"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2933,6 +3043,10 @@ msgstr ""
|
|||||||
msgid "Playback speed"
|
msgid "Playback speed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAccountList.vue:34
|
||||||
|
msgid "Please add an email account to continue."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: crm/integrations/twilio/twilio_handler.py:119
|
#: crm/integrations/twilio/twilio_handler.py:119
|
||||||
msgid "Please enable twilio settings before making a call."
|
msgid "Please enable twilio settings before making a call."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2941,11 +3055,11 @@ msgstr ""
|
|||||||
msgid "Please enter a valid URL"
|
msgid "Please enter a valid URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:630 frontend/src/pages/MobileLead.vue:439
|
#: frontend/src/pages/Lead.vue:634 frontend/src/pages/MobileLead.vue:439
|
||||||
msgid "Please select an existing contact"
|
msgid "Please select an existing contact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Lead.vue:640 frontend/src/pages/MobileLead.vue:449
|
#: frontend/src/pages/Lead.vue:644 frontend/src/pages/MobileLead.vue:449
|
||||||
msgid "Please select an existing organization"
|
msgid "Please select an existing organization"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2964,7 +3078,7 @@ msgstr ""
|
|||||||
msgid "Primary"
|
msgid "Primary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:691 frontend/src/pages/MobileDeal.vue:590
|
#: frontend/src/pages/Deal.vue:695 frontend/src/pages/MobileDeal.vue:590
|
||||||
msgid "Primary contact set"
|
msgid "Primary contact set"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3099,7 +3213,7 @@ msgstr ""
|
|||||||
msgid "Reject"
|
msgid "Reject"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:627
|
#: frontend/src/pages/Deal.vue:631
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3433,7 +3547,7 @@ msgstr ""
|
|||||||
msgid "Set an organization"
|
msgid "Set an organization"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:635 frontend/src/pages/MobileDeal.vue:534
|
#: frontend/src/pages/Deal.vue:639 frontend/src/pages/MobileDeal.vue:534
|
||||||
msgid "Set as Primary Contact"
|
msgid "Set as Primary Contact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3449,12 +3563,56 @@ msgstr ""
|
|||||||
msgid "Setting up"
|
msgid "Setting up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:145
|
||||||
|
msgid "Setting up Frappe Mail requires you to have an API key and API Secret of your email account. Read more "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:97
|
||||||
|
msgid ""
|
||||||
|
"Setting up GMail requires you to enable two factor authentication\n"
|
||||||
|
"\t\t and app specific passwords. Read more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:105
|
||||||
|
msgid ""
|
||||||
|
"Setting up Outlook requires you to enable two factor authentication\n"
|
||||||
|
"\t\t and app specific passwords. Read more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:113
|
||||||
|
msgid ""
|
||||||
|
"Setting up Sendgrid requires you to enable two factor authentication\n"
|
||||||
|
"\t\t and app specific passwords. Read more "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:121
|
||||||
|
msgid ""
|
||||||
|
"Setting up SparkPost requires you to enable two factor authentication\n"
|
||||||
|
"\t\t and app specific passwords. Read more "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:129
|
||||||
|
msgid ""
|
||||||
|
"Setting up Yahoo requires you to enable two factor authentication\n"
|
||||||
|
"\t\t and app specific passwords. Read more "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:137
|
||||||
|
msgid ""
|
||||||
|
"Setting up Yandex requires you to enable two factor authentication\n"
|
||||||
|
"\t\t and app specific passwords. Read more "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Layouts/AppSidebar.vue:494
|
#: frontend/src/components/Layouts/AppSidebar.vue:494
|
||||||
#: frontend/src/components/Settings/Settings.vue:11
|
#: frontend/src/components/Settings/Settings.vue:11
|
||||||
#: frontend/src/components/Settings/Settings.vue:81
|
#: frontend/src/components/Settings/Settings.vue:81
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailAdd.vue:6
|
||||||
|
msgid "Setup Email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Activities/Activities.vue:230
|
#: frontend/src/components/Activities/Activities.vue:230
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3550,10 +3708,10 @@ msgstr ""
|
|||||||
#: crm/fcrm/doctype/crm_invitation/crm_invitation.json
|
#: crm/fcrm/doctype/crm_invitation/crm_invitation.json
|
||||||
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
#: crm/fcrm/doctype/crm_lead/crm_lead.json
|
||||||
#: crm/fcrm/doctype/crm_lead_status/crm_lead_status.json
|
#: crm/fcrm/doctype/crm_lead_status/crm_lead_status.json
|
||||||
#: crm/fcrm/doctype/crm_task/crm_task.json frontend/src/pages/Contact.vue:635
|
#: crm/fcrm/doctype/crm_task/crm_task.json frontend/src/pages/Contact.vue:639
|
||||||
#: frontend/src/pages/MobileContact.vue:613
|
#: frontend/src/pages/MobileContact.vue:613
|
||||||
#: frontend/src/pages/MobileOrganization.vue:499
|
#: frontend/src/pages/MobileOrganization.vue:499
|
||||||
#: frontend/src/pages/Organization.vue:534
|
#: frontend/src/pages/Organization.vue:538
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3596,6 +3754,10 @@ msgstr ""
|
|||||||
msgid "Sunday"
|
msgid "Sunday"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/emailConfig.js:16
|
||||||
|
msgid "Support / Sales"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/FilesUploader/FilesUploader.vue:49
|
#: frontend/src/components/FilesUploader/FilesUploader.vue:49
|
||||||
msgid "Switch camera"
|
msgid "Switch camera"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3631,7 +3793,7 @@ msgstr ""
|
|||||||
msgid "Task"
|
msgid "Task"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:567 frontend/src/pages/Lead.vue:553
|
#: frontend/src/pages/Deal.vue:571 frontend/src/pages/Lead.vue:557
|
||||||
#: frontend/src/pages/MobileDeal.vue:467 frontend/src/pages/MobileLead.vue:370
|
#: frontend/src/pages/MobileDeal.vue:467 frontend/src/pages/MobileLead.vue:370
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3751,6 +3913,10 @@ msgstr ""
|
|||||||
msgid "To User"
|
msgid "To User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:119
|
||||||
|
msgid "To know more about setting up email accounts, click"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Filter.vue:619
|
#: frontend/src/components/Filter.vue:619
|
||||||
msgid "Today"
|
msgid "Today"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3882,6 +4048,10 @@ msgstr ""
|
|||||||
msgid "Update"
|
msgid "Update"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:74
|
||||||
|
msgid "Update Account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Modals/EditValueModal.vue:30
|
#: frontend/src/components/Modals/EditValueModal.vue:30
|
||||||
msgid "Update {0} Records"
|
msgid "Update {0} Records"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3965,7 +4135,7 @@ msgid "Website"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/MobileOrganization.vue:308
|
#: frontend/src/pages/MobileOrganization.vue:308
|
||||||
#: frontend/src/pages/Organization.vue:350
|
#: frontend/src/pages/Organization.vue:354
|
||||||
msgid "Website not found"
|
msgid "Website not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3995,7 +4165,7 @@ msgstr ""
|
|||||||
#: crm/fcrm/doctype/crm_notification/crm_notification.json
|
#: crm/fcrm/doctype/crm_notification/crm_notification.json
|
||||||
#: frontend/src/components/Layouts/AppSidebar.vue:562
|
#: frontend/src/components/Layouts/AppSidebar.vue:562
|
||||||
#: frontend/src/components/Settings/Settings.vue:124
|
#: frontend/src/components/Settings/Settings.vue:124
|
||||||
#: frontend/src/pages/Deal.vue:582 frontend/src/pages/Lead.vue:568
|
#: frontend/src/pages/Deal.vue:586 frontend/src/pages/Lead.vue:572
|
||||||
#: frontend/src/pages/MobileDeal.vue:482 frontend/src/pages/MobileLead.vue:385
|
#: frontend/src/pages/MobileDeal.vue:482 frontend/src/pages/MobileLead.vue:385
|
||||||
msgid "WhatsApp"
|
msgid "WhatsApp"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4146,6 +4316,10 @@ msgstr ""
|
|||||||
msgid "has reached out"
|
msgid "has reached out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: frontend/src/components/Settings/EmailEdit.vue:25
|
||||||
|
msgid "here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/components/Settings/InviteMemberPage.vue:17
|
#: frontend/src/components/Settings/InviteMemberPage.vue:17
|
||||||
msgid "john@doe.com"
|
msgid "john@doe.com"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4247,7 +4421,7 @@ msgstr ""
|
|||||||
msgid "{0} assigned a {1} {2} to you"
|
msgid "{0} assigned a {1} {2} to you"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: frontend/src/pages/Deal.vue:492 frontend/src/pages/Lead.vue:478
|
#: frontend/src/pages/Deal.vue:496 frontend/src/pages/Lead.vue:482
|
||||||
#: frontend/src/pages/MobileDeal.vue:385 frontend/src/pages/MobileLead.vue:288
|
#: frontend/src/pages/MobileDeal.vue:385 frontend/src/pages/MobileLead.vue:288
|
||||||
msgid "{0} is a required field"
|
msgid "{0} is a required field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -12,3 +12,4 @@ crm.patches.v1_0.create_default_sidebar_fields_layout
|
|||||||
crm.patches.v1_0.update_deal_quick_entry_layout
|
crm.patches.v1_0.update_deal_quick_entry_layout
|
||||||
crm.patches.v1_0.update_layouts_to_new_format
|
crm.patches.v1_0.update_layouts_to_new_format
|
||||||
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
||||||
|
crm.patches.v1_0.create_default_scripts
|
||||||
5
crm/patches/v1_0/create_default_scripts.py
Normal file
5
crm/patches/v1_0/create_default_scripts.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from crm.install import add_default_scripts
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
add_default_scripts()
|
||||||
@ -1,12 +1,13 @@
|
|||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# MIT License. See license.txt
|
# MIT License. See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
import click
|
import click
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
def before_uninstall():
|
def before_uninstall():
|
||||||
delete_email_template_custom_fields()
|
delete_email_template_custom_fields()
|
||||||
|
|
||||||
|
|
||||||
def delete_email_template_custom_fields():
|
def delete_email_template_custom_fields():
|
||||||
if frappe.get_meta("Email Template").has_field("enabled"):
|
if frappe.get_meta("Email Template").has_field("enabled"):
|
||||||
click.secho("* Uninstalling Custom Fields from Email Template")
|
click.secho("* Uninstalling Custom Fields from Email Template")
|
||||||
|
|||||||
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@ -53,6 +53,7 @@ declare module 'vue' {
|
|||||||
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
|
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
|
||||||
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
|
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
|
||||||
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
||||||
|
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||||
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
||||||
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
||||||
@ -113,9 +114,11 @@ declare module 'vue' {
|
|||||||
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
||||||
Filter: typeof import('./src/components/Filter.vue')['default']
|
Filter: typeof import('./src/components/Filter.vue')['default']
|
||||||
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
||||||
|
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
|
||||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
||||||
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
||||||
GeneralSettings: typeof import('./src/components/Settings/GeneralSettings.vue')['default']
|
GeneralSettings: typeof import('./src/components/Settings/GeneralSettings.vue')['default']
|
||||||
|
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
|
||||||
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
||||||
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
||||||
GridFieldsEditorModal: typeof import('./src/components/Controls/GridFieldsEditorModal.vue')['default']
|
GridFieldsEditorModal: typeof import('./src/components/Controls/GridFieldsEditorModal.vue')['default']
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
||||||
{{ __('Data') }}
|
{{ __('Data') }}
|
||||||
<Badge
|
<Badge
|
||||||
v-if="data.isDirty"
|
v-if="document.isDirty"
|
||||||
class="ml-3"
|
class="ml-3"
|
||||||
:label="'Not Saved'"
|
:label="'Not Saved'"
|
||||||
theme="orange"
|
theme="orange"
|
||||||
@ -20,15 +20,15 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
:disabled="!data.isDirty"
|
:disabled="!document.isDirty"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:loading="data.save.loading"
|
:loading="document.save.loading"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="data.get.loading"
|
v-if="document.get.loading"
|
||||||
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-gray-500"
|
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-gray-500"
|
||||||
>
|
>
|
||||||
<LoadingIndicator class="h-6 w-6" />
|
<LoadingIndicator class="h-6 w-6" />
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<FieldLayout
|
<FieldLayout
|
||||||
v-if="tabs.data"
|
v-if="tabs.data"
|
||||||
:tabs="tabs.data"
|
:tabs="tabs.data"
|
||||||
:data="data.doc"
|
:data="document.doc"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -49,7 +49,7 @@
|
|||||||
@reload="
|
@reload="
|
||||||
() => {
|
() => {
|
||||||
tabs.reload()
|
tabs.reload()
|
||||||
data.reload()
|
document.reload()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@ -59,10 +59,10 @@
|
|||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import DataFieldsModal from '@/components/Modals/DataFieldsModal.vue'
|
import DataFieldsModal from '@/components/Modals/DataFieldsModal.vue'
|
||||||
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||||
import { Badge, createResource, createDocumentResource } from 'frappe-ui'
|
import { Badge, createResource } from 'frappe-ui'
|
||||||
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||||
import { createToast } from '@/utils'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
import { isMobileView } from '@/composables/settings'
|
import { isMobileView } from '@/composables/settings'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
@ -76,33 +76,11 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isManager } = usersStore()
|
const { isManager } = usersStore()
|
||||||
|
|
||||||
const showDataFieldsModal = ref(false)
|
const showDataFieldsModal = ref(false)
|
||||||
|
|
||||||
const data = createDocumentResource({
|
const { document } = useDocument(props.doctype, props.docname)
|
||||||
doctype: props.doctype,
|
|
||||||
name: props.docname,
|
|
||||||
setValue: {
|
|
||||||
onSuccess: () => {
|
|
||||||
data.reload()
|
|
||||||
createToast({
|
|
||||||
title: 'Data Updated',
|
|
||||||
icon: 'check',
|
|
||||||
iconClasses: 'text-ink-green-3',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
createToast({
|
|
||||||
title: 'Error',
|
|
||||||
text: err.messages[0],
|
|
||||||
icon: 'x',
|
|
||||||
iconClasses: 'text-red-600',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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',
|
||||||
@ -112,19 +90,19 @@ const tabs = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function saveChanges() {
|
function saveChanges() {
|
||||||
data.save.submit()
|
document.save.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => data.doc,
|
() => document.doc,
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (!oldValue) return
|
if (!oldValue) return
|
||||||
if (newValue && oldValue) {
|
if (newValue && oldValue) {
|
||||||
const isDirty =
|
const isDirty =
|
||||||
JSON.stringify(newValue) !== JSON.stringify(data.originalDoc)
|
JSON.stringify(newValue) !== JSON.stringify(document.originalDoc)
|
||||||
data.isDirty = isDirty
|
document.isDirty = isDirty
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
data.save.loading = false
|
document.save.loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
58
frontend/src/components/Controls/FormattedInput.vue
Normal file
58
frontend/src/components/Controls/FormattedInput.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<TextInput
|
||||||
|
ref="inputRef"
|
||||||
|
:value="displayValue"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="isFocused = false"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
<slot name="description">
|
||||||
|
<p v-if="attrs.description" class="mt-1.5" :class="descriptionClasses">
|
||||||
|
{{ attrs.description }}
|
||||||
|
</p>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { TextInput } from 'frappe-ui'
|
||||||
|
import { ref, computed, nextTick, useAttrs } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
formattedValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const isFocused = ref(false)
|
||||||
|
const inputRef = ref(null)
|
||||||
|
|
||||||
|
function handleFocus() {
|
||||||
|
isFocused.value = true
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (inputRef.value) {
|
||||||
|
inputRef.value.el?.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayValue = computed(() => {
|
||||||
|
return isFocused.value ? props.value : props.formattedValue || props.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const descriptionClasses = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
sm: 'text-xs',
|
||||||
|
md: 'text-base',
|
||||||
|
}[attrs.size || 'sm'],
|
||||||
|
'text-ink-gray-5',
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -33,10 +33,23 @@
|
|||||||
<div
|
<div
|
||||||
v-for="field in fields"
|
v-for="field in fields"
|
||||||
class="border-r border-outline-gray-2 p-2 truncate"
|
class="border-r border-outline-gray-2 p-2 truncate"
|
||||||
|
:class="
|
||||||
|
['Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)
|
||||||
|
? 'text-right'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
:key="field.fieldname"
|
:key="field.fieldname"
|
||||||
:title="field.label"
|
:title="field.label"
|
||||||
>
|
>
|
||||||
{{ __(field.label) }}
|
{{ __(field.label) }}
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
field.reqd ||
|
||||||
|
(field.mandatory_depends_on && field.mandatory_via_depends_on)
|
||||||
|
"
|
||||||
|
class="text-ink-red-2"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-12">
|
<div class="w-12">
|
||||||
@ -93,7 +106,16 @@
|
|||||||
:key="field.fieldname"
|
:key="field.fieldname"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="field.read_only && field.fieldtype !== 'Check'"
|
v-if="
|
||||||
|
field.read_only &&
|
||||||
|
![
|
||||||
|
'Int',
|
||||||
|
'Float',
|
||||||
|
'Currency',
|
||||||
|
'Percent',
|
||||||
|
'Check',
|
||||||
|
].includes(field.fieldtype)
|
||||||
|
"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
@ -104,13 +126,17 @@
|
|||||||
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
class="text-sm text-ink-gray-8"
|
class="text-sm text-ink-gray-8"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
:doctype="
|
:doctype="
|
||||||
field.fieldtype == 'Link'
|
field.fieldtype == 'Link'
|
||||||
? field.options
|
? field.options
|
||||||
: row[field.options]
|
: row[field.options]
|
||||||
"
|
"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
|
:onCreate="
|
||||||
|
(value, close) => field.create(v, field, row, close)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
v-else-if="field.fieldtype === 'User'"
|
v-else-if="field.fieldtype === 'User'"
|
||||||
@ -118,7 +144,7 @@
|
|||||||
:value="getUser(row[field.fieldname]).full_name"
|
:value="getUser(row[field.fieldname]).full_name"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="(v) => (row[field.fieldname] = v)"
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:hideMe="true"
|
:hideMe="true"
|
||||||
>
|
>
|
||||||
@ -148,23 +174,26 @@
|
|||||||
class="cursor-pointer duration-300"
|
class="cursor-pointer duration-300"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
:disabled="!gridSettings.editable_grid"
|
:disabled="!gridSettings.editable_grid"
|
||||||
|
@change="(e) => fieldChange(e.target.checked, field, row)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-else-if="field.fieldtype === 'Date'"
|
v-else-if="field.fieldtype === 'Date'"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:formatter="(date) => getFormat(date, '', true)"
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
input-class="border-none text-sm text-ink-gray-8"
|
input-class="border-none text-sm text-ink-gray-8"
|
||||||
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
/>
|
/>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
v-else-if="field.fieldtype === 'Datetime'"
|
v-else-if="field.fieldtype === 'Datetime'"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:formatter="(date) => getFormat(date, '', true, true)"
|
:formatter="(date) => getFormat(date, '', true, true)"
|
||||||
input-class="border-none text-sm text-ink-gray-8"
|
input-class="border-none text-sm text-ink-gray-8"
|
||||||
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@ -175,13 +204,8 @@
|
|||||||
rows="1"
|
rows="1"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
/>
|
@change="fieldChange($event.target.value, field, row)"
|
||||||
<FormControl
|
|
||||||
v-else-if="['Int'].includes(field.fieldtype)"
|
|
||||||
type="number"
|
|
||||||
variant="outline"
|
|
||||||
v-model="row[field.fieldname]"
|
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Select'"
|
v-else-if="field.fieldtype === 'Select'"
|
||||||
@ -190,6 +214,48 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
|
@change="(e) => fieldChange(e.target.value, field, row)"
|
||||||
|
/>
|
||||||
|
<FormattedInput
|
||||||
|
v-else-if="field.fieldtype === 'Int'"
|
||||||
|
class="[&_input]:text-right"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
:value="row[field.fieldname] || '0'"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange($event.target.value, field, row)"
|
||||||
|
/>
|
||||||
|
<FormattedInput
|
||||||
|
v-else-if="field.fieldtype === 'Percent'"
|
||||||
|
class="[&_input]:text-right"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
:value="getFloatWithPrecision(field.fieldname, row)"
|
||||||
|
:formattedValue="(row[field.fieldname] || '0') + '%'"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
|
/>
|
||||||
|
<FormattedInput
|
||||||
|
v-else-if="field.fieldtype === 'Float'"
|
||||||
|
class="[&_input]:text-right"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
:value="getFloatWithPrecision(field.fieldname, row)"
|
||||||
|
:formattedValue="row[field.fieldname]"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
|
/>
|
||||||
|
<FormattedInput
|
||||||
|
v-else-if="field.fieldtype === 'Currency'"
|
||||||
|
class="[&_input]:text-right"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
:value="getCurrencyWithPrecision(field.fieldname, row)"
|
||||||
|
:formattedValue="
|
||||||
|
getFormattedCurrency(field.fieldname, row, parentDoc)
|
||||||
|
"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
@ -198,6 +264,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
|
@change="fieldChange($event.target.value, field, row)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -258,6 +325,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import FormattedInput from '@/components/Controls/FormattedInput.vue'
|
||||||
import GridFieldsEditorModal from '@/components/Controls/GridFieldsEditorModal.vue'
|
import GridFieldsEditorModal from '@/components/Controls/GridFieldsEditorModal.vue'
|
||||||
import GridRowFieldsModal from '@/components/Controls/GridRowFieldsModal.vue'
|
import GridRowFieldsModal from '@/components/Controls/GridRowFieldsModal.vue'
|
||||||
import GridRowModal from '@/components/Controls/GridRowModal.vue'
|
import GridRowModal from '@/components/Controls/GridRowModal.vue'
|
||||||
@ -265,8 +333,10 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
|
|||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { getRandom, getFormat, isTouchScreenDevice } from '@/utils'
|
import { getRandom, getFormat, isTouchScreenDevice } from '@/utils'
|
||||||
|
import { flt } from '@/utils/numberFormat.js'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { getMeta } from '@/stores/meta'
|
import { getMeta } from '@/stores/meta'
|
||||||
|
import { createDocument } from '@/composables/document'
|
||||||
import {
|
import {
|
||||||
FeatherIcon,
|
FeatherIcon,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -274,9 +344,10 @@ import {
|
|||||||
DateTimePicker,
|
DateTimePicker,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
dayjs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, inject, provide } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
@ -291,15 +362,32 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
parentFieldname: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getGridViewSettings, getFields, getGridSettings } = getMeta(
|
const triggerOnChange = inject('triggerOnChange')
|
||||||
props.doctype,
|
const triggerOnRowAdd = inject('triggerOnRowAdd')
|
||||||
)
|
const triggerOnRowRemove = inject('triggerOnRowRemove')
|
||||||
|
|
||||||
|
const {
|
||||||
|
getGridViewSettings,
|
||||||
|
getFields,
|
||||||
|
getFloatWithPrecision,
|
||||||
|
getCurrencyWithPrecision,
|
||||||
|
getFormattedCurrency,
|
||||||
|
getGridSettings,
|
||||||
|
} = getMeta(props.doctype)
|
||||||
getMeta(props.parentDoctype)
|
getMeta(props.parentDoctype)
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
const rows = defineModel()
|
const rows = defineModel()
|
||||||
|
const parentDoc = defineModel('parent')
|
||||||
|
|
||||||
|
provide('parentDoc', parentDoc)
|
||||||
|
|
||||||
const showRowList = ref(new Array(rows.value?.length || []).fill(false))
|
const showRowList = ref(new Array(rows.value?.length || []).fill(false))
|
||||||
const selectedRows = reactive(new Set())
|
const selectedRows = reactive(new Set())
|
||||||
|
|
||||||
@ -322,7 +410,22 @@ const fields = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const allFields = computed(() => {
|
||||||
|
return getFields()?.map((f) => getFieldObj(f)) || []
|
||||||
|
})
|
||||||
|
|
||||||
function getFieldObj(field) {
|
function getFieldObj(field) {
|
||||||
|
if (field.fieldtype === 'Link' && field.options !== 'User') {
|
||||||
|
if (!field.create) {
|
||||||
|
field.create = (value, field, row, close) => {
|
||||||
|
const callback = (d) => {
|
||||||
|
if (d) fieldChange(d.name, field, row)
|
||||||
|
}
|
||||||
|
createDocument(field.options, value, close, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
filters: field.link_filters && JSON.parse(field.link_filters),
|
filters: field.link_filters && JSON.parse(field.link_filters),
|
||||||
@ -367,21 +470,71 @@ const toggleSelectRow = (row) => {
|
|||||||
|
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
const newRow = {}
|
const newRow = {}
|
||||||
fields.value?.forEach((field) => {
|
allFields.value?.forEach((field) => {
|
||||||
if (field.fieldtype === 'Check') newRow[field.fieldname] = false
|
if (field.fieldtype === 'Check') {
|
||||||
else newRow[field.fieldname] = ''
|
newRow[field.fieldname] = false
|
||||||
|
} else {
|
||||||
|
newRow[field.fieldname] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.default) {
|
||||||
|
newRow[field.fieldname] = getDefaultValue(field.default, field.fieldtype)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
newRow.name = getRandom(10)
|
newRow.name = getRandom(10)
|
||||||
showRowList.value.push(false)
|
showRowList.value.push(false)
|
||||||
newRow['__islocal'] = true
|
newRow['__islocal'] = true
|
||||||
|
newRow['idx'] = rows.value.length + 1
|
||||||
|
newRow['doctype'] = props.doctype
|
||||||
|
newRow['parentfield'] = props.parentFieldname
|
||||||
|
newRow['parenttype'] = props.parentDoctype
|
||||||
rows.value.push(newRow)
|
rows.value.push(newRow)
|
||||||
|
triggerOnRowAdd(newRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRows = () => {
|
const deleteRows = () => {
|
||||||
rows.value = rows.value.filter((row) => !selectedRows.has(row.name))
|
rows.value = rows.value.filter((row) => !selectedRows.has(row.name))
|
||||||
|
triggerOnRowRemove(selectedRows, rows.value)
|
||||||
|
|
||||||
showRowList.value.pop()
|
showRowList.value.pop()
|
||||||
selectedRows.clear()
|
selectedRows.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldChange(value, field, row) {
|
||||||
|
row[field.fieldname] = value
|
||||||
|
triggerOnChange(field.fieldname, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultValue(defaultValue, fieldtype) {
|
||||||
|
if (['Float', 'Currency', 'Percent'].includes(fieldtype)) {
|
||||||
|
return flt(defaultValue)
|
||||||
|
} else if (fieldtype === 'Check') {
|
||||||
|
if (['1', 'true', 'True'].includes(defaultValue)) {
|
||||||
|
return true
|
||||||
|
} else if (['0', 'false', 'False'].includes(defaultValue)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (fieldtype === 'Int') {
|
||||||
|
return parseInt(defaultValue)
|
||||||
|
} else if (defaultValue === 'Today' && fieldtype === 'Date') {
|
||||||
|
return dayjs().format('YYYY-MM-DD')
|
||||||
|
} else if (
|
||||||
|
['Now', 'now'].includes(defaultValue) &&
|
||||||
|
fieldtype === 'Datetime'
|
||||||
|
) {
|
||||||
|
return dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
} else if (['Now', 'now'].includes(defaultValue) && fieldtype === 'Time') {
|
||||||
|
return dayjs().format('HH:mm:ss')
|
||||||
|
} else if (fieldtype === 'Date') {
|
||||||
|
return dayjs(defaultValue).format('YYYY-MM-DD')
|
||||||
|
} else if (fieldtype === 'Datetime') {
|
||||||
|
return dayjs(defaultValue).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
} else if (fieldtype === 'Time') {
|
||||||
|
return dayjs(defaultValue).format('HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -139,9 +139,14 @@ const oldFields = computed(() => {
|
|||||||
const fields = ref(JSON.parse(JSON.stringify(oldFields.value || [])))
|
const fields = ref(JSON.parse(JSON.stringify(oldFields.value || [])))
|
||||||
|
|
||||||
const dropdownFields = computed(() => {
|
const dropdownFields = computed(() => {
|
||||||
return getFields()?.filter(
|
return getFields()?.filter((field) => {
|
||||||
(field) => !fields.value.find((f) => f.fieldname === field.fieldname),
|
return (
|
||||||
)
|
!fields.value.find((f) => f.fieldname === field.fieldname) &&
|
||||||
|
!['Tab Break', 'Section Break', 'Column Break', 'Table'].includes(
|
||||||
|
field.fieldtype,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
|||||||
@ -23,7 +23,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data" />
|
<FieldLayout
|
||||||
|
v-if="tabs.data"
|
||||||
|
:tabs="tabs.data"
|
||||||
|
:data="data"
|
||||||
|
:doctype="doctype"
|
||||||
|
:isGridRow="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -60,6 +60,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
const { getFields } = getMeta(props.doctype)
|
const { getFields } = getMeta(props.doctype)
|
||||||
|
|
||||||
const values = defineModel()
|
const values = defineModel()
|
||||||
@ -109,14 +111,16 @@ const addValue = (value) => {
|
|||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
values.value.push({ [linkField.value.fieldname]: value })
|
values.value.push({ [linkField.value.fieldname]: value })
|
||||||
|
emit('change', values.value)
|
||||||
!error.value && (query.value = '')
|
!error.value && (query.value = '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeValue = (value) => {
|
const removeValue = (value) => {
|
||||||
values.value = values.value.filter(
|
let _value = values.value.filter(
|
||||||
(row) => row[linkField.value.fieldname] !== value,
|
(row) => row[linkField.value.fieldname] !== value,
|
||||||
)
|
)
|
||||||
|
emit('change', _value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeLastValue = () => {
|
const removeLastValue = () => {
|
||||||
@ -125,12 +129,11 @@ const removeLastValue = () => {
|
|||||||
let valueRef = valuesRef.value[valuesRef.value.length - 1]?.$el
|
let valueRef = valuesRef.value[valuesRef.value.length - 1]?.$el
|
||||||
if (document.activeElement === valueRef) {
|
if (document.activeElement === valueRef) {
|
||||||
values.value.pop()
|
values.value.pop()
|
||||||
|
emit('change', values.value)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (values.value.length) {
|
if (values.value.length) {
|
||||||
valueRef = valuesRef.value[valuesRef.value.length - 1].$el
|
valueRef = valuesRef.value[valuesRef.value.length - 1].$el
|
||||||
valueRef?.focus()
|
valueRef?.focus()
|
||||||
} else {
|
|
||||||
setFocus()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -37,8 +37,8 @@ import { isMobileView } from '@/composables/settings'
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
actions: {
|
actions: {
|
||||||
type: Object,
|
type: [Object, Array, undefined],
|
||||||
required: true,
|
default: () => [],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ const groupedActions = computed(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
_actions = _actions.concat(
|
_actions = _actions.concat(
|
||||||
props.actions.filter((action) => action.group && !action.buttonLabel)
|
props.actions.filter((action) => action.group && !action.buttonLabel),
|
||||||
)
|
)
|
||||||
return _actions
|
return _actions
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,22 +7,30 @@
|
|||||||
field.reqd ||
|
field.reqd ||
|
||||||
(field.mandatory_depends_on && field.mandatory_via_depends_on)
|
(field.mandatory_depends_on && field.mandatory_via_depends_on)
|
||||||
"
|
"
|
||||||
class="text-ink-red-3"
|
class="text-ink-red-2"
|
||||||
>*</span
|
>*</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="field.read_only && field.fieldtype !== 'Check'"
|
v-if="
|
||||||
|
field.read_only &&
|
||||||
|
!['Int', 'Float', 'Currency', 'Percent', 'Check'].includes(
|
||||||
|
field.fieldtype,
|
||||||
|
)
|
||||||
|
"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
:disabled="true"
|
:disabled="true"
|
||||||
|
:description="field.description"
|
||||||
/>
|
/>
|
||||||
<Grid
|
<Grid
|
||||||
v-else-if="field.fieldtype === 'Table'"
|
v-else-if="field.fieldtype === 'Table'"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
|
v-model:parent="data"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
:parentDoctype="doctype"
|
:parentDoctype="doctype"
|
||||||
|
:parentFieldname="field.fieldname"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Select'"
|
v-else-if="field.fieldtype === 'Select'"
|
||||||
@ -31,7 +39,9 @@
|
|||||||
:class="field.prefix ? 'prefix' : ''"
|
:class="field.prefix ? 'prefix' : ''"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
|
@change="(e) => fieldChange(e.target.value, field)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
|
:description="field.description"
|
||||||
>
|
>
|
||||||
<template v-if="field.prefix" #prefix>
|
<template v-if="field.prefix" #prefix>
|
||||||
<IndicatorIcon :class="field.prefix" />
|
<IndicatorIcon :class="field.prefix" />
|
||||||
@ -42,8 +52,9 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
@change="(e) => (data[field.fieldname] = e.target.checked)"
|
@change="(e) => fieldChange(e.target.checked, field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
|
:description="field.description"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
class="text-sm text-ink-gray-5"
|
class="text-sm text-ink-gray-5"
|
||||||
@ -70,7 +81,7 @@
|
|||||||
field.fieldtype == 'Link' ? field.options : data[field.options]
|
field.fieldtype == 'Link' ? field.options : data[field.options]
|
||||||
"
|
"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="(v) => (data[field.fieldname] = v)"
|
@change="(v) => fieldChange(v, field)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:onCreate="field.create"
|
:onCreate="field.create"
|
||||||
/>
|
/>
|
||||||
@ -90,6 +101,7 @@
|
|||||||
v-else-if="field.fieldtype === 'Table MultiSelect'"
|
v-else-if="field.fieldtype === 'Table MultiSelect'"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
|
@change="(v) => fieldChange(v, field)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@ -98,7 +110,7 @@
|
|||||||
:value="data[field.fieldname] && getUser(data[field.fieldname]).full_name"
|
:value="data[field.fieldname] && getUser(data[field.fieldname]).full_name"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="(v) => (data[field.fieldname] = v)"
|
@change="(v) => fieldChange(v, field)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:hideMe="true"
|
:hideMe="true"
|
||||||
>
|
>
|
||||||
@ -123,80 +135,95 @@
|
|||||||
</Link>
|
</Link>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
v-else-if="field.fieldtype === 'Datetime'"
|
v-else-if="field.fieldtype === 'Datetime'"
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
:formatter="(date) => getFormat(date, '', true, true)"
|
:formatter="(date) => getFormat(date, '', true, true)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
input-class="border-none"
|
input-class="border-none"
|
||||||
|
@change="(v) => fieldChange(v, field)"
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-else-if="field.fieldtype === 'Date'"
|
v-else-if="field.fieldtype === 'Date'"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
:formatter="(date) => getFormat(date, '', true)"
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
input-class="border-none"
|
input-class="border-none"
|
||||||
|
@change="(v) => fieldChange(v, field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="
|
v-else-if="
|
||||||
['Small Text', 'Text', 'Long Text', 'Code'].includes(field.fieldtype)
|
['Small Text', 'Text', 'Long Text', 'Code'].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
:value="data[field.fieldname]"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
:description="field.description"
|
||||||
|
@change="fieldChange($event.target.value, field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="['Int'].includes(field.fieldtype)"
|
v-else-if="['Int'].includes(field.fieldtype)"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
:description="field.description"
|
||||||
|
@change="fieldChange($event.target.value, field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Percent'"
|
v-else-if="field.fieldtype === 'Percent'"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedPercent(field.fieldname, data)"
|
:value="getFormattedPercent(field.fieldname, data)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="data[field.fieldname] = flt($event.target.value)"
|
:description="field.description"
|
||||||
|
@change="fieldChange(flt($event.target.value), field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Float'"
|
v-else-if="field.fieldtype === 'Float'"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedFloat(field.fieldname, data)"
|
:value="getFormattedFloat(field.fieldname, data)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="data[field.fieldname] = flt($event.target.value)"
|
:description="field.description"
|
||||||
|
@change="fieldChange(flt($event.target.value), field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Currency'"
|
v-else-if="field.fieldtype === 'Currency'"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedCurrency(field.fieldname, data)"
|
:value="getFormattedCurrency(field.fieldname, data, parentDoc)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="data[field.fieldname] = flt($event.target.value)"
|
:description="field.description"
|
||||||
|
@change="fieldChange(flt($event.target.value), field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
|
:description="field.description"
|
||||||
|
@change="fieldChange($event.target.value, field)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import FormattedInput from '@/components/Controls/FormattedInput.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import TableMultiselectInput from '@/components/Controls/TableMultiselectInput.vue'
|
import TableMultiselectInput from '@/components/Controls/TableMultiselectInput.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Grid from '@/components/Controls/Grid.vue'
|
import Grid from '@/components/Controls/Grid.vue'
|
||||||
|
import { createDocument } from '@/composables/document'
|
||||||
import { getFormat, evaluateDependsOnValue } from '@/utils'
|
import { getFormat, evaluateDependsOnValue } from '@/utils'
|
||||||
import { flt } from '@/utils/numberFormat.js'
|
import { flt } from '@/utils/numberFormat.js'
|
||||||
import { getMeta } from '@/stores/meta'
|
import { getMeta } from '@/stores/meta'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, provide, inject } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
field: Object,
|
field: Object,
|
||||||
@ -205,11 +232,32 @@ const props = defineProps({
|
|||||||
const data = inject('data')
|
const data = inject('data')
|
||||||
const doctype = inject('doctype')
|
const doctype = inject('doctype')
|
||||||
const preview = inject('preview')
|
const preview = inject('preview')
|
||||||
|
const isGridRow = inject('isGridRow')
|
||||||
|
|
||||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||||
getMeta(doctype)
|
getMeta(doctype)
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
|
let triggerOnChange
|
||||||
|
let parentDoc
|
||||||
|
|
||||||
|
if (!isGridRow) {
|
||||||
|
const {
|
||||||
|
triggerOnChange: trigger,
|
||||||
|
triggerOnRowAdd,
|
||||||
|
triggerOnRowRemove,
|
||||||
|
} = useDocument(doctype, data.value.name)
|
||||||
|
triggerOnChange = trigger
|
||||||
|
|
||||||
|
provide('triggerOnChange', triggerOnChange)
|
||||||
|
provide('triggerOnRowAdd', triggerOnRowAdd)
|
||||||
|
provide('triggerOnRowRemove', triggerOnRowRemove)
|
||||||
|
} else {
|
||||||
|
triggerOnChange = inject('triggerOnChange')
|
||||||
|
parentDoc = inject('parentDoc')
|
||||||
|
}
|
||||||
|
|
||||||
const field = computed(() => {
|
const field = computed(() => {
|
||||||
let field = props.field
|
let field = props.field
|
||||||
if (field.fieldtype == 'Select' && typeof field.options === 'string') {
|
if (field.fieldtype == 'Select' && typeof field.options === 'string') {
|
||||||
@ -226,6 +274,17 @@ const field = computed(() => {
|
|||||||
field.fieldtype = 'User'
|
field.fieldtype = 'User'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.fieldtype === 'Link' && field.options !== 'User') {
|
||||||
|
if (!field.create) {
|
||||||
|
field.create = (value, close) => {
|
||||||
|
const callback = (d) => {
|
||||||
|
if (d) fieldChange(d.name, field)
|
||||||
|
}
|
||||||
|
createDocument(field.options, value, close, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _field = {
|
let _field = {
|
||||||
...field,
|
...field,
|
||||||
filters: field.link_filters && JSON.parse(field.link_filters),
|
filters: field.link_filters && JSON.parse(field.link_filters),
|
||||||
@ -265,6 +324,16 @@ const getPlaceholder = (field) => {
|
|||||||
return __('Enter {0}', [__(field.label)])
|
return __('Enter {0}', [__(field.label)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldChange(value, df) {
|
||||||
|
data.value[df.fieldname] = value
|
||||||
|
|
||||||
|
if (isGridRow) {
|
||||||
|
triggerOnChange(df.fieldname, data.value)
|
||||||
|
} else {
|
||||||
|
triggerOnChange(df.fieldname)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.form-control.prefix select) {
|
:deep(.form-control.prefix select) {
|
||||||
|
|||||||
@ -34,6 +34,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'CRM Lead',
|
default: 'CRM Lead',
|
||||||
},
|
},
|
||||||
|
isGridRow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
preview: {
|
preview: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -55,6 +59,7 @@ provide(
|
|||||||
provide('hasTabs', hasTabs)
|
provide('hasTabs', hasTabs)
|
||||||
provide('doctype', props.doctype)
|
provide('doctype', props.doctype)
|
||||||
provide('preview', props.preview)
|
provide('preview', props.preview)
|
||||||
|
provide('isGridRow', props.isGridRow)
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.section:not(:has(.field)) {
|
.section:not(:has(.field)) {
|
||||||
|
|||||||
@ -277,13 +277,13 @@ const fields = createResource({
|
|||||||
]
|
]
|
||||||
let existingFields = []
|
let existingFields = []
|
||||||
|
|
||||||
for (let tab of props.tabs) {
|
props.tabs?.forEach((tab) => {
|
||||||
for (let section of tab.sections) {
|
tab.sections?.forEach((section) => {
|
||||||
for (let column of section.columns) {
|
section.columns?.forEach((column) => {
|
||||||
existingFields = existingFields.concat(column.fields)
|
existingFields = existingFields.concat(column.fields)
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
return data.filter((field) => {
|
return data.filter((field) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -7,9 +7,11 @@
|
|||||||
<AppHeader />
|
<AppHeader />
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
<GlobalModals />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppSidebar from '@/components/Layouts/AppSidebar.vue'
|
import AppSidebar from '@/components/Layouts/AppSidebar.vue'
|
||||||
import AppHeader from '@/components/Layouts/AppHeader.vue'
|
import AppHeader from '@/components/Layouts/AppHeader.vue'
|
||||||
|
import GlobalModals from '@/components/Modals/GlobalModals.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
147
frontend/src/components/Modals/CreateDocumentModal.vue
Normal file
147
frontend/src/components/Modals/CreateDocumentModal.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model="show" :options="dialogOptions">
|
||||||
|
<template #body>
|
||||||
|
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
|
||||||
|
<div class="mb-5 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||||
|
{{ __(dialogOptions.title) || __('Untitled') }}
|
||||||
|
</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="show = false">
|
||||||
|
<FeatherIcon name="x" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="tabs.data">
|
||||||
|
<FieldLayout :tabs="tabs.data" :data="_data" :doctype="doctype" />
|
||||||
|
<ErrorMessage class="mt-2" :message="error" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 pb-7 pt-4 sm:px-6">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Button
|
||||||
|
class="w-full"
|
||||||
|
v-for="action in dialogOptions.actions"
|
||||||
|
:key="action.label"
|
||||||
|
v-bind="action"
|
||||||
|
:label="__(action.label)"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
import { FeatherIcon, createResource, ErrorMessage, call } from 'frappe-ui'
|
||||||
|
import { ref, nextTick, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['showQuickEntryModal', 'callback'])
|
||||||
|
|
||||||
|
const { isManager } = usersStore()
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
let _data = ref({})
|
||||||
|
|
||||||
|
const dialogOptions = computed(() => {
|
||||||
|
let doctype = props.doctype
|
||||||
|
|
||||||
|
if (doctype.startsWith('CRM ') || doctype.startsWith('FCRM ')) {
|
||||||
|
doctype = doctype.replace(/^(CRM |FCRM )/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = __('New {0}', [doctype])
|
||||||
|
let size = 'xl'
|
||||||
|
let actions = [
|
||||||
|
{
|
||||||
|
label: __('Create'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: () => create(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return { title, size, actions }
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['QuickEntry', props.doctype],
|
||||||
|
params: { doctype: props.doctype, type: 'Quick Entry' },
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function create() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
let doc = await call(
|
||||||
|
'frappe.client.insert',
|
||||||
|
{
|
||||||
|
doc: {
|
||||||
|
doctype: props.doctype,
|
||||||
|
..._data.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
loading.value = false
|
||||||
|
if (err.error) {
|
||||||
|
error.value = err.error.messages?.[0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
show.value = false
|
||||||
|
emit('callback', doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => show.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
_data.value = { ...props.data }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function openQuickEntryModal() {
|
||||||
|
emit('showQuickEntryModal', props.doctype)
|
||||||
|
nextTick(() => {
|
||||||
|
show.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
34
frontend/src/components/Modals/GlobalModals.vue
Normal file
34
frontend/src/components/Modals/GlobalModals.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<CreateDocumentModal
|
||||||
|
v-if="showCreateDocumentModal"
|
||||||
|
v-model="showCreateDocumentModal"
|
||||||
|
:doctype="createDocumentDoctype"
|
||||||
|
:data="createDocumentData"
|
||||||
|
@showQuickEntryModal="(dt) => openQuickEntryModal(dt)"
|
||||||
|
@callback="(data) => createDocumentCallback(data)"
|
||||||
|
/>
|
||||||
|
<QuickEntryModal
|
||||||
|
v-if="showQuickEntryModal"
|
||||||
|
v-model="showQuickEntryModal"
|
||||||
|
:doctype="quickEntryDoctype"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import CreateDocumentModal from '@/components/Modals/CreateDocumentModal.vue'
|
||||||
|
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||||
|
import {
|
||||||
|
showCreateDocumentModal,
|
||||||
|
createDocumentDoctype,
|
||||||
|
createDocumentData,
|
||||||
|
createDocumentCallback,
|
||||||
|
} from '@/composables/document'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const showQuickEntryModal = ref(false)
|
||||||
|
const quickEntryDoctype = ref('')
|
||||||
|
|
||||||
|
function openQuickEntryModal(dt) {
|
||||||
|
showQuickEntryModal.value = true
|
||||||
|
quickEntryDoctype.value = dt
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -101,6 +101,7 @@
|
|||||||
v-model="settings.doc.dropdown_items"
|
v-model="settings.doc.dropdown_items"
|
||||||
doctype="CRM Dropdown Item"
|
doctype="CRM Dropdown Item"
|
||||||
parentDoctype="FCRM Settings"
|
parentDoctype="FCRM Settings"
|
||||||
|
parentFieldname="dropdown_items"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sections flex flex-col overflow-y-auto">
|
<div
|
||||||
|
v-if="!document.get.loading"
|
||||||
|
class="sections flex flex-col overflow-y-auto"
|
||||||
|
>
|
||||||
<template v-for="(section, i) in _sections" :key="section.name">
|
<template v-for="(section, i) in _sections" :key="section.name">
|
||||||
<div v-if="section.visible" class="section flex flex-col">
|
<div v-if="section.visible" class="section flex flex-col">
|
||||||
<div
|
<div
|
||||||
@ -50,7 +53,7 @@
|
|||||||
(field.mandatory_depends_on &&
|
(field.mandatory_depends_on &&
|
||||||
field.mandatory_via_depends_on)
|
field.mandatory_via_depends_on)
|
||||||
"
|
"
|
||||||
class="text-ink-red-3"
|
class="text-ink-red-2"
|
||||||
>*</span
|
>*</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -62,26 +65,33 @@
|
|||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
field.read_only &&
|
field.read_only &&
|
||||||
!['Check', 'Dropdown'].includes(field.fieldtype)
|
![
|
||||||
|
'Int',
|
||||||
|
'Float',
|
||||||
|
'Currency',
|
||||||
|
'Percent',
|
||||||
|
'Check',
|
||||||
|
'Dropdown',
|
||||||
|
].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
class="flex h-7 cursor-pointer items-center px-2 py-1 text-ink-gray-5"
|
class="flex h-7 cursor-pointer items-center px-2 py-1 text-ink-gray-5"
|
||||||
>
|
>
|
||||||
<Tooltip :text="__(field.tooltip)">
|
<Tooltip :text="__(field.tooltip)">
|
||||||
<div>{{ data[field.fieldname] }}</div>
|
<div>{{ document.doc[field.fieldname] }}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="field.fieldtype === 'Dropdown'">
|
<div v-else-if="field.fieldtype === 'Dropdown'">
|
||||||
<NestedPopover>
|
<NestedPopover>
|
||||||
<template #target="{ open }">
|
<template #target="{ open }">
|
||||||
<Button
|
<Button
|
||||||
:label="data[field.fieldname]"
|
:label="document.doc[field.fieldname]"
|
||||||
class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-surface-gray-2 px-2 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3"
|
class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-surface-gray-2 px-2 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="data[field.fieldname]"
|
v-if="document.doc[field.fieldname]"
|
||||||
class="truncate"
|
class="truncate"
|
||||||
>
|
>
|
||||||
{{ data[field.fieldname] }}
|
{{ document.doc[field.fieldname] }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@ -138,13 +148,9 @@
|
|||||||
v-else-if="field.fieldtype == 'Check'"
|
v-else-if="field.fieldtype == 'Check'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="data[field.fieldname]"
|
v-model="document.doc[field.fieldname]"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange($event.target.checked, field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
$event.target.checked,
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
/>
|
/>
|
||||||
@ -159,43 +165,40 @@
|
|||||||
"
|
"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Select'"
|
v-else-if="field.fieldtype === 'Select'"
|
||||||
class="form-control cursor-pointer [&_select]:cursor-pointer truncate"
|
class="form-control cursor-pointer [&_select]:cursor-pointer truncate"
|
||||||
type="select"
|
type="select"
|
||||||
v-model="data[field.fieldname]"
|
v-model="document.doc[field.fieldname]"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
v-else-if="field.fieldtype === 'User'"
|
v-else-if="field.fieldtype === 'User'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:value="
|
:value="
|
||||||
data[field.fieldname] &&
|
document.doc[field.fieldname] &&
|
||||||
getUser(data[field.fieldname]).full_name
|
getUser(document.doc[field.fieldname]).full_name
|
||||||
"
|
"
|
||||||
doctype="User"
|
doctype="User"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
:placeholder="'Select' + ' ' + field.label + '...'"
|
:placeholder="'Select' + ' ' + field.label + '...'"
|
||||||
:hideMe="true"
|
:hideMe="true"
|
||||||
>
|
>
|
||||||
<template v-if="data[field.fieldname]" #prefix>
|
<template
|
||||||
|
v-if="document.doc[field.fieldname]"
|
||||||
|
#prefix
|
||||||
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
class="mr-1.5"
|
class="mr-1.5"
|
||||||
:user="data[field.fieldname]"
|
:user="document.doc[field.fieldname]"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -219,17 +222,15 @@
|
|||||||
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
class="form-control select-text"
|
class="form-control select-text"
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:doctype="
|
:doctype="
|
||||||
field.fieldtype == 'Link'
|
field.fieldtype == 'Link'
|
||||||
? field.options
|
? field.options
|
||||||
: data[field.options]
|
: document.doc[field.options]
|
||||||
"
|
"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
:onCreate="field.create"
|
:onCreate="field.create"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@ -238,15 +239,13 @@
|
|||||||
>
|
>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
icon-left=""
|
icon-left=""
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:formatter="
|
:formatter="
|
||||||
(date) => getFormat(date, '', true, true)
|
(date) => getFormat(date, '', true, true)
|
||||||
"
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
placement="left-start"
|
placement="left-start"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -255,81 +254,73 @@
|
|||||||
>
|
>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
icon-left=""
|
icon-left=""
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:formatter="(date) => getFormat(date, '', true)"
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
placement="left-start"
|
placement="left-start"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Percent'"
|
v-else-if="field.fieldtype === 'Percent'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedPercent(field.fieldname, data)"
|
:value="
|
||||||
|
getFormattedPercent(field.fieldname, document.doc)
|
||||||
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange(flt($event.target.value), field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
flt($event.target.value),
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Int'"
|
v-else-if="field.fieldtype === 'Int'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="number"
|
type="text"
|
||||||
v-model="data[field.fieldname]"
|
v-model="document.doc[field.fieldname]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
:disabled="Boolean(field.read_only)"
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Float'"
|
v-else-if="field.fieldtype === 'Float'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedFloat(field.fieldname, data)"
|
:value="
|
||||||
|
getFormattedFloat(field.fieldname, document.doc)
|
||||||
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange(flt($event.target.value), field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
flt($event.target.value),
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Currency'"
|
v-else-if="field.fieldtype === 'Currency'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedCurrency(field.fieldname, data)"
|
:value="
|
||||||
|
getFormattedCurrency(field.fieldname, document.doc)
|
||||||
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange(flt($event.target.value), field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
flt($event.target.value),
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-1">
|
<div class="ml-1">
|
||||||
@ -337,19 +328,23 @@
|
|||||||
v-if="
|
v-if="
|
||||||
field.fieldtype === 'Link' &&
|
field.fieldtype === 'Link' &&
|
||||||
field.link &&
|
field.link &&
|
||||||
data[field.fieldname]
|
document.doc[field.fieldname]
|
||||||
"
|
"
|
||||||
class="h-4 w-4 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
class="h-4 w-4 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
||||||
@click.stop="field.link(data[field.fieldname])"
|
@click.stop="
|
||||||
|
field.link(document.doc[field.fieldname])
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<EditIcon
|
<EditIcon
|
||||||
v-if="
|
v-if="
|
||||||
field.fieldtype === 'Link' &&
|
field.fieldtype === 'Link' &&
|
||||||
field.edit &&
|
field.edit &&
|
||||||
data[field.fieldname]
|
document.doc[field.fieldname]
|
||||||
"
|
"
|
||||||
class="size-3.5 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
class="size-3.5 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
||||||
@click.stop="field.edit(data[field.fieldname])"
|
@click.stop="
|
||||||
|
field.edit(document.doc[field.fieldname])
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -371,6 +366,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import FormattedInput from '@/components/Controls/FormattedInput.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import NestedPopover from '@/components/NestedPopover.vue'
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
import DropdownItem from '@/components/DropdownItem.vue'
|
import DropdownItem from '@/components/DropdownItem.vue'
|
||||||
@ -386,6 +382,7 @@ 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 } from 'frappe-ui'
|
import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -395,6 +392,11 @@ const props = defineProps({
|
|||||||
doctype: {
|
doctype: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'CRM Lead',
|
default: 'CRM Lead',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
docname: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -407,13 +409,15 @@ const props = defineProps({
|
|||||||
|
|
||||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||||
getMeta(props.doctype)
|
getMeta(props.doctype)
|
||||||
|
|
||||||
const { isManager, getUser } = usersStore()
|
const { isManager, getUser } = usersStore()
|
||||||
|
|
||||||
const emit = defineEmits(['update', 'reload'])
|
const emit = defineEmits(['update', 'reload'])
|
||||||
|
|
||||||
const data = defineModel()
|
|
||||||
const showSidePanelModal = ref(false)
|
const showSidePanelModal = ref(false)
|
||||||
|
|
||||||
|
const { document, triggerOnChange } = useDocument(props.doctype, props.docname)
|
||||||
|
|
||||||
const _sections = computed(() => {
|
const _sections = computed(() => {
|
||||||
if (!props.sections?.length) return []
|
if (!props.sections?.length) return []
|
||||||
let editButtonAdded = false
|
let editButtonAdded = false
|
||||||
@ -453,11 +457,11 @@ function parsedField(field) {
|
|||||||
placeholder: field.placeholder || field.label,
|
placeholder: field.placeholder || field.label,
|
||||||
display_via_depends_on: evaluateDependsOnValue(
|
display_via_depends_on: evaluateDependsOnValue(
|
||||||
field.depends_on,
|
field.depends_on,
|
||||||
data.value,
|
document.doc,
|
||||||
),
|
),
|
||||||
mandatory_via_depends_on: evaluateDependsOnValue(
|
mandatory_via_depends_on: evaluateDependsOnValue(
|
||||||
field.mandatory_depends_on,
|
field.mandatory_depends_on,
|
||||||
data.value,
|
document.doc,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,6 +469,14 @@ function parsedField(field) {
|
|||||||
return _field
|
return _field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fieldChange(value, df) {
|
||||||
|
document.doc[df.fieldname] = value
|
||||||
|
|
||||||
|
await triggerOnChange(df.fieldname)
|
||||||
|
|
||||||
|
document.save.submit()
|
||||||
|
}
|
||||||
|
|
||||||
function parsedSection(section, editButtonAdded) {
|
function parsedSection(section, editButtonAdded) {
|
||||||
let isContactSection = section.name == 'contacts_section'
|
let isContactSection = section.name == 'contacts_section'
|
||||||
section.showEditButton = !(
|
section.showEditButton = !(
|
||||||
@ -485,7 +497,7 @@ function isFieldVisible(field) {
|
|||||||
if (props.preview) return true
|
if (props.preview) return true
|
||||||
return (
|
return (
|
||||||
(field.fieldtype == 'Check' ||
|
(field.fieldtype == 'Check' ||
|
||||||
(field.read_only && data.value[field.fieldname]) ||
|
(field.read_only && document.doc?.[field.fieldname]) ||
|
||||||
!field.read_only) &&
|
!field.read_only) &&
|
||||||
(!field.depends_on || field.display_via_depends_on) &&
|
(!field.depends_on || field.display_via_depends_on) &&
|
||||||
!field.hidden
|
!field.hidden
|
||||||
|
|||||||
16
frontend/src/composables/document.js
Normal file
16
frontend/src/composables/document.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const showCreateDocumentModal = ref(false)
|
||||||
|
export const createDocumentDoctype = ref('')
|
||||||
|
export const createDocumentData = ref({})
|
||||||
|
export const createDocumentCallback = ref(null)
|
||||||
|
|
||||||
|
export function createDocument(doctype, obj, close, callback) {
|
||||||
|
if (doctype) {
|
||||||
|
close?.()
|
||||||
|
createDocumentDoctype.value = doctype
|
||||||
|
createDocumentData.value = obj || {}
|
||||||
|
createDocumentCallback.value = callback || null
|
||||||
|
showCreateDocumentModal.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
142
frontend/src/data/document.js
Normal file
142
frontend/src/data/document.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { getScript } from '@/data/script'
|
||||||
|
import { createToast, runSequentially } from '@/utils'
|
||||||
|
import { createDocumentResource } from 'frappe-ui'
|
||||||
|
|
||||||
|
const documentsCache = {}
|
||||||
|
const controllersCache = {}
|
||||||
|
|
||||||
|
export function useDocument(doctype, docname) {
|
||||||
|
const { setupScript } = getScript(doctype)
|
||||||
|
|
||||||
|
documentsCache[doctype] = documentsCache[doctype] || {}
|
||||||
|
|
||||||
|
if (!documentsCache[doctype][docname]) {
|
||||||
|
documentsCache[doctype][docname] = createDocumentResource({
|
||||||
|
doctype: doctype,
|
||||||
|
name: docname,
|
||||||
|
onSuccess: () => setupFormScript(),
|
||||||
|
setValue: {
|
||||||
|
onSuccess: () => {
|
||||||
|
createToast({
|
||||||
|
title: __('Document updated successfully'),
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'text-ink-green-3',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
createToast({
|
||||||
|
title: __('Error updating document'),
|
||||||
|
text: err.messages[0],
|
||||||
|
icon: 'x',
|
||||||
|
iconClasses: 'text-red-600',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupFormScript() {
|
||||||
|
if (controllersCache[doctype]?.[docname]) return
|
||||||
|
|
||||||
|
if (!controllersCache[doctype]) {
|
||||||
|
controllersCache[doctype] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
controllersCache[doctype][docname] = setupScript(
|
||||||
|
documentsCache[doctype][docname],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getControllers(row = null) {
|
||||||
|
const _doctype = row?.doctype || doctype
|
||||||
|
return (controllersCache[doctype]?.[docname] || []).filter(
|
||||||
|
(c) => c.constructor.name === _doctype.replace(/\s+/g, ''),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnRefresh() {
|
||||||
|
const handler = async function () {
|
||||||
|
await this.refresh()
|
||||||
|
}
|
||||||
|
await trigger(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnChange(fieldname, row) {
|
||||||
|
const handler = async function () {
|
||||||
|
if (row) {
|
||||||
|
this.currentRowIdx = row.idx
|
||||||
|
this.value = row[fieldname]
|
||||||
|
this.oldValue = getOldValue(fieldname, row)
|
||||||
|
} else {
|
||||||
|
this.value = documentsCache[doctype][docname].doc[fieldname]
|
||||||
|
this.oldValue = getOldValue(fieldname)
|
||||||
|
}
|
||||||
|
await this[fieldname]?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
await trigger(handler, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnRowAdd(row) {
|
||||||
|
const handler = async function () {
|
||||||
|
this.currentRowIdx = row.idx
|
||||||
|
this.value = row
|
||||||
|
await this[row.parentfield + '_add']?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
await trigger(handler, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnRowRemove(selectedRows, rows) {
|
||||||
|
const handler = async function () {
|
||||||
|
if (selectedRows.size === 1) {
|
||||||
|
const selectedRow = Array.from(selectedRows)[0]
|
||||||
|
this.currentRowIdx = rows.find((r) => r.name === selectedRow).idx
|
||||||
|
} else {
|
||||||
|
delete this.currentRowIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedRows = Array.from(selectedRows)
|
||||||
|
this.rows = rows
|
||||||
|
|
||||||
|
await this[rows[0].parentfield + '_remove']?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
await trigger(handler, rows[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trigger(taskFn, row = null) {
|
||||||
|
const controllers = getControllers(row)
|
||||||
|
if (!controllers.length) return
|
||||||
|
|
||||||
|
const tasks = controllers.map(
|
||||||
|
(controller) => async () => await taskFn.call(controller),
|
||||||
|
)
|
||||||
|
|
||||||
|
await runSequentially(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOldValue(fieldname, row) {
|
||||||
|
if (!documentsCache[doctype][docname]) return ''
|
||||||
|
|
||||||
|
const document = documentsCache[doctype][docname]
|
||||||
|
const oldDoc = document.originalDoc
|
||||||
|
|
||||||
|
if (row?.name) {
|
||||||
|
return oldDoc?.[row.parentfield]?.find((r) => r.name === row.name)?.[
|
||||||
|
fieldname
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldDoc?.[fieldname] || document.doc[fieldname]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
document: documentsCache[doctype][docname],
|
||||||
|
triggerOnChange,
|
||||||
|
triggerOnRowAdd,
|
||||||
|
triggerOnRowRemove,
|
||||||
|
triggerOnRefresh,
|
||||||
|
setupFormScript,
|
||||||
|
}
|
||||||
|
}
|
||||||
271
frontend/src/data/script.js
Normal file
271
frontend/src/data/script.js
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import { globalStore } from '@/stores/global'
|
||||||
|
import { getMeta } from '@/stores/meta'
|
||||||
|
import { createToast } from '@/utils'
|
||||||
|
import { call, createListResource } from 'frappe-ui'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
const doctypeScripts = reactive({})
|
||||||
|
|
||||||
|
export function getScript(doctype, view = 'Form') {
|
||||||
|
const scripts = createListResource({
|
||||||
|
doctype: 'CRM Form Script',
|
||||||
|
cache: ['Form Scripts', doctype, view],
|
||||||
|
fields: ['name', 'dt', 'view', 'script'],
|
||||||
|
filters: { view, dt: doctype, enabled: 1 },
|
||||||
|
onSuccess: (_scripts) => {
|
||||||
|
for (let script of _scripts) {
|
||||||
|
if (!doctypeScripts[doctype]) {
|
||||||
|
doctypeScripts[doctype] = {}
|
||||||
|
}
|
||||||
|
doctypeScripts[doctype][script.name] = script || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!doctypeScripts[doctype] && !scripts.loading) {
|
||||||
|
scripts.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupScript(document, helpers = {}) {
|
||||||
|
let scripts = doctypeScripts[doctype]
|
||||||
|
if (!scripts) return null
|
||||||
|
|
||||||
|
const { $dialog, $socket, makeCall } = globalStore()
|
||||||
|
|
||||||
|
helpers.createDialog = $dialog
|
||||||
|
helpers.createToast = createToast
|
||||||
|
helpers.socket = $socket
|
||||||
|
helpers.router = router
|
||||||
|
helpers.call = call
|
||||||
|
|
||||||
|
helpers.crm = {
|
||||||
|
makePhoneCall: makeCall,
|
||||||
|
}
|
||||||
|
|
||||||
|
return setupMultipleFormControllers(scripts, document, helpers)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMultipleFormControllers(scriptStrings, document, helpers) {
|
||||||
|
const controllers = []
|
||||||
|
let parentInstanceIdx = null
|
||||||
|
|
||||||
|
for (let scriptName in scriptStrings) {
|
||||||
|
let script = scriptStrings[scriptName]?.script
|
||||||
|
if (!script) continue
|
||||||
|
try {
|
||||||
|
const classNames = getClassNames(script)
|
||||||
|
if (!classNames) continue
|
||||||
|
|
||||||
|
classNames.forEach((className) => {
|
||||||
|
const FormClass = evaluateFormClass(script, className, helpers)
|
||||||
|
if (!FormClass) return
|
||||||
|
|
||||||
|
let parentInstance = null
|
||||||
|
let doctypeName = doctype.replace(/\s+/g, '')
|
||||||
|
|
||||||
|
let { doctypeMeta } = getMeta(doctype)
|
||||||
|
|
||||||
|
// if className is not doctype name, then it is a child doctype
|
||||||
|
let isChildDoctype = className !== doctypeName
|
||||||
|
|
||||||
|
if (isChildDoctype) {
|
||||||
|
if (!controllers.length) {
|
||||||
|
console.error(
|
||||||
|
__(
|
||||||
|
'⚠️ No class found for doctype: {0}, it is mandatory to have a class for the parent doctype. it can be empty, but it should be present.',
|
||||||
|
[doctype],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parentInstance = controllers[parentInstanceIdx]
|
||||||
|
} else {
|
||||||
|
parentInstanceIdx = controllers.length || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = setupFormController(
|
||||||
|
FormClass,
|
||||||
|
doctypeMeta,
|
||||||
|
document,
|
||||||
|
parentInstance,
|
||||||
|
isChildDoctype,
|
||||||
|
)
|
||||||
|
|
||||||
|
controllers.push(instance)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(__('Failed to load form controller: {0}', [err]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return controllers
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupFormController(
|
||||||
|
FormClass,
|
||||||
|
meta,
|
||||||
|
document,
|
||||||
|
parentInstance = null,
|
||||||
|
isChildDoctype = false,
|
||||||
|
) {
|
||||||
|
let instance = new FormClass()
|
||||||
|
|
||||||
|
instance._isChildDoctype = isChildDoctype
|
||||||
|
|
||||||
|
for (const key in document) {
|
||||||
|
if (document.hasOwnProperty(key)) {
|
||||||
|
instance[key] = document[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.getMeta = async (doctype) => {
|
||||||
|
if (!meta[doctype]) {
|
||||||
|
await getMeta(doctype)
|
||||||
|
return meta[doctype]
|
||||||
|
}
|
||||||
|
return meta[doctype]
|
||||||
|
}
|
||||||
|
|
||||||
|
setupHelperMethods(FormClass, document)
|
||||||
|
|
||||||
|
if (isChildDoctype) {
|
||||||
|
instance.doc = createDocProxy(document.doc, parentInstance, instance)
|
||||||
|
|
||||||
|
if (!parentInstance._childInstances) {
|
||||||
|
parentInstance._childInstances = []
|
||||||
|
}
|
||||||
|
|
||||||
|
parentInstance._childInstances.push(instance)
|
||||||
|
} else {
|
||||||
|
instance.doc = createDocProxy(document.doc, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupHelperMethods(FormClass, document) {
|
||||||
|
if (typeof FormClass.prototype.getRow !== 'function') {
|
||||||
|
FormClass.prototype.getRow = function (parentField, idx) {
|
||||||
|
let data = document.doc
|
||||||
|
idx = idx || this.currentRowIdx
|
||||||
|
|
||||||
|
let dt = null
|
||||||
|
|
||||||
|
if (this instanceof Array) {
|
||||||
|
const { getFields } = getMeta(data.doctype)
|
||||||
|
let fields = getFields()
|
||||||
|
let field = fields.find((f) => f.fieldname === parentField)
|
||||||
|
dt = field?.options?.replace(/\s+/g, '')
|
||||||
|
|
||||||
|
if (!idx && dt) {
|
||||||
|
idx = this.find((r) => r.constructor.name === dt)?.currentRowIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data[parentField]) {
|
||||||
|
console.warn(
|
||||||
|
__('⚠️ No data found for parent field: {0}', [parentField]),
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const row = data[parentField].find((r) => r.idx === idx)
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
console.warn(
|
||||||
|
__('⚠️ No row found for idx: {0} in parent field: {1}', [
|
||||||
|
idx,
|
||||||
|
parentField,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
row.parent = row.parent || data.name
|
||||||
|
|
||||||
|
if (this instanceof Array && dt) {
|
||||||
|
return createDocProxy(
|
||||||
|
row,
|
||||||
|
this.find((r) => r.constructor.name === dt),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createDocProxy(row, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility function to setup a form controller
|
||||||
|
function getClassNames(script) {
|
||||||
|
const withoutComments = script
|
||||||
|
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
||||||
|
|
||||||
|
// Match class declarations
|
||||||
|
return (
|
||||||
|
[...withoutComments.matchAll(/class\s+([A-Za-z0-9_]+)/g)].map(
|
||||||
|
(match) => match[1],
|
||||||
|
) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateFormClass(script, className, helpers = {}) {
|
||||||
|
const helperKeys = Object.keys(helpers)
|
||||||
|
const helperValues = Object.values(helpers)
|
||||||
|
|
||||||
|
const wrappedScript = `
|
||||||
|
${script}
|
||||||
|
return ${className};
|
||||||
|
`
|
||||||
|
|
||||||
|
const FormClass = new Function(...helperKeys, wrappedScript)(
|
||||||
|
...helperValues,
|
||||||
|
)
|
||||||
|
return FormClass
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDocProxy(data, instance, childInstance = null) {
|
||||||
|
return new Proxy(data, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (prop === 'trigger') {
|
||||||
|
if ('trigger' in data) {
|
||||||
|
console.warn(
|
||||||
|
__(
|
||||||
|
'⚠️ Avoid using "trigger" as a field name — it conflicts with the built-in trigger() method.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (methodName, ...args) => {
|
||||||
|
const method = instance[methodName]
|
||||||
|
if (typeof method === 'function') {
|
||||||
|
return method.apply(instance, args)
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
__('⚠️ Method "{0}" not found in class.', [methodName]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === 'getRow') {
|
||||||
|
return instance.getRow.bind(
|
||||||
|
childInstance || instance._childInstances || instance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[prop]
|
||||||
|
},
|
||||||
|
set(target, prop, value) {
|
||||||
|
target[prop] = value
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scripts,
|
||||||
|
setupScript,
|
||||||
|
setupFormController,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -129,10 +129,10 @@
|
|||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<SidePanelLayout
|
<SidePanelLayout
|
||||||
v-model="deal.data"
|
|
||||||
:sections="sections.data"
|
:sections="sections.data"
|
||||||
:addContact="addContact"
|
:addContact="addContact"
|
||||||
doctype="CRM Deal"
|
doctype="CRM Deal"
|
||||||
|
:docname="deal.data.name"
|
||||||
@update="updateField"
|
@update="updateField"
|
||||||
@reload="sections.reload"
|
@reload="sections.reload"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -182,9 +182,9 @@
|
|||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<SidePanelLayout
|
<SidePanelLayout
|
||||||
v-model="lead.data"
|
|
||||||
:sections="sections.data"
|
:sections="sections.data"
|
||||||
doctype="CRM Lead"
|
doctype="CRM Lead"
|
||||||
|
:docname="lead.data.name"
|
||||||
@update="updateField"
|
@update="updateField"
|
||||||
@reload="sections.reload"
|
@reload="sections.reload"
|
||||||
/>
|
/>
|
||||||
@ -706,10 +706,10 @@ const dealTabs = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
transform: (_tabs) => {
|
transform: (_tabs) => {
|
||||||
let hasFields = false
|
let hasFields = false
|
||||||
let parsedTabs = _tabs.forEach((tab) => {
|
let parsedTabs = _tabs?.forEach((tab) => {
|
||||||
tab.sections.forEach((section) => {
|
tab.sections?.forEach((section) => {
|
||||||
section.columns.forEach((column) => {
|
section.columns?.forEach((column) => {
|
||||||
column.fields.forEach((field) => {
|
column.fields?.forEach((field) => {
|
||||||
hasFields = true
|
hasFields = true
|
||||||
if (field.fieldname == 'status') {
|
if (field.fieldname == 'status') {
|
||||||
field.fieldtype = 'Select'
|
field.fieldtype = 'Select'
|
||||||
|
|||||||
@ -39,7 +39,19 @@ export function getMeta(doctype) {
|
|||||||
return formatNumber(doc[fieldname], '', precision)
|
return formatNumber(doc[fieldname], '', precision)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFormattedCurrency(fieldname, doc) {
|
function getFloatWithPrecision(fieldname, doc) {
|
||||||
|
let df = doctypeMeta[doctype]?.fields.find((f) => f.fieldname == fieldname)
|
||||||
|
let precision = df?.precision || null
|
||||||
|
return formatNumber(doc[fieldname], '', precision)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrencyWithPrecision(fieldname, doc) {
|
||||||
|
let df = doctypeMeta[doctype]?.fields.find((f) => f.fieldname == fieldname)
|
||||||
|
let precision = df?.precision || null
|
||||||
|
return formatCurrency(doc[fieldname], '', '', precision)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedCurrency(fieldname, doc, parentDoc = null) {
|
||||||
let currency = window.sysdefaults.currency || 'USD'
|
let currency = window.sysdefaults.currency || 'USD'
|
||||||
let df = doctypeMeta[doctype]?.fields.find((f) => f.fieldname == fieldname)
|
let df = doctypeMeta[doctype]?.fields.find((f) => f.fieldname == fieldname)
|
||||||
let precision = df?.precision || null
|
let precision = df?.precision || null
|
||||||
@ -47,8 +59,11 @@ export function getMeta(doctype) {
|
|||||||
if (df && df.options) {
|
if (df && df.options) {
|
||||||
if (df.options.indexOf(':') != -1) {
|
if (df.options.indexOf(':') != -1) {
|
||||||
currency = currency
|
currency = currency
|
||||||
|
// TODO: Handle this case
|
||||||
} else if (doc && doc[df.options]) {
|
} else if (doc && doc[df.options]) {
|
||||||
currency = doc[df.options]
|
currency = doc[df.options]
|
||||||
|
} else if (parentDoc && parentDoc[df.options]) {
|
||||||
|
currency = parentDoc[df.options]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +141,8 @@ export function getMeta(doctype) {
|
|||||||
getGridSettings,
|
getGridSettings,
|
||||||
getGridViewSettings,
|
getGridViewSettings,
|
||||||
saveUserSettings,
|
saveUserSettings,
|
||||||
|
getFloatWithPrecision,
|
||||||
|
getCurrencyWithPrecision,
|
||||||
getFormattedFloat,
|
getFormattedFloat,
|
||||||
getFormattedPercent,
|
getFormattedPercent,
|
||||||
getFormattedCurrency,
|
getFormattedCurrency,
|
||||||
|
|||||||
@ -152,6 +152,7 @@ export function setupAssignees(doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getFormScript(script, obj) {
|
async function getFormScript(script, obj) {
|
||||||
|
if (!script.includes('setupForm(')) return {}
|
||||||
let scriptFn = new Function(script + '\nreturn setupForm')()
|
let scriptFn = new Function(script + '\nreturn setupForm')()
|
||||||
let formScript = await scriptFn(obj)
|
let formScript = await scriptFn(obj)
|
||||||
return formScript || {}
|
return formScript || {}
|
||||||
@ -348,3 +349,9 @@ export function getRandom(len = 4) {
|
|||||||
|
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function runSequentially(functions) {
|
||||||
|
return functions.reduce((promise, fn) => {
|
||||||
|
return promise.then(() => fn())
|
||||||
|
}, Promise.resolve())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { get } from '@vueuse/core'
|
|
||||||
|
|
||||||
const NUMBER_FORMAT_INFO = {
|
const NUMBER_FORMAT_INFO = {
|
||||||
'#,###.##': { decimalStr: '.', groupSep: ',' },
|
'#,###.##': { decimalStr: '.', groupSep: ',' },
|
||||||
'#.###,##': { decimalStr: ',', groupSep: '.' },
|
'#.###,##': { decimalStr: ',', groupSep: '.' },
|
||||||
@ -183,10 +181,13 @@ export function formatCurrency(value, format, currency = 'USD', precision = 2) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
format = getNumberFormat(format)
|
format = getNumberFormat(format)
|
||||||
let symbol = getCurrencySymbol(currency)
|
|
||||||
|
|
||||||
if (symbol) {
|
if (currency) {
|
||||||
return __(symbol) + ' ' + formatNumber(value, format, precision)
|
let symbol = getCurrencySymbol(currency)
|
||||||
|
|
||||||
|
if (symbol) {
|
||||||
|
return __(symbol) + ' ' + formatNumber(value, format, precision)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatNumber(value, format, precision)
|
return formatNumber(value, format, precision)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user