Merge pull request #816 from frappe/mergify/bp/main-hotfix/pr-814
This commit is contained in:
commit
0b6954e728
@ -5,4 +5,68 @@ frappe.ui.form.on("CRM Deal", {
|
|||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
frm.add_web_link(`/crm/deals/${frm.doc.name}`, __("Open in Portal"));
|
frm.add_web_link(`/crm/deals/${frm.doc.name}`, __("Open in Portal"));
|
||||||
},
|
},
|
||||||
|
update_total: function (frm) {
|
||||||
|
let total = 0;
|
||||||
|
let total_qty = 0;
|
||||||
|
let net_total = 0;
|
||||||
|
frm.doc.products.forEach((d) => {
|
||||||
|
total += d.amount;
|
||||||
|
total_qty += d.qty;
|
||||||
|
net_total += d.net_amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
|
||||||
|
frappe.model.set_value(
|
||||||
|
frm.doctype,
|
||||||
|
frm.docname,
|
||||||
|
"net_total",
|
||||||
|
net_total || total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("CRM Products", {
|
||||||
|
products_add: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
products_remove: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
product_code: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
|
||||||
|
},
|
||||||
|
rate: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
qty: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
discount_percentage: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.discount_percentage && d.amount) {
|
||||||
|
discount_amount = (d.discount_percentage / 100) * d.amount;
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"discount_amount",
|
||||||
|
discount_amount
|
||||||
|
);
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"net_amount",
|
||||||
|
d.amount - discount_amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -43,6 +43,12 @@
|
|||||||
"mobile_no",
|
"mobile_no",
|
||||||
"phone",
|
"phone",
|
||||||
"gender",
|
"gender",
|
||||||
|
"products_tab",
|
||||||
|
"products",
|
||||||
|
"section_break_ccbj",
|
||||||
|
"total",
|
||||||
|
"column_break_udbq",
|
||||||
|
"net_total",
|
||||||
"sla_tab",
|
"sla_tab",
|
||||||
"sla",
|
"sla",
|
||||||
"sla_creation",
|
"sla_creation",
|
||||||
@ -334,11 +340,46 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
"options": "Currency"
|
"options": "Currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Products",
|
||||||
|
"options": "CRM Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ccbj",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_udbq",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Total after discount",
|
||||||
|
"fieldname": "net_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-11 14:31:41.058895",
|
"modified": "2025-05-12 12:30:55.415282",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
@ -370,10 +411,11 @@
|
|||||||
"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",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "organization",
|
"title_field": "organization",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,4 +5,68 @@ frappe.ui.form.on("CRM Lead", {
|
|||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
frm.add_web_link(`/crm/leads/${frm.doc.name}`, __("Open in Portal"));
|
frm.add_web_link(`/crm/leads/${frm.doc.name}`, __("Open in Portal"));
|
||||||
},
|
},
|
||||||
|
update_total: function (frm) {
|
||||||
|
let total = 0;
|
||||||
|
let total_qty = 0;
|
||||||
|
let net_total = 0;
|
||||||
|
frm.doc.products.forEach((d) => {
|
||||||
|
total += d.amount;
|
||||||
|
total_qty += d.qty;
|
||||||
|
net_total += d.net_amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
|
||||||
|
frappe.model.set_value(
|
||||||
|
frm.doctype,
|
||||||
|
frm.docname,
|
||||||
|
"net_total",
|
||||||
|
net_total || total
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("CRM Products", {
|
||||||
|
products_add: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
products_remove: function (frm, cdt, cdn) {
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
product_code: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
|
||||||
|
},
|
||||||
|
rate: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
qty: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.rate && d.qty) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
},
|
||||||
|
discount_percentage: function (frm, cdt, cdn) {
|
||||||
|
let d = frappe.get_doc(cdt, cdn);
|
||||||
|
if (d.discount_percentage && d.amount) {
|
||||||
|
discount_amount = (d.discount_percentage / 100) * d.amount;
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"discount_amount",
|
||||||
|
discount_amount
|
||||||
|
);
|
||||||
|
frappe.model.set_value(
|
||||||
|
cdt,
|
||||||
|
cdn,
|
||||||
|
"net_amount",
|
||||||
|
d.amount - discount_amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frm.trigger("update_total");
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -37,6 +37,12 @@
|
|||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"image",
|
"image",
|
||||||
"converted",
|
"converted",
|
||||||
|
"products_tab",
|
||||||
|
"products",
|
||||||
|
"section_break_ggwh",
|
||||||
|
"total",
|
||||||
|
"column_break_uisv",
|
||||||
|
"net_total",
|
||||||
"sla_tab",
|
"sla_tab",
|
||||||
"sla",
|
"sla",
|
||||||
"sla_creation",
|
"sla_creation",
|
||||||
@ -285,12 +291,47 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Status Change Log",
|
"label": "Status Change Log",
|
||||||
"options": "CRM Status Change Log"
|
"options": "CRM Status Change Log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "products",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Products",
|
||||||
|
"options": "CRM Products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ggwh",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_uisv",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Total after discount",
|
||||||
|
"fieldname": "net_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-02 22:14:01.991054",
|
"modified": "2025-05-14 19:51:06.184569",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Lead",
|
"name": "CRM Lead",
|
||||||
@ -331,6 +372,7 @@
|
|||||||
"share": 1
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sender_field": "email",
|
"sender_field": "email",
|
||||||
"sender_name_field": "first_name",
|
"sender_name_field": "first_name",
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
@ -339,4 +381,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "lead_name",
|
"title_field": "lead_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
0
crm/fcrm/doctype/crm_product/__init__.py
Normal file
0
crm/fcrm/doctype/crm_product/__init__.py
Normal file
9
crm/fcrm/doctype/crm_product/crm_product.js
Normal file
9
crm/fcrm/doctype/crm_product/crm_product.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on("CRM Product", {
|
||||||
|
product_code: function (frm) {
|
||||||
|
if (!frm.doc.product_name)
|
||||||
|
frm.set_value("product_name", frm.doc.product_code);
|
||||||
|
}
|
||||||
|
});
|
||||||
105
crm/fcrm/doctype/crm_product/crm_product.json
Normal file
105
crm/fcrm/doctype/crm_product/crm_product.json
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_import": 1,
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:product_code",
|
||||||
|
"creation": "2025-04-28 11:45:09.309636",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
|
"product_code",
|
||||||
|
"product_name",
|
||||||
|
"column_break_bpdj",
|
||||||
|
"disabled",
|
||||||
|
"standard_rate",
|
||||||
|
"image",
|
||||||
|
"section_break_rtwm",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Naming Series",
|
||||||
|
"options": "CRM-PROD-.YYYY.-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "product_code",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Product Code",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "product_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Product Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bpdj",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "disabled",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Disabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_rtwm",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "standard_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Standard Selling Rate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"image_field": "image",
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"make_attachments_public": 1,
|
||||||
|
"modified": "2025-04-28 12:47:25.087957",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "CRM Product",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"search_fields": "product_name,description",
|
||||||
|
"show_name_in_global_search": 1,
|
||||||
|
"show_preview_popup": 1,
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "product_name",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
16
crm/fcrm/doctype/crm_product/crm_product.py
Normal file
16
crm/fcrm/doctype/crm_product/crm_product.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMProduct(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.set_product_name()
|
||||||
|
|
||||||
|
def set_product_name(self):
|
||||||
|
if not self.product_name:
|
||||||
|
self.product_name = self.product_code
|
||||||
|
else:
|
||||||
|
self.product_name = self.product_name.strip()
|
||||||
29
crm/fcrm/doctype/crm_product/test_crm_product.py
Normal file
29
crm/fcrm/doctype/crm_product/test_crm_product.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestCRMProduct(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for CRMProduct.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestCRMProduct(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for CRMProduct.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
0
crm/fcrm/doctype/crm_products/__init__.py
Normal file
0
crm/fcrm/doctype/crm_products/__init__.py
Normal file
136
crm/fcrm/doctype/crm_products/crm_products.json
Normal file
136
crm/fcrm/doctype/crm_products/crm_products.json
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-04-28 12:50:49.812915",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"product_code",
|
||||||
|
"column_break_gvbc",
|
||||||
|
"product_name",
|
||||||
|
"section_break_fnvf",
|
||||||
|
"qty",
|
||||||
|
"column_break_ajac",
|
||||||
|
"rate",
|
||||||
|
"section_break_olqb",
|
||||||
|
"discount_percentage",
|
||||||
|
"column_break_uvra",
|
||||||
|
"discount_amount",
|
||||||
|
"section_break_cnpb",
|
||||||
|
"column_break_pozr",
|
||||||
|
"amount",
|
||||||
|
"column_break_ejqw",
|
||||||
|
"net_amount"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_gvbc",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "product_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Product Name",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_fnvf",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_olqb",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"fieldname": "discount_percentage",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"label": "Discount %"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "discount_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Discount Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_cnpb",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_pozr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"fieldname": "rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Rate",
|
||||||
|
"options": "currency",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"depends_on": "discount_percentage",
|
||||||
|
"description": "Amount after discount",
|
||||||
|
"fieldname": "net_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"columns": 5,
|
||||||
|
"fieldname": "product_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Product",
|
||||||
|
"options": "CRM Product"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Quantity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ajac",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_uvra",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ejqw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-05-14 18:52:26.183306",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "CRM Products",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
110
crm/fcrm/doctype/crm_products/crm_products.py
Normal file
110
crm/fcrm/doctype/crm_products/crm_products.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMProducts(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def create_product_details_script(doctype):
|
||||||
|
if not frappe.db.exists("CRM Form Script", "Product Details Script for " + doctype):
|
||||||
|
script = get_product_details_script(doctype)
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "CRM Form Script",
|
||||||
|
"name": "Product Details Script for " + doctype,
|
||||||
|
"dt": doctype,
|
||||||
|
"view": "Form",
|
||||||
|
"script": script,
|
||||||
|
"enabled": 1,
|
||||||
|
"is_standard": 1,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_details_script(doctype):
|
||||||
|
doctype_class = "class " + doctype.replace(" ", "")
|
||||||
|
|
||||||
|
return (
|
||||||
|
doctype_class
|
||||||
|
+ " {"
|
||||||
|
+ """
|
||||||
|
update_total() {
|
||||||
|
let total = 0
|
||||||
|
let total_qty = 0
|
||||||
|
let net_total = 0
|
||||||
|
let discount_applied = false
|
||||||
|
|
||||||
|
this.doc.products.forEach((d) => {
|
||||||
|
total += d.amount
|
||||||
|
net_total += d.net_amount
|
||||||
|
if (d.discount_percentage > 0) {
|
||||||
|
discount_applied = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.doc.total = total
|
||||||
|
this.doc.net_total = net_total || total
|
||||||
|
|
||||||
|
if (!net_total && discount_applied) {
|
||||||
|
this.doc.net_total = net_total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CRMProducts {
|
||||||
|
products_add() {
|
||||||
|
let row = this.doc.getRow('products')
|
||||||
|
row.trigger('qty')
|
||||||
|
this.doc.trigger('update_total')
|
||||||
|
}
|
||||||
|
|
||||||
|
products_remove() {
|
||||||
|
this.doc.trigger('update_total')
|
||||||
|
}
|
||||||
|
|
||||||
|
async product_code(idx) {
|
||||||
|
let row = this.doc.getRow('products', idx)
|
||||||
|
|
||||||
|
let a = await call("frappe.client.get_value", {
|
||||||
|
doctype: "CRM Product",
|
||||||
|
filters: { name: row.product_code },
|
||||||
|
fieldname: ["product_name", "standard_rate"],
|
||||||
|
})
|
||||||
|
|
||||||
|
row.product_name = a.product_name
|
||||||
|
if (a.standard_rate && !row.rate) {
|
||||||
|
row.rate = a.standard_rate
|
||||||
|
row.trigger("rate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qty(idx) {
|
||||||
|
let row = this.doc.getRow('products', idx)
|
||||||
|
row.amount = row.qty * row.rate
|
||||||
|
row.trigger('discount_percentage', idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
rate() {
|
||||||
|
let row = this.doc.getRow('products')
|
||||||
|
row.amount = row.qty * row.rate
|
||||||
|
row.trigger('discount_percentage')
|
||||||
|
}
|
||||||
|
|
||||||
|
discount_percentage(idx) {
|
||||||
|
let row = this.doc.getRow('products', idx)
|
||||||
|
if (!row.discount_percentage) {
|
||||||
|
row.net_amount = row.amount
|
||||||
|
row.discount_amount = 0
|
||||||
|
}
|
||||||
|
if (row.discount_percentage && row.amount) {
|
||||||
|
row.discount_amount = (row.discount_percentage / 100) * row.amount
|
||||||
|
row.net_amount = row.amount - row.discount_amount
|
||||||
|
}
|
||||||
|
this.doc.trigger('update_total')
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
)
|
||||||
@ -4,6 +4,8 @@ import click
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
from crm.fcrm.doctype.crm_products.crm_products import create_product_details_script
|
||||||
|
|
||||||
|
|
||||||
def before_install():
|
def before_install():
|
||||||
pass
|
pass
|
||||||
@ -19,6 +21,7 @@ def after_install(force=False):
|
|||||||
add_default_industries()
|
add_default_industries()
|
||||||
add_default_lead_sources()
|
add_default_lead_sources()
|
||||||
add_standard_dropdown_items()
|
add_standard_dropdown_items()
|
||||||
|
add_default_scripts()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@ -353,3 +356,8 @@ def add_standard_dropdown_items():
|
|||||||
crm_settings.append("dropdown_items", item)
|
crm_settings.append("dropdown_items", item)
|
||||||
|
|
||||||
crm_settings.save()
|
crm_settings.save()
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_scripts():
|
||||||
|
for doctype in ["CRM Lead", "CRM Deal"]:
|
||||||
|
create_product_details_script(doctype)
|
||||||
|
|||||||
@ -11,4 +11,5 @@ crm.patches.v1_0.create_default_fields_layout #22/01/2025
|
|||||||
crm.patches.v1_0.create_default_sidebar_fields_layout
|
crm.patches.v1_0.create_default_sidebar_fields_layout
|
||||||
crm.patches.v1_0.update_deal_quick_entry_layout
|
crm.patches.v1_0.update_deal_quick_entry_layout
|
||||||
crm.patches.v1_0.update_layouts_to_new_format
|
crm.patches.v1_0.update_layouts_to_new_format
|
||||||
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
||||||
|
crm.patches.v1_0.create_default_scripts
|
||||||
5
crm/patches/v1_0/create_default_scripts.py
Normal file
5
crm/patches/v1_0/create_default_scripts.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from crm.install import add_default_scripts
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
add_default_scripts()
|
||||||
@ -1,12 +1,13 @@
|
|||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# MIT License. See license.txt
|
# MIT License. See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
import click
|
import click
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
def before_uninstall():
|
def before_uninstall():
|
||||||
delete_email_template_custom_fields()
|
delete_email_template_custom_fields()
|
||||||
|
|
||||||
|
|
||||||
def delete_email_template_custom_fields():
|
def delete_email_template_custom_fields():
|
||||||
if frappe.get_meta("Email Template").has_field("enabled"):
|
if frappe.get_meta("Email Template").has_field("enabled"):
|
||||||
click.secho("* Uninstalling Custom Fields from Email Template")
|
click.secho("* Uninstalling Custom Fields from Email Template")
|
||||||
@ -19,4 +20,4 @@ def delete_email_template_custom_fields():
|
|||||||
for fieldname in fieldnames:
|
for fieldname in fieldnames:
|
||||||
frappe.db.delete("Custom Field", {"name": "Email Template-" + fieldname})
|
frappe.db.delete("Custom Field", {"name": "Email Template-" + fieldname})
|
||||||
|
|
||||||
frappe.clear_cache(doctype="Email Template")
|
frappe.clear_cache(doctype="Email Template")
|
||||||
|
|||||||
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@ -53,6 +53,7 @@ declare module 'vue' {
|
|||||||
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
|
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
|
||||||
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
|
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
|
||||||
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
||||||
|
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||||
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
||||||
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
||||||
@ -113,9 +114,11 @@ declare module 'vue' {
|
|||||||
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
||||||
Filter: typeof import('./src/components/Filter.vue')['default']
|
Filter: typeof import('./src/components/Filter.vue')['default']
|
||||||
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
||||||
|
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
|
||||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
||||||
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
||||||
GeneralSettings: typeof import('./src/components/Settings/GeneralSettings.vue')['default']
|
GeneralSettings: typeof import('./src/components/Settings/GeneralSettings.vue')['default']
|
||||||
|
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
|
||||||
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
||||||
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
||||||
GridFieldsEditorModal: typeof import('./src/components/Controls/GridFieldsEditorModal.vue')['default']
|
GridFieldsEditorModal: typeof import('./src/components/Controls/GridFieldsEditorModal.vue')['default']
|
||||||
|
|||||||
58
frontend/src/components/Controls/FormattedInput.vue
Normal file
58
frontend/src/components/Controls/FormattedInput.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<TextInput
|
||||||
|
ref="inputRef"
|
||||||
|
:value="displayValue"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="isFocused = false"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
<slot name="description">
|
||||||
|
<p v-if="attrs.description" class="mt-1.5" :class="descriptionClasses">
|
||||||
|
{{ attrs.description }}
|
||||||
|
</p>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { TextInput } from 'frappe-ui'
|
||||||
|
import { ref, computed, nextTick, useAttrs } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
formattedValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const isFocused = ref(false)
|
||||||
|
const inputRef = ref(null)
|
||||||
|
|
||||||
|
function handleFocus() {
|
||||||
|
isFocused.value = true
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (inputRef.value) {
|
||||||
|
inputRef.value.el?.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayValue = computed(() => {
|
||||||
|
return isFocused.value ? props.value : props.formattedValue || props.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const descriptionClasses = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
sm: 'text-xs',
|
||||||
|
md: 'text-base',
|
||||||
|
}[attrs.size || 'sm'],
|
||||||
|
'text-ink-gray-5',
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -33,10 +33,23 @@
|
|||||||
<div
|
<div
|
||||||
v-for="field in fields"
|
v-for="field in fields"
|
||||||
class="border-r border-outline-gray-2 p-2 truncate"
|
class="border-r border-outline-gray-2 p-2 truncate"
|
||||||
|
:class="
|
||||||
|
['Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)
|
||||||
|
? 'text-right'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
:key="field.fieldname"
|
:key="field.fieldname"
|
||||||
:title="field.label"
|
:title="field.label"
|
||||||
>
|
>
|
||||||
{{ __(field.label) }}
|
{{ __(field.label) }}
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
field.reqd ||
|
||||||
|
(field.mandatory_depends_on && field.mandatory_via_depends_on)
|
||||||
|
"
|
||||||
|
class="text-ink-red-2"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-12">
|
<div class="w-12">
|
||||||
@ -95,7 +108,13 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
v-if="
|
v-if="
|
||||||
field.read_only &&
|
field.read_only &&
|
||||||
!['Float', 'Currency', 'Check'].includes(field.fieldtype)
|
![
|
||||||
|
'Int',
|
||||||
|
'Float',
|
||||||
|
'Currency',
|
||||||
|
'Percent',
|
||||||
|
'Check',
|
||||||
|
].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
@ -115,6 +134,9 @@
|
|||||||
"
|
"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="(v) => fieldChange(v, field, row)"
|
@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'"
|
||||||
@ -194,34 +216,44 @@
|
|||||||
:options="field.options"
|
:options="field.options"
|
||||||
@change="(e) => fieldChange(e.target.value, field, row)"
|
@change="(e) => fieldChange(e.target.value, field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="['Int'].includes(field.fieldtype)"
|
v-else-if="field.fieldtype === 'Int'"
|
||||||
type="number"
|
class="[&_input]:text-right"
|
||||||
|
type="text"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:value="row[field.fieldname]"
|
:value="row[field.fieldname] || '0'"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="fieldChange($event.target.value, field, row)"
|
@change="fieldChange($event.target.value, field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Percent'"
|
v-else-if="field.fieldtype === 'Percent'"
|
||||||
|
class="[&_input]:text-right"
|
||||||
type="text"
|
type="text"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:value="getFormattedPercent(field.fieldname, row)"
|
:value="getFloatWithPrecision(field.fieldname, row)"
|
||||||
|
:formattedValue="(row[field.fieldname] || '0') + '%'"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="fieldChange(flt($event.target.value), field, row)"
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Float'"
|
v-else-if="field.fieldtype === 'Float'"
|
||||||
|
class="[&_input]:text-right"
|
||||||
type="text"
|
type="text"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:value="getFormattedFloat(field.fieldname, row)"
|
:value="getFloatWithPrecision(field.fieldname, row)"
|
||||||
|
:formattedValue="row[field.fieldname]"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="fieldChange(flt($event.target.value), field, row)"
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormattedInput
|
||||||
v-else-if="field.fieldtype === 'Currency'"
|
v-else-if="field.fieldtype === 'Currency'"
|
||||||
|
class="[&_input]:text-right"
|
||||||
type="text"
|
type="text"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:value="getFormattedCurrency(field.fieldname, row)"
|
:value="getCurrencyWithPrecision(field.fieldname, row)"
|
||||||
|
:formattedValue="
|
||||||
|
getFormattedCurrency(field.fieldname, row, parentDoc)
|
||||||
|
"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="fieldChange(flt($event.target.value), field, row)"
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
/>
|
/>
|
||||||
@ -293,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'
|
||||||
@ -303,6 +336,7 @@ import { getRandom, getFormat, isTouchScreenDevice } from '@/utils'
|
|||||||
import { flt } from '@/utils/numberFormat.js'
|
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,
|
||||||
@ -313,7 +347,7 @@ import {
|
|||||||
dayjs,
|
dayjs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import { ref, reactive, computed, inject } from 'vue'
|
import { ref, reactive, computed, inject, provide } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
@ -341,8 +375,8 @@ const triggerOnRowRemove = inject('triggerOnRowRemove')
|
|||||||
const {
|
const {
|
||||||
getGridViewSettings,
|
getGridViewSettings,
|
||||||
getFields,
|
getFields,
|
||||||
getFormattedPercent,
|
getFloatWithPrecision,
|
||||||
getFormattedFloat,
|
getCurrencyWithPrecision,
|
||||||
getFormattedCurrency,
|
getFormattedCurrency,
|
||||||
getGridSettings,
|
getGridSettings,
|
||||||
} = getMeta(props.doctype)
|
} = getMeta(props.doctype)
|
||||||
@ -350,6 +384,10 @@ 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())
|
||||||
|
|
||||||
@ -377,6 +415,17 @@ const allFields = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
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),
|
||||||
|
|||||||
@ -7,23 +7,27 @@
|
|||||||
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="
|
v-if="
|
||||||
field.read_only &&
|
field.read_only &&
|
||||||
!['Float', 'Currency', 'Check'].includes(field.fieldtype)
|
!['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"
|
:parentFieldname="field.fieldname"
|
||||||
@ -37,6 +41,7 @@
|
|||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
@change="(e) => fieldChange(e.target.value, field)"
|
@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" />
|
||||||
@ -49,6 +54,7 @@
|
|||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
@change="(e) => fieldChange(e.target.checked, field)"
|
@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"
|
||||||
@ -150,39 +156,45 @@
|
|||||||
['Small Text', 'Text', 'Long Text', 'Code'].includes(field.fieldtype)
|
['Small Text', 'Text', 'Long Text', 'Code'].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:placeholder="getPlaceholder(field)"
|
|
||||||
:value="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
|
:placeholder="getPlaceholder(field)"
|
||||||
|
:description="field.description"
|
||||||
@change="fieldChange($event.target.value, field)"
|
@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)"
|
||||||
:value="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
:description="field.description"
|
||||||
@change="fieldChange($event.target.value, field)"
|
@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)"
|
||||||
|
:description="field.description"
|
||||||
@change="fieldChange(flt($event.target.value), field)"
|
@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)"
|
||||||
|
:description="field.description"
|
||||||
@change="fieldChange(flt($event.target.value), field)"
|
@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)"
|
||||||
|
:description="field.description"
|
||||||
@change="fieldChange(flt($event.target.value), field)"
|
@change="fieldChange(flt($event.target.value), field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
@ -191,17 +203,20 @@
|
|||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:value="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)"
|
@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'
|
||||||
@ -225,6 +240,7 @@ const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
|||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
let triggerOnChange
|
let triggerOnChange
|
||||||
|
let parentDoc
|
||||||
|
|
||||||
if (!isGridRow) {
|
if (!isGridRow) {
|
||||||
const {
|
const {
|
||||||
@ -239,6 +255,7 @@ if (!isGridRow) {
|
|||||||
provide('triggerOnRowRemove', triggerOnRowRemove)
|
provide('triggerOnRowRemove', triggerOnRowRemove)
|
||||||
} else {
|
} else {
|
||||||
triggerOnChange = inject('triggerOnChange')
|
triggerOnChange = inject('triggerOnChange')
|
||||||
|
parentDoc = inject('parentDoc')
|
||||||
}
|
}
|
||||||
|
|
||||||
const field = computed(() => {
|
const field = computed(() => {
|
||||||
@ -257,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),
|
||||||
|
|||||||
@ -7,9 +7,11 @@
|
|||||||
<AppHeader />
|
<AppHeader />
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
<GlobalModals />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppSidebar from '@/components/Layouts/AppSidebar.vue'
|
import AppSidebar from '@/components/Layouts/AppSidebar.vue'
|
||||||
import AppHeader from '@/components/Layouts/AppHeader.vue'
|
import AppHeader from '@/components/Layouts/AppHeader.vue'
|
||||||
|
import GlobalModals from '@/components/Modals/GlobalModals.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
147
frontend/src/components/Modals/CreateDocumentModal.vue
Normal file
147
frontend/src/components/Modals/CreateDocumentModal.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model="show" :options="dialogOptions">
|
||||||
|
<template #body>
|
||||||
|
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
|
||||||
|
<div class="mb-5 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||||
|
{{ __(dialogOptions.title) || __('Untitled') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
v-if="isManager() && !isMobileView"
|
||||||
|
variant="ghost"
|
||||||
|
class="w-7"
|
||||||
|
@click="openQuickEntryModal"
|
||||||
|
>
|
||||||
|
<EditIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" class="w-7" @click="show = false">
|
||||||
|
<FeatherIcon name="x" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="tabs.data">
|
||||||
|
<FieldLayout :tabs="tabs.data" :data="_data" :doctype="doctype" />
|
||||||
|
<ErrorMessage class="mt-2" :message="error" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 pb-7 pt-4 sm:px-6">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Button
|
||||||
|
class="w-full"
|
||||||
|
v-for="action in dialogOptions.actions"
|
||||||
|
:key="action.label"
|
||||||
|
v-bind="action"
|
||||||
|
:label="__(action.label)"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
import { FeatherIcon, createResource, ErrorMessage, call } from 'frappe-ui'
|
||||||
|
import { ref, nextTick, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['showQuickEntryModal', 'callback'])
|
||||||
|
|
||||||
|
const { isManager } = usersStore()
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
let _data = ref({})
|
||||||
|
|
||||||
|
const dialogOptions = computed(() => {
|
||||||
|
let doctype = props.doctype
|
||||||
|
|
||||||
|
if (doctype.startsWith('CRM ') || doctype.startsWith('FCRM ')) {
|
||||||
|
doctype = doctype.replace(/^(CRM |FCRM )/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = __('New {0}', [doctype])
|
||||||
|
let size = 'xl'
|
||||||
|
let actions = [
|
||||||
|
{
|
||||||
|
label: __('Create'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: () => create(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return { title, size, actions }
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['QuickEntry', props.doctype],
|
||||||
|
params: { doctype: props.doctype, type: 'Quick Entry' },
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function create() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
let doc = await call(
|
||||||
|
'frappe.client.insert',
|
||||||
|
{
|
||||||
|
doc: {
|
||||||
|
doctype: props.doctype,
|
||||||
|
..._data.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
loading.value = false
|
||||||
|
if (err.error) {
|
||||||
|
error.value = err.error.messages?.[0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
show.value = false
|
||||||
|
emit('callback', doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => show.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
_data.value = { ...props.data }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function openQuickEntryModal() {
|
||||||
|
emit('showQuickEntryModal', props.doctype)
|
||||||
|
nextTick(() => {
|
||||||
|
show.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
34
frontend/src/components/Modals/GlobalModals.vue
Normal file
34
frontend/src/components/Modals/GlobalModals.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<CreateDocumentModal
|
||||||
|
v-if="showCreateDocumentModal"
|
||||||
|
v-model="showCreateDocumentModal"
|
||||||
|
:doctype="createDocumentDoctype"
|
||||||
|
:data="createDocumentData"
|
||||||
|
@showQuickEntryModal="(dt) => openQuickEntryModal(dt)"
|
||||||
|
@callback="(data) => createDocumentCallback(data)"
|
||||||
|
/>
|
||||||
|
<QuickEntryModal
|
||||||
|
v-if="showQuickEntryModal"
|
||||||
|
v-model="showQuickEntryModal"
|
||||||
|
:doctype="quickEntryDoctype"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import CreateDocumentModal from '@/components/Modals/CreateDocumentModal.vue'
|
||||||
|
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||||
|
import {
|
||||||
|
showCreateDocumentModal,
|
||||||
|
createDocumentDoctype,
|
||||||
|
createDocumentData,
|
||||||
|
createDocumentCallback,
|
||||||
|
} from '@/composables/document'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const showQuickEntryModal = ref(false)
|
||||||
|
const quickEntryDoctype = ref('')
|
||||||
|
|
||||||
|
function openQuickEntryModal(dt) {
|
||||||
|
showQuickEntryModal.value = true
|
||||||
|
quickEntryDoctype.value = dt
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -53,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>
|
||||||
@ -65,7 +65,14 @@
|
|||||||
<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"
|
||||||
>
|
>
|
||||||
@ -254,7 +261,7 @@
|
|||||||
@change="(v) => fieldChange(v, field)"
|
@change="(v) => fieldChange(v, field)"
|
||||||
/>
|
/>
|
||||||
</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"
|
||||||
@ -266,17 +273,19 @@
|
|||||||
@change.stop="
|
@change.stop="
|
||||||
fieldChange(flt($event.target.value), field)
|
fieldChange(flt($event.target.value), field)
|
||||||
"
|
"
|
||||||
|
: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="document.doc[field.fieldname]"
|
v-model="document.doc[field.fieldname]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="fieldChange($event.target.value, field)"
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
|
: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"
|
||||||
@ -288,8 +297,9 @@
|
|||||||
@change.stop="
|
@change.stop="
|
||||||
fieldChange(flt($event.target.value), field)
|
fieldChange(flt($event.target.value), field)
|
||||||
"
|
"
|
||||||
|
: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"
|
||||||
@ -301,6 +311,7 @@
|
|||||||
@change.stop="
|
@change.stop="
|
||||||
fieldChange(flt($event.target.value), field)
|
fieldChange(flt($event.target.value), field)
|
||||||
"
|
"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
@ -355,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'
|
||||||
|
|||||||
16
frontend/src/composables/document.js
Normal file
16
frontend/src/composables/document.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const showCreateDocumentModal = ref(false)
|
||||||
|
export const createDocumentDoctype = ref('')
|
||||||
|
export const createDocumentData = ref({})
|
||||||
|
export const createDocumentCallback = ref(null)
|
||||||
|
|
||||||
|
export function createDocument(doctype, obj, close, callback) {
|
||||||
|
if (doctype) {
|
||||||
|
close?.()
|
||||||
|
createDocumentDoctype.value = doctype
|
||||||
|
createDocumentData.value = obj || {}
|
||||||
|
createDocumentCallback.value = callback || null
|
||||||
|
showCreateDocumentModal.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,14 +36,20 @@ export function useDocument(doctype, docname) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupFormScript() {
|
function setupFormScript() {
|
||||||
if (controllersCache[doctype]) return
|
if (controllersCache[doctype]?.[docname]) return
|
||||||
|
|
||||||
controllersCache[doctype] = setupScript(documentsCache[doctype][docname])
|
if (!controllersCache[doctype]) {
|
||||||
|
controllersCache[doctype] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
controllersCache[doctype][docname] = setupScript(
|
||||||
|
documentsCache[doctype][docname],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getControllers(row = null) {
|
function getControllers(row = null) {
|
||||||
const _doctype = row?.doctype || doctype
|
const _doctype = row?.doctype || doctype
|
||||||
return (controllersCache[doctype] || []).filter(
|
return (controllersCache[doctype]?.[docname] || []).filter(
|
||||||
(c) => c.constructor.name === _doctype.replace(/\s+/g, ''),
|
(c) => c.constructor.name === _doctype.replace(/\s+/g, ''),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,8 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
) {
|
) {
|
||||||
let instance = new FormClass()
|
let instance = new FormClass()
|
||||||
|
|
||||||
|
instance._isChildDoctype = isChildDoctype
|
||||||
|
|
||||||
for (const key in document) {
|
for (const key in document) {
|
||||||
if (document.hasOwnProperty(key)) {
|
if (document.hasOwnProperty(key)) {
|
||||||
instance[key] = document[key]
|
instance[key] = document[key]
|
||||||
@ -125,10 +127,16 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
return meta[doctype]
|
return meta[doctype]
|
||||||
}
|
}
|
||||||
|
|
||||||
setupHelperMethods(FormClass, instance, parentInstance, document)
|
setupHelperMethods(FormClass, document)
|
||||||
|
|
||||||
if (isChildDoctype) {
|
if (isChildDoctype) {
|
||||||
instance.doc = createDocProxy(document.doc, parentInstance)
|
instance.doc = createDocProxy(document.doc, parentInstance, instance)
|
||||||
|
|
||||||
|
if (!parentInstance._childInstances) {
|
||||||
|
parentInstance._childInstances = []
|
||||||
|
}
|
||||||
|
|
||||||
|
parentInstance._childInstances.push(instance)
|
||||||
} else {
|
} else {
|
||||||
instance.doc = createDocProxy(document.doc, instance)
|
instance.doc = createDocProxy(document.doc, instance)
|
||||||
}
|
}
|
||||||
@ -136,36 +144,55 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupHelperMethods(FormClass, instance, parentInstance, document) {
|
function setupHelperMethods(FormClass, document) {
|
||||||
if (typeof FormClass.prototype.getRow !== 'function') {
|
if (typeof FormClass.prototype.getRow !== 'function') {
|
||||||
FormClass.prototype.getRow = (parentField, idx) =>
|
FormClass.prototype.getRow = function (parentField, idx) {
|
||||||
getRow(parentField, idx, document.doc, instance)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
exposeHiddenMethods(instance, parentInstance, ['getRow'])
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRow(parentField, idx, data, instance) {
|
|
||||||
idx = idx || instance.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
|
|
||||||
|
|
||||||
return createDocProxy(row, instance)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// utility function to setup a form controller
|
// utility function to setup a form controller
|
||||||
@ -197,7 +224,7 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
return FormClass
|
return FormClass
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDocProxy(data, instance) {
|
function createDocProxy(data, instance, childInstance = null) {
|
||||||
return new Proxy(data, {
|
return new Proxy(data, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
if (prop === 'trigger') {
|
if (prop === 'trigger') {
|
||||||
@ -221,6 +248,12 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prop === 'getRow') {
|
||||||
|
return instance.getRow.bind(
|
||||||
|
childInstance || instance._childInstances || instance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return target[prop]
|
return target[prop]
|
||||||
},
|
},
|
||||||
set(target, prop, value) {
|
set(target, prop, value) {
|
||||||
@ -230,25 +263,6 @@ export function getScript(doctype, view = 'Form') {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function exposeHiddenMethods(instance, parentInstance, methodNames = []) {
|
|
||||||
for (const name of methodNames) {
|
|
||||||
// remove the method from parent instance if it exists
|
|
||||||
if (parentInstance && parentInstance[name]) {
|
|
||||||
delete instance.doc[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof instance[name] === 'function' && !instance.doc[name]) {
|
|
||||||
// Show as actual method on doc, bound to instance
|
|
||||||
Object.defineProperty(instance.doc, name, {
|
|
||||||
value: (...args) => instance[name](...args),
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scripts,
|
scripts,
|
||||||
setupScript,
|
setupScript,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { get } from '@vueuse/core'
|
|
||||||
|
|
||||||
const NUMBER_FORMAT_INFO = {
|
const NUMBER_FORMAT_INFO = {
|
||||||
'#,###.##': { decimalStr: '.', groupSep: ',' },
|
'#,###.##': { decimalStr: '.', groupSep: ',' },
|
||||||
'#.###,##': { decimalStr: ',', groupSep: '.' },
|
'#.###,##': { decimalStr: ',', groupSep: '.' },
|
||||||
@ -183,10 +181,13 @@ export function formatCurrency(value, format, currency = 'USD', precision = 2) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
format = getNumberFormat(format)
|
format = getNumberFormat(format)
|
||||||
let symbol = getCurrencySymbol(currency)
|
|
||||||
|
|
||||||
if (symbol) {
|
if (currency) {
|
||||||
return __(symbol) + ' ' + formatNumber(value, format, precision)
|
let symbol = getCurrencySymbol(currency)
|
||||||
|
|
||||||
|
if (symbol) {
|
||||||
|
return __(symbol) + ' ' + formatNumber(value, format, precision)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatNumber(value, format, precision)
|
return formatNumber(value, format, precision)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user