Merge pull request #817 from frappe/main-hotfix

This commit is contained in:
Shariq Ansari 2025-05-15 00:06:42 +05:30 committed by GitHub
commit 7396878a3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 2003 additions and 262 deletions

View File

@ -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");
}
}); });

View File

@ -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",

View File

@ -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:

View File

@ -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");
}
}); });

View File

@ -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,

View File

View 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);
}
});

View 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
}

View 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()

View 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

View 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": []
}

View 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')
}
}"""
)

View File

@ -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)

View File

@ -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 ""

View File

@ -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

View File

@ -0,0 +1,5 @@
from crm.install import add_default_scripts
def execute():
add_default_scripts()

View File

@ -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")

View File

@ -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']

View File

@ -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
} }
} }
}, },

View 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>

View File

@ -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>

View File

@ -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() {

View File

@ -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>

View File

@ -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 {

View File

@ -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
}) })

View File

@ -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) {

View File

@ -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)) {

View File

@ -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 (

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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

View 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
}
}

View 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
View 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,
}
}

View File

@ -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"
> >

View File

@ -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'

View File

@ -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,

View File

@ -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())
}

View File

@ -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,11 +181,14 @@ export function formatCurrency(value, format, currency = 'USD', precision = 2) {
// } // }
format = getNumberFormat(format) format = getNumberFormat(format)
if (currency) {
let symbol = getCurrencySymbol(currency) let symbol = getCurrencySymbol(currency)
if (symbol) { if (symbol) {
return __(symbol) + ' ' + formatNumber(value, format, precision) return __(symbol) + ' ' + formatNumber(value, format, precision)
} }
}
return formatNumber(value, format, precision) return formatNumber(value, format, precision)
} }