Skip to content

Commit 2e49844

Browse files
authored
Merge pull request #346 from nabinhait/erpnext-integration
feat: Create Quotation, Contact and Customer in ERPNext from Deal
2 parents 681c0a8 + 020c285 commit 2e49844

File tree

11 files changed

+351
-2
lines changed

11 files changed

+351
-2
lines changed

crm/fcrm/doctype/erpnext_crm_settings/__init__.py

Whitespace-only changes.

crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.js

Whitespace-only changes.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"actions": [],
3+
"allow_rename": 1,
4+
"creation": "2024-07-02 15:23:17.022214",
5+
"doctype": "DocType",
6+
"engine": "InnoDB",
7+
"field_order": [
8+
"enabled",
9+
"is_erpnext_in_the_current_site",
10+
"column_break_vfru",
11+
"erpnext_company",
12+
"section_break_oubd",
13+
"erpnext_site_url",
14+
"column_break_fllx",
15+
"api_key",
16+
"api_secret"
17+
],
18+
"fields": [
19+
{
20+
"depends_on": "eval:doc.enabled && !doc.is_erpnext_in_the_current_site",
21+
"fieldname": "api_key",
22+
"fieldtype": "Data",
23+
"label": "API Key",
24+
"mandatory_depends_on": "eval:!doc.is_erpnext_in_the_current_site"
25+
},
26+
{
27+
"depends_on": "eval:doc.enabled && !doc.is_erpnext_in_the_current_site",
28+
"fieldname": "api_secret",
29+
"fieldtype": "Data",
30+
"label": "API Secret",
31+
"mandatory_depends_on": "eval:!doc.is_erpnext_in_the_current_site"
32+
},
33+
{
34+
"depends_on": "enabled",
35+
"fieldname": "section_break_oubd",
36+
"fieldtype": "Section Break"
37+
},
38+
{
39+
"fieldname": "column_break_fllx",
40+
"fieldtype": "Column Break"
41+
},
42+
{
43+
"depends_on": "eval:doc.enabled && !doc.is_erpnext_in_the_current_site",
44+
"fieldname": "erpnext_site_url",
45+
"fieldtype": "Data",
46+
"label": "ERPNext Site URL",
47+
"mandatory_depends_on": "eval:!doc.is_erpnext_in_the_current_site"
48+
},
49+
{
50+
"depends_on": "enabled",
51+
"fieldname": "erpnext_company",
52+
"fieldtype": "Data",
53+
"label": "Company in ERPNext Site",
54+
"mandatory_depends_on": "enabled"
55+
},
56+
{
57+
"fieldname": "column_break_vfru",
58+
"fieldtype": "Column Break"
59+
},
60+
{
61+
"default": "0",
62+
"depends_on": "enabled",
63+
"fieldname": "is_erpnext_in_the_current_site",
64+
"fieldtype": "Check",
65+
"label": "Is ERPNext in the current site?"
66+
},
67+
{
68+
"default": "0",
69+
"fieldname": "enabled",
70+
"fieldtype": "Check",
71+
"label": "Enabled"
72+
}
73+
],
74+
"index_web_pages_for_search": 1,
75+
"issingle": 1,
76+
"links": [],
77+
"modified": "2024-09-13 15:06:23.317262",
78+
"modified_by": "Administrator",
79+
"module": "FCRM",
80+
"name": "ERPNext CRM Settings",
81+
"owner": "Administrator",
82+
"permissions": [
83+
{
84+
"create": 1,
85+
"delete": 1,
86+
"email": 1,
87+
"print": 1,
88+
"read": 1,
89+
"role": "System Manager",
90+
"share": 1,
91+
"write": 1
92+
}
93+
],
94+
"sort_field": "creation",
95+
"sort_order": "DESC",
96+
"states": []
97+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Copyright (c) 2024, Frappe and contributors
2+
# For license information, please see license.txt
3+
4+
import frappe
5+
from frappe import _
6+
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
7+
from frappe.model.document import Document
8+
from frappe.frappeclient import FrappeClient
9+
from frappe.utils import get_url_to_form
10+
import json
11+
12+
class ERPNextCRMSettings(Document):
13+
def validate(self):
14+
if self.enabled:
15+
self.validate_if_erpnext_installed()
16+
self.add_quotation_to_option()
17+
self.create_custom_fields()
18+
self.create_crm_form_script()
19+
20+
def validate_if_erpnext_installed(self):
21+
if self.is_erpnext_in_the_current_site:
22+
if "erpnext" not in frappe.get_installed_apps():
23+
frappe.throw(_("ERPNext is not installed in the current site"))
24+
25+
def add_quotation_to_option(self):
26+
if self.is_erpnext_in_the_current_site:
27+
if not frappe.db.exists("Property Setter", {"name": "Quotation-quotation_to-link_filters"}):
28+
make_property_setter(
29+
doctype="Quotation",
30+
fieldname="quotation_to",
31+
property="link_filters",
32+
value='[["DocType","name","in", ["Customer", "Lead", "Prospect", "Frappe CRM Deal"]]]',
33+
property_type="JSON",
34+
validate_fields_for_doctype=False,
35+
)
36+
37+
def create_custom_fields(self):
38+
if self.is_erpnext_in_the_current_site:
39+
from erpnext.crm.frappe_crm_api import create_custom_fields_for_frappe_crm
40+
create_custom_fields_for_frappe_crm()
41+
else:
42+
self.create_custom_fields_in_remote_site()
43+
44+
def create_custom_fields_in_remote_site(self):
45+
client = get_erpnext_site_client(self)
46+
try:
47+
client.post_api("erpnext.crm.frappe_crm_api.create_custom_fields_for_frappe_crm")
48+
except Exception:
49+
frappe.log_error(
50+
frappe.get_traceback(),
51+
f"Error while creating custom field in the remote erpnext site: {self.erpnext_site_url}"
52+
)
53+
frappe.throw("Error while creating custom field in ERPNext, check error log for more details")
54+
55+
def create_crm_form_script(self):
56+
if not frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"):
57+
script = get_crm_form_script()
58+
frappe.get_doc({
59+
"doctype": "CRM Form Script",
60+
"name": "Create Quotation from CRM Deal",
61+
"dt": "CRM Deal",
62+
"view": "Form",
63+
"script": script,
64+
"enabled": 1,
65+
"is_standard": 1
66+
}).insert()
67+
68+
def get_erpnext_site_client(erpnext_crm_settings):
69+
site_url = erpnext_crm_settings.erpnext_site_url
70+
api_key = erpnext_crm_settings.api_key
71+
api_secret = erpnext_crm_settings.api_secret
72+
73+
return FrappeClient(
74+
site_url, api_key=api_key, api_secret=api_secret
75+
)
76+
77+
@frappe.whitelist()
78+
def get_quotation_url(crm_deal, organization):
79+
erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings")
80+
if not erpnext_crm_settings.enabled:
81+
frappe.throw(_("ERPNext is not integrated with the CRM"))
82+
83+
if erpnext_crm_settings.is_erpnext_in_the_current_site:
84+
quotation_url = get_url_to_form("Quotation")
85+
return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}"
86+
else:
87+
site_url = erpnext_crm_settings.get("erpnext_site_url")
88+
quotation_url = f"{site_url}/app/quotation"
89+
90+
prospect = create_prospect_in_remote_site(crm_deal, erpnext_crm_settings)
91+
return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}"
92+
93+
def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings):
94+
try:
95+
client = get_erpnext_site_client(erpnext_crm_settings)
96+
doc = frappe.get_doc("CRM Deal", crm_deal)
97+
contacts = get_contacts(doc)
98+
return client.post_api("erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal",
99+
{
100+
"organization": doc.organization,
101+
"lead_name": doc.lead_name,
102+
"no_of_employees": doc.no_of_employees,
103+
"deal_owner": doc.deal_owner,
104+
"crm_deal": doc.name,
105+
"territory": doc.territory,
106+
"industry": doc.industry,
107+
"website": doc.website,
108+
"annual_revenue": doc.annual_revenue,
109+
"contacts": json.dumps(contacts),
110+
"erpnext_company": erpnext_crm_settings.erpnext_company
111+
},
112+
)
113+
except Exception:
114+
frappe.log_error(
115+
frappe.get_traceback(),
116+
f"Error while creating prospect in remote site: {erpnext_crm_settings.erpnext_site_url}"
117+
)
118+
frappe.throw(_("Error while creating prospect in ERPNext, check error log for more details"))
119+
120+
def get_contacts(doc):
121+
contacts = []
122+
for c in doc.contacts:
123+
contacts.append({
124+
"contact": c.contact,
125+
"full_name": c.full_name,
126+
"email": c.email,
127+
"mobile_no": c.mobile_no,
128+
"gender": c.gender,
129+
"is_primary": c.is_primary,
130+
})
131+
return contacts
132+
133+
def create_customer_in_erpnext(doc, method):
134+
erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings")
135+
if not erpnext_crm_settings.enabled or doc.status != "Won":
136+
return
137+
138+
contacts = get_contacts(doc)
139+
customer = {
140+
"customer_name": doc.organization,
141+
"customer_group": "All Customer Groups",
142+
"customer_type": "Company",
143+
"territory": doc.territory,
144+
"default_currency": doc.currency,
145+
"industry": doc.industry,
146+
"website": doc.website,
147+
"crm_deal": doc.name,
148+
"contacts": json.dumps(contacts),
149+
}
150+
if erpnext_crm_settings.is_erpnext_in_the_current_site:
151+
from erpnext.crm.frappe_crm_api import create_customer
152+
create_customer(customer)
153+
else:
154+
create_customer_in_remote_site(customer, erpnext_crm_settings)
155+
156+
def create_customer_in_remote_site(customer, erpnext_crm_settings):
157+
client = get_erpnext_site_client(erpnext_crm_settings)
158+
try:
159+
client.post_api("erpnext.crm.frappe_crm_api.create_customer", customer)
160+
except Exception:
161+
frappe.log_error(
162+
frappe.get_traceback(),
163+
"Error while creating customer in remote site"
164+
)
165+
frappe.throw(_("Error while creating customer in ERPNext, check error log for more details"))
166+
167+
def get_crm_form_script():
168+
return """
169+
function setupForm({ doc, call, $dialog, updateField, createToast }) {
170+
let actions = [];
171+
if (!["Lost", "Won"].includes(doc?.status)) {
172+
actions.push({
173+
label: __("Create Quotation"),
174+
onClick: async () => {
175+
let quotation_url = await call(
176+
"crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.get_quotation_url",
177+
{
178+
crm_deal: doc.name,
179+
organization: doc.organization
180+
}
181+
);
182+
183+
if (quotation_url) {
184+
window.open(quotation_url, '_blank');
185+
}
186+
}
187+
})
188+
}
189+
190+
return {
191+
actions: actions,
192+
};
193+
}
194+
"""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
2+
# See license.txt
3+
4+
# import frappe
5+
from frappe.tests.utils import FrappeTestCase
6+
7+
8+
class TestERPNextCRMSettings(FrappeTestCase):
9+
pass

crm/hooks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@
152152
"validate": ["crm.api.whatsapp.validate"],
153153
"on_update": ["crm.api.whatsapp.on_update"],
154154
},
155+
"CRM Deal": {
156+
"on_update": ["crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.create_customer_in_erpnext"],
157+
},
155158
}
156159

157160
# Scheduled Tasks

frontend/src/components/Fields.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div
44
v-for="section in sections"
55
:key="section.label"
6-
class="first:border-t-0 first:pt-0"
6+
class="section first:border-t-0 first:pt-0"
77
:class="section.hideBorder ? '' : 'border-t pt-4'"
88
>
99
<div
@@ -22,6 +22,7 @@
2222
>
2323
<div v-for="field in section.fields" :key="field.name">
2424
<div
25+
class="settings-field"
2526
v-if="
2627
(field.type == 'Check' ||
2728
(field.read_only && data[field.name]) ||
@@ -231,4 +232,12 @@ const props = defineProps({
231232
:deep(.form-control.prefix select) {
232233
padding-left: 2rem;
233234
}
235+
236+
.section {
237+
display: none;
238+
}
239+
240+
.section:has(.settings-field) {
241+
display: block;
242+
}
234243
</style>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<svg
3+
width="18"
4+
height="18"
5+
viewBox="0 0 18 18"
6+
fill="none"
7+
xmlns="http://www.w3.org/2000/svg"
8+
>
9+
<path
10+
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"
11+
stroke="currentColor"
12+
/>
13+
<path
14+
fill-rule="evenodd"
15+
clip-rule="evenodd"
16+
d="M11.7819 6.27142H11.5136H8.02453H6.28001V4.84002H11.7819V6.27142ZM8.02451 9.62623V11.5944H11.8267V13.0258H6.27999V8.19484H8.02451H11.5135V9.62623H8.02451Z"
17+
fill="currentColor"
18+
/>
19+
</svg>
20+
</template>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template>
2+
<SettingsPage doctype="ERPNext CRM Settings" :title="__('ERPNext Settings')" class="p-8" />
3+
</template>
4+
<script setup>
5+
import SettingsPage from '@/components/Settings/SettingsPage.vue'
6+
</script>

frontend/src/components/Settings/SettingsModal.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@
3939
<script setup>
4040
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
4141
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
42+
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
4243
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
4344
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
4445
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
4546
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
47+
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
4648
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
4749
import SidebarLink from '@/components/SidebarLink.vue'
4850
import { isWhatsappInstalled } from '@/composables/settings'
@@ -83,6 +85,11 @@ const tabs = computed(() => {
8385
component: markRaw(WhatsAppSettings),
8486
condition: () => isWhatsappInstalled.value,
8587
},
88+
{
89+
label: __('ERPNext'),
90+
icon: ERPNextIcon,
91+
component: markRaw(ERPNextSettings),
92+
},
8693
],
8794
},
8895
]

0 commit comments

Comments
 (0)