feat: FB Lead Sync first cut
This commit is contained in:
parent
b1b7c5d246
commit
d0cccc2e61
@ -22,6 +22,8 @@ add_to_apps_screen = [
|
||||
}
|
||||
]
|
||||
|
||||
export_python_type_annotations = True
|
||||
|
||||
# Includes in <head>
|
||||
# ------------------
|
||||
|
||||
|
||||
0
crm/lead_syncing/__init__.py
Normal file
0
crm/lead_syncing/__init__.py
Normal file
0
crm/lead_syncing/doctype/__init__.py
Normal file
0
crm/lead_syncing/doctype/__init__.py
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-09-26 19:45:47.696180",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
"id",
|
||||
"column_break_hgde",
|
||||
"key",
|
||||
"type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hgde",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "id",
|
||||
"fieldtype": "Data",
|
||||
"label": "ID"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-26 19:48:30.897092",
|
||||
"modified_by": "hussain@frappe.io",
|
||||
"module": "Lead Syncing",
|
||||
"name": "Facebook Lead Form Question",
|
||||
"owner": "hussain@frappe.io",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
# 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 FacebookLeadFormQuestion(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
id: DF.Data | None
|
||||
key: DF.Data
|
||||
label: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
0
crm/lead_syncing/doctype/facebook_page/__init__.py
Normal file
0
crm/lead_syncing/doctype/facebook_page/__init__.py
Normal file
8
crm/lead_syncing/doctype/facebook_page/facebook_page.js
Normal file
8
crm/lead_syncing/doctype/facebook_page/facebook_page.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Facebook Page", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
83
crm/lead_syncing/doctype/facebook_page/facebook_page.json
Normal file
83
crm/lead_syncing/doctype/facebook_page/facebook_page.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "field:id",
|
||||
"creation": "2025-09-26 18:59:12.833879",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"page_name",
|
||||
"account_id",
|
||||
"category",
|
||||
"column_break_eteo",
|
||||
"id",
|
||||
"access_token"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "category",
|
||||
"fieldtype": "Data",
|
||||
"label": "Category"
|
||||
},
|
||||
{
|
||||
"fieldname": "id",
|
||||
"fieldtype": "Data",
|
||||
"label": "ID",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_eteo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Access Token"
|
||||
},
|
||||
{
|
||||
"fieldname": "page_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Page Name"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Facebook Lead Form",
|
||||
"link_fieldname": "page"
|
||||
}
|
||||
],
|
||||
"modified": "2025-09-26 19:36:59.413214",
|
||||
"modified_by": "hussain@frappe.io",
|
||||
"module": "Lead Syncing",
|
||||
"name": "Facebook Page",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "hussain@frappe.io",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "page_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
24
crm/lead_syncing/doctype/facebook_page/facebook_page.py
Normal file
24
crm/lead_syncing/doctype/facebook_page/facebook_page.py
Normal file
@ -0,0 +1,24 @@
|
||||
# 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 FacebookPage(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
access_token: DF.SmallText | None
|
||||
account_id: DF.Data | None
|
||||
category: DF.Data | None
|
||||
id: DF.Data | None
|
||||
page_name: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
22
crm/lead_syncing/doctype/facebook_page/test_facebook_page.py
Normal file
22
crm/lead_syncing/doctype/facebook_page/test_facebook_page.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
# 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 IntegrationTestFacebookPage(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for FacebookPage.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-09-26 18:54:57.313880",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"source_field_name",
|
||||
"column_break_zbml",
|
||||
"crm_field_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "source_field_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Source Field Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_zbml",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "crm_field_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "CRM Field Name",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-26 18:55:59.773584",
|
||||
"modified_by": "hussain@frappe.io",
|
||||
"module": "Lead Syncing",
|
||||
"name": "Lead Form Field Mapping",
|
||||
"owner": "hussain@frappe.io",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
# 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 LeadFormFieldMapping(Document):
|
||||
pass
|
||||
@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Lead Sync Source", {
|
||||
refresh(frm) {
|
||||
frm.add_custom_button(__('Sync Now'), () => {
|
||||
frm.call("sync_leads").then(() => {
|
||||
frappe.msgprint(__('Lead sync initiated.'));
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
105
crm/lead_syncing/doctype/lead_sync_source/lead_sync_source.json
Normal file
105
crm/lead_syncing/doctype/lead_sync_source/lead_sync_source.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2025-09-26 18:51:41.145560",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"access_token",
|
||||
"column_break_lwcw",
|
||||
"last_synced_at",
|
||||
"section_break_itlk",
|
||||
"field_mapping",
|
||||
"facebook_tab",
|
||||
"facebook_page",
|
||||
"column_break_uxlr",
|
||||
"facebook_lead_form"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Facebook",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"options": "Facebook"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lwcw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "last_synced_at",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Last Synced At",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Access Token"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_itlk",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "field_mapping",
|
||||
"fieldtype": "Table",
|
||||
"label": "Field Mapping",
|
||||
"options": "Lead Form Field Mapping"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type===\"Facebook\"",
|
||||
"fieldname": "facebook_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Facebook"
|
||||
},
|
||||
{
|
||||
"fieldname": "facebook_page",
|
||||
"fieldtype": "Link",
|
||||
"label": "Facebook Page",
|
||||
"options": "Facebook Page"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uxlr",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "facebook_lead_form",
|
||||
"fieldtype": "Link",
|
||||
"label": "Facebook Lead Form",
|
||||
"options": "Facebook Lead Form"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-26 19:04:31.584094",
|
||||
"modified_by": "hussain@frappe.io",
|
||||
"module": "Lead Syncing",
|
||||
"name": "Lead Sync Source",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "hussain@frappe.io",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "type",
|
||||
"track_changes": 1
|
||||
}
|
||||
116
crm/lead_syncing/doctype/lead_sync_source/lead_sync_source.py
Normal file
116
crm/lead_syncing/doctype/lead_sync_source/lead_sync_source.py
Normal file
@ -0,0 +1,116 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.integrations.utils import make_get_request
|
||||
from frappe.model.document import Document
|
||||
|
||||
FB_GRAPH_API_BASE = "https://graph.facebook.com"
|
||||
FB_GRAPH_API_VERSION = "v23.0"
|
||||
|
||||
|
||||
def get_fb_graph_api_url(endpoint: str) -> str:
|
||||
if endpoint.startswith("/"):
|
||||
endpoint = endpoint[1:]
|
||||
|
||||
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
|
||||
|
||||
|
||||
class LeadSyncSource(Document):
|
||||
def before_save(self):
|
||||
if self.type == "Facebook" and self.access_token:
|
||||
fetch_and_store_pages_from_facebook(self.access_token)
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
def sync_leads(self):
|
||||
if self.type == "Facebook" and self.access_token:
|
||||
sync_leads_from_facebook(self.access_token, self.facebook_lead_form)
|
||||
|
||||
|
||||
def sync_leads_from_facebook(access_token: str, lead_form_id: str) -> None:
|
||||
url = get_fb_graph_api_url(f"/{lead_form_id}/leads")
|
||||
leads = make_get_request(
|
||||
url,
|
||||
params={
|
||||
"access_token": access_token,
|
||||
"fields": "id,created_time,field_data",
|
||||
"limit": 15000,
|
||||
},
|
||||
).get("data", [])
|
||||
for lead in leads:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Lead",
|
||||
"first_name": lead["field_data"][0]["values"][0],
|
||||
"source": "Facebook",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def fetch_and_store_pages_from_facebook(access_token: str) -> None:
|
||||
account_details = get_fb_account_details(access_token)
|
||||
if not account_details.get("id"):
|
||||
frappe.log_error("Invalid access token provided for Facebook.", "Lead Sync Source")
|
||||
return
|
||||
|
||||
url = get_fb_graph_api_url("/me/accounts")
|
||||
pages = make_get_request(url, params={"access_token": access_token}).get("data", [])
|
||||
for page in pages:
|
||||
page_id = page["id"]
|
||||
already_synced = frappe.db.exists("Facebook Page", page_id)
|
||||
if not already_synced:
|
||||
create_facebook_page_in_db(page, account_details)
|
||||
fetch_and_store_leadgen_forms_from_facebook(page_id, page["access_token"])
|
||||
|
||||
|
||||
def get_fb_account_details(access_token: str) -> dict:
|
||||
url = get_fb_graph_api_url("me")
|
||||
return make_get_request(url, params={"access_token": access_token})
|
||||
|
||||
|
||||
def create_facebook_page_in_db(page: dict, account_details: dict) -> None:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Facebook Page",
|
||||
"page_name": page["name"],
|
||||
"id": page["id"],
|
||||
"category": page["category"],
|
||||
"access_token": page["access_token"],
|
||||
"account_id": account_details["id"],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def fetch_and_store_leadgen_forms_from_facebook(page_id: str, page_access_token: str) -> None:
|
||||
fields = "id,name,questions"
|
||||
url = get_fb_graph_api_url(f"/{page_id}/leadgen_forms")
|
||||
forms = make_get_request(
|
||||
url,
|
||||
params={
|
||||
"access_token": page_access_token,
|
||||
"fields": fields,
|
||||
"limit": 15000,
|
||||
},
|
||||
).get("data", [])
|
||||
for form in forms:
|
||||
form_id = form["id"]
|
||||
already_synced = frappe.db.exists("Facebook Lead Form", form_id)
|
||||
if already_synced:
|
||||
continue
|
||||
create_facebook_lead_form_in_db(form, page_id)
|
||||
|
||||
|
||||
def create_facebook_lead_form_in_db(form: dict, page_id: str) -> None:
|
||||
form_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Facebook Lead Form",
|
||||
"form_name": form["name"],
|
||||
"id": form["id"],
|
||||
"page": page_id,
|
||||
"questions": form["questions"],
|
||||
}
|
||||
)
|
||||
|
||||
frappe.errprint(form_doc.as_dict())
|
||||
form_doc.insert(ignore_permissions=True)
|
||||
@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
# 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 IntegrationTestLeadSyncSource(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LeadSyncSource.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@ -1 +1,2 @@
|
||||
FCRM
|
||||
FCRM
|
||||
Lead Syncing
|
||||
@ -61,4 +61,4 @@ typing-modules = ["frappe.types.DF"]
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "tab"
|
||||
docstring-code-format = true
|
||||
# docstring-code-format = true
|
||||
Loading…
x
Reference in New Issue
Block a user