Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create Quotation, Contact and Customer in ERPNext from Deal #346

Merged
merged 5 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
97 changes: 97 additions & 0 deletions crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json
Original file line number Diff line number Diff line change
@@ -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 changes: 194 additions & 0 deletions crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py
Original file line number Diff line number Diff line change
@@ -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,
};
}
"""
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions crm/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@
"validate": ["crm.api.whatsapp.validate"],
"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
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/components/Fields.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div
v-for="section in sections"
: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'"
>
<div
Expand All @@ -22,6 +22,7 @@
>
<div v-for="field in section.fields" :key="field.name">
<div
class="settings-field"
v-if="
(field.type == 'Check' ||
(field.read_only && data[field.name]) ||
Expand Down Expand Up @@ -218,4 +219,12 @@ const props = defineProps({
:deep(.form-control.prefix select) {
padding-left: 2rem;
}

.section {
display: none;
}

.section:has(.settings-field) {
display: block;
}
</style>
20 changes: 20 additions & 0 deletions frontend/src/components/Icons/ERPNextIcon.vue
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions frontend/src/components/Settings/ERPNextSettings.vue
Original file line number Diff line number Diff line change
@@ -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>
7 changes: 7 additions & 0 deletions frontend/src/components/Settings/SettingsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@
<script setup>
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
import SidebarLink from '@/components/SidebarLink.vue'
import { isWhatsappInstalled } from '@/composables/settings'
Expand Down Expand Up @@ -83,6 +85,11 @@ const tabs = computed(() => {
component: markRaw(WhatsAppSettings),
condition: () => isWhatsappInstalled.value,
},
{
label: __('ERPNext'),
icon: ERPNextIcon,
component: markRaw(ERPNextSettings),
},
],
},
]
Expand Down
Loading
Loading