Merge pull request #346 from nabinhait/erpnext-integration
feat: Create Quotation, Contact and Customer in ERPNext from Deal
This commit is contained in:
commit
2e49844ea0
0
crm/fcrm/doctype/erpnext_crm_settings/__init__.py
Normal file
0
crm/fcrm/doctype/erpnext_crm_settings/__init__.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2024-07-02 15:23:17.022214",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"enabled",
|
||||||
|
"is_erpnext_in_the_current_site",
|
||||||
|
"column_break_vfru",
|
||||||
|
"erpnext_company",
|
||||||
|
"section_break_oubd",
|
||||||
|
"erpnext_site_url",
|
||||||
|
"column_break_fllx",
|
||||||
|
"api_key",
|
||||||
|
"api_secret"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.enabled && !doc.is_erpnext_in_the_current_site",
|
||||||
|
"fieldname": "api_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "API Key",
|
||||||
|
"mandatory_depends_on": "eval:!doc.is_erpnext_in_the_current_site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.enabled && !doc.is_erpnext_in_the_current_site",
|
||||||
|
"fieldname": "api_secret",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "API Secret",
|
||||||
|
"mandatory_depends_on": "eval:!doc.is_erpnext_in_the_current_site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"fieldname": "section_break_oubd",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_fllx",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.enabled && !doc.is_erpnext_in_the_current_site",
|
||||||
|
"fieldname": "erpnext_site_url",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "ERPNext Site URL",
|
||||||
|
"mandatory_depends_on": "eval:!doc.is_erpnext_in_the_current_site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"fieldname": "erpnext_company",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Company in ERPNext Site",
|
||||||
|
"mandatory_depends_on": "enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vfru",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"fieldname": "is_erpnext_in_the_current_site",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is ERPNext in the current site?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enabled",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"issingle": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2024-09-13 15:06:23.317262",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "ERPNext CRM Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
194
crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py
Normal file
194
crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# Copyright (c) 2024, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.frappeclient import FrappeClient
|
||||||
|
from frappe.utils import get_url_to_form
|
||||||
|
import json
|
||||||
|
|
||||||
|
class ERPNextCRMSettings(Document):
|
||||||
|
def validate(self):
|
||||||
|
if self.enabled:
|
||||||
|
self.validate_if_erpnext_installed()
|
||||||
|
self.add_quotation_to_option()
|
||||||
|
self.create_custom_fields()
|
||||||
|
self.create_crm_form_script()
|
||||||
|
|
||||||
|
def validate_if_erpnext_installed(self):
|
||||||
|
if self.is_erpnext_in_the_current_site:
|
||||||
|
if "erpnext" not in frappe.get_installed_apps():
|
||||||
|
frappe.throw(_("ERPNext is not installed in the current site"))
|
||||||
|
|
||||||
|
def add_quotation_to_option(self):
|
||||||
|
if self.is_erpnext_in_the_current_site:
|
||||||
|
if not frappe.db.exists("Property Setter", {"name": "Quotation-quotation_to-link_filters"}):
|
||||||
|
make_property_setter(
|
||||||
|
doctype="Quotation",
|
||||||
|
fieldname="quotation_to",
|
||||||
|
property="link_filters",
|
||||||
|
value='[["DocType","name","in", ["Customer", "Lead", "Prospect", "Frappe CRM Deal"]]]',
|
||||||
|
property_type="JSON",
|
||||||
|
validate_fields_for_doctype=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_custom_fields(self):
|
||||||
|
if self.is_erpnext_in_the_current_site:
|
||||||
|
from erpnext.crm.frappe_crm_api import create_custom_fields_for_frappe_crm
|
||||||
|
create_custom_fields_for_frappe_crm()
|
||||||
|
else:
|
||||||
|
self.create_custom_fields_in_remote_site()
|
||||||
|
|
||||||
|
def create_custom_fields_in_remote_site(self):
|
||||||
|
client = get_erpnext_site_client(self)
|
||||||
|
try:
|
||||||
|
client.post_api("erpnext.crm.frappe_crm_api.create_custom_fields_for_frappe_crm")
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(
|
||||||
|
frappe.get_traceback(),
|
||||||
|
f"Error while creating custom field in the remote erpnext site: {self.erpnext_site_url}"
|
||||||
|
)
|
||||||
|
frappe.throw("Error while creating custom field in ERPNext, check error log for more details")
|
||||||
|
|
||||||
|
def create_crm_form_script(self):
|
||||||
|
if not frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"):
|
||||||
|
script = get_crm_form_script()
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "CRM Form Script",
|
||||||
|
"name": "Create Quotation from CRM Deal",
|
||||||
|
"dt": "CRM Deal",
|
||||||
|
"view": "Form",
|
||||||
|
"script": script,
|
||||||
|
"enabled": 1,
|
||||||
|
"is_standard": 1
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
def get_erpnext_site_client(erpnext_crm_settings):
|
||||||
|
site_url = erpnext_crm_settings.erpnext_site_url
|
||||||
|
api_key = erpnext_crm_settings.api_key
|
||||||
|
api_secret = erpnext_crm_settings.api_secret
|
||||||
|
|
||||||
|
return FrappeClient(
|
||||||
|
site_url, api_key=api_key, api_secret=api_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_quotation_url(crm_deal, organization):
|
||||||
|
erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings")
|
||||||
|
if not erpnext_crm_settings.enabled:
|
||||||
|
frappe.throw(_("ERPNext is not integrated with the CRM"))
|
||||||
|
|
||||||
|
if erpnext_crm_settings.is_erpnext_in_the_current_site:
|
||||||
|
quotation_url = get_url_to_form("Quotation")
|
||||||
|
return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}"
|
||||||
|
else:
|
||||||
|
site_url = erpnext_crm_settings.get("erpnext_site_url")
|
||||||
|
quotation_url = f"{site_url}/app/quotation"
|
||||||
|
|
||||||
|
prospect = create_prospect_in_remote_site(crm_deal, erpnext_crm_settings)
|
||||||
|
return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}"
|
||||||
|
|
||||||
|
def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings):
|
||||||
|
try:
|
||||||
|
client = get_erpnext_site_client(erpnext_crm_settings)
|
||||||
|
doc = frappe.get_doc("CRM Deal", crm_deal)
|
||||||
|
contacts = get_contacts(doc)
|
||||||
|
return client.post_api("erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal",
|
||||||
|
{
|
||||||
|
"organization": doc.organization,
|
||||||
|
"lead_name": doc.lead_name,
|
||||||
|
"no_of_employees": doc.no_of_employees,
|
||||||
|
"deal_owner": doc.deal_owner,
|
||||||
|
"crm_deal": doc.name,
|
||||||
|
"territory": doc.territory,
|
||||||
|
"industry": doc.industry,
|
||||||
|
"website": doc.website,
|
||||||
|
"annual_revenue": doc.annual_revenue,
|
||||||
|
"contacts": json.dumps(contacts),
|
||||||
|
"erpnext_company": erpnext_crm_settings.erpnext_company
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(
|
||||||
|
frappe.get_traceback(),
|
||||||
|
f"Error while creating prospect in remote site: {erpnext_crm_settings.erpnext_site_url}"
|
||||||
|
)
|
||||||
|
frappe.throw(_("Error while creating prospect in ERPNext, check error log for more details"))
|
||||||
|
|
||||||
|
def get_contacts(doc):
|
||||||
|
contacts = []
|
||||||
|
for c in doc.contacts:
|
||||||
|
contacts.append({
|
||||||
|
"contact": c.contact,
|
||||||
|
"full_name": c.full_name,
|
||||||
|
"email": c.email,
|
||||||
|
"mobile_no": c.mobile_no,
|
||||||
|
"gender": c.gender,
|
||||||
|
"is_primary": c.is_primary,
|
||||||
|
})
|
||||||
|
return contacts
|
||||||
|
|
||||||
|
def create_customer_in_erpnext(doc, method):
|
||||||
|
erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings")
|
||||||
|
if not erpnext_crm_settings.enabled or doc.status != "Won":
|
||||||
|
return
|
||||||
|
|
||||||
|
contacts = get_contacts(doc)
|
||||||
|
customer = {
|
||||||
|
"customer_name": doc.organization,
|
||||||
|
"customer_group": "All Customer Groups",
|
||||||
|
"customer_type": "Company",
|
||||||
|
"territory": doc.territory,
|
||||||
|
"default_currency": doc.currency,
|
||||||
|
"industry": doc.industry,
|
||||||
|
"website": doc.website,
|
||||||
|
"crm_deal": doc.name,
|
||||||
|
"contacts": json.dumps(contacts),
|
||||||
|
}
|
||||||
|
if erpnext_crm_settings.is_erpnext_in_the_current_site:
|
||||||
|
from erpnext.crm.frappe_crm_api import create_customer
|
||||||
|
create_customer(customer)
|
||||||
|
else:
|
||||||
|
create_customer_in_remote_site(customer, erpnext_crm_settings)
|
||||||
|
|
||||||
|
def create_customer_in_remote_site(customer, erpnext_crm_settings):
|
||||||
|
client = get_erpnext_site_client(erpnext_crm_settings)
|
||||||
|
try:
|
||||||
|
client.post_api("erpnext.crm.frappe_crm_api.create_customer", customer)
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(
|
||||||
|
frappe.get_traceback(),
|
||||||
|
"Error while creating customer in remote site"
|
||||||
|
)
|
||||||
|
frappe.throw(_("Error while creating customer in ERPNext, check error log for more details"))
|
||||||
|
|
||||||
|
def get_crm_form_script():
|
||||||
|
return """
|
||||||
|
function setupForm({ doc, call, $dialog, updateField, createToast }) {
|
||||||
|
let actions = [];
|
||||||
|
if (!["Lost", "Won"].includes(doc?.status)) {
|
||||||
|
actions.push({
|
||||||
|
label: __("Create Quotation"),
|
||||||
|
onClick: async () => {
|
||||||
|
let quotation_url = await call(
|
||||||
|
"crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.get_quotation_url",
|
||||||
|
{
|
||||||
|
crm_deal: doc.name,
|
||||||
|
organization: doc.organization
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (quotation_url) {
|
||||||
|
window.open(quotation_url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
actions: actions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"""
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestERPNextCRMSettings(FrappeTestCase):
|
||||||
|
pass
|
||||||
@ -152,6 +152,9 @@ doc_events = {
|
|||||||
"validate": ["crm.api.whatsapp.validate"],
|
"validate": ["crm.api.whatsapp.validate"],
|
||||||
"on_update": ["crm.api.whatsapp.on_update"],
|
"on_update": ["crm.api.whatsapp.on_update"],
|
||||||
},
|
},
|
||||||
|
"CRM Deal": {
|
||||||
|
"on_update": ["crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.create_customer_in_erpnext"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="section in sections"
|
v-for="section in sections"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="first:border-t-0 first:pt-0"
|
class="section first:border-t-0 first:pt-0"
|
||||||
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -22,6 +22,7 @@
|
|||||||
>
|
>
|
||||||
<div v-for="field in section.fields" :key="field.name">
|
<div v-for="field in section.fields" :key="field.name">
|
||||||
<div
|
<div
|
||||||
|
class="settings-field"
|
||||||
v-if="
|
v-if="
|
||||||
(field.type == 'Check' ||
|
(field.type == 'Check' ||
|
||||||
(field.read_only && data[field.name]) ||
|
(field.read_only && data[field.name]) ||
|
||||||
@ -231,4 +232,12 @@ const props = defineProps({
|
|||||||
:deep(.form-control.prefix select) {
|
:deep(.form-control.prefix select) {
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:has(.settings-field) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
20
frontend/src/components/Icons/ERPNextIcon.vue
Normal file
20
frontend/src/components/Icons/ERPNextIcon.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1 5C1 2.79086 2.79086 1 5 1H13C15.2091 1 17 2.79086 17 5V13C17 15.2091 15.2091 17 13 17H5C2.79086 17 1 15.2091 1 13V5Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M11.7819 6.27142H11.5136H8.02453H6.28001V4.84002H11.7819V6.27142ZM8.02451 9.62623V11.5944H11.8267V13.0258H6.27999V8.19484H8.02451H11.5135V9.62623H8.02451Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend/src/components/Settings/ERPNextSettings.vue
Normal file
6
frontend/src/components/Settings/ERPNextSettings.vue
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<SettingsPage doctype="ERPNext CRM Settings" :title="__('ERPNext Settings')" class="p-8" />
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import SettingsPage from '@/components/Settings/SettingsPage.vue'
|
||||||
|
</script>
|
||||||
@ -39,10 +39,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||||
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||||
|
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
|
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
|
||||||
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
||||||
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
||||||
|
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
|
||||||
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
import { isWhatsappInstalled } from '@/composables/settings'
|
import { isWhatsappInstalled } from '@/composables/settings'
|
||||||
@ -83,6 +85,11 @@ const tabs = computed(() => {
|
|||||||
component: markRaw(WhatsAppSettings),
|
component: markRaw(WhatsAppSettings),
|
||||||
condition: () => isWhatsappInstalled.value,
|
condition: () => isWhatsappInstalled.value,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: __('ERPNext'),
|
||||||
|
icon: ERPNextIcon,
|
||||||
|
component: markRaw(ERPNextSettings),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-8">
|
<div class="flex h-full flex-col gap-8">
|
||||||
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
<div>{{ __(doctype) }}</div>
|
<div>{{ title || __(doctype) }}</div>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="data.isDirty"
|
v-if="data.isDirty"
|
||||||
:label="__('Not Saved')"
|
:label="__('Not Saved')"
|
||||||
@ -45,6 +45,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
successMessage: {
|
successMessage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Updated Successfully',
|
default: 'Updated Successfully',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user