snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-18 01:36:01 +05:00
commit db69de1c11
905 changed files with 102713 additions and 0 deletions

View File

View File

@@ -0,0 +1,37 @@
import frappe
def sync_leads_from_all_enabled_sources(frequency: str | None = None) -> None:
enabled_sources = frappe.get_all(
"Lead Sync Source", filters={"enabled": 1, "background_sync_frequency": frequency}, pluck="name"
)
for source in enabled_sources:
lead_sync_source = frappe.get_cached_doc("Lead Sync Source", source)
try:
lead_sync_source._sync_leads()
except Exception as _:
frappe.log_error(f"Error syncing leads for source {source}")
def sync_leads_from_sources_5_minutes() -> None:
sync_leads_from_all_enabled_sources("Every 5 Minutes")
def sync_leads_from_sources_10_minutes() -> None:
sync_leads_from_all_enabled_sources("Every 10 Minutes")
def sync_leads_from_sources_15_minutes() -> None:
sync_leads_from_all_enabled_sources("Every 15 Minutes")
def sync_leads_from_sources_hourly() -> None:
sync_leads_from_all_enabled_sources("Hourly")
def sync_leads_from_sources_daily() -> None:
sync_leads_from_all_enabled_sources("Daily")
def sync_leads_from_sources_monthly() -> None:
sync_leads_from_all_enabled_sources("Monthly")

View File

View 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 Lead Form", {
refresh(frm) {
//
},
});

View File

@@ -0,0 +1,93 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:id",
"creation": "2025-09-26 19:01:48.325681",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"page",
"id",
"column_break_ahyo",
"form_name",
"section_break_iqhq",
"questions"
],
"fields": [
{
"fieldname": "page",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Page",
"options": "Facebook Page",
"reqd": 1
},
{
"fieldname": "id",
"fieldtype": "Data",
"label": "ID",
"unique": 1
},
{
"fieldname": "column_break_ahyo",
"fieldtype": "Column Break"
},
{
"fieldname": "form_name",
"fieldtype": "Data",
"label": "Form Name"
},
{
"fieldname": "section_break_iqhq",
"fieldtype": "Section Break"
},
{
"fieldname": "questions",
"fieldtype": "Table",
"label": "Questions",
"options": "Facebook Lead Form Question"
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-02-23 12:45:22.196877",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Facebook Lead Form",
"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
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "form_name"
}

View File

@@ -0,0 +1,48 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils.data import comma_and
class FacebookLeadForm(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
from crm.lead_syncing.doctype.facebook_lead_form_question.facebook_lead_form_question import (
FacebookLeadFormQuestion,
)
form_name: DF.Data | None
id: DF.Data | None
page: DF.Link
questions: DF.Table[FacebookLeadFormQuestion]
# end: auto-generated types
def validate(self):
self.check_mandatory_crm_fields_mapped()
def check_mandatory_crm_fields_mapped(self):
# right now only first name is mandatory
# later this can be elaborated to use doctype meta
mandatory_crm_lead_fields = [{"label": "First Name", "fieldname": "first_name"}]
mandatory_crm_lead_fieldnames = set(f["fieldname"] for f in mandatory_crm_lead_fields)
if self.is_new():
return
mapped_fields = set(q.mapped_to_crm_field for q in self.questions)
not_mapped = list(mandatory_crm_lead_fieldnames.difference(mapped_fields))
if not_mapped:
not_mapped_labels = [
f["label"] for f in mandatory_crm_lead_fields if f["fieldname"] in not_mapped
]
formatted_fields_list = frappe.bold(comma_and(not_mapped_labels))
frappe.throw(frappe._("Mandatory field(s) {0} must be mapped").format(formatted_fields_list))

View File

@@ -0,0 +1,20 @@
# 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 IntegrationTestFacebookLeadForm(IntegrationTestCase):
"""
Integration tests for FacebookLeadForm.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,66 @@
{
"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",
"mapped_to_crm_field"
],
"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"
},
{
"fieldname": "mapped_to_crm_field",
"fieldtype": "Autocomplete",
"in_list_view": 1,
"label": "Mapped to CRM Field"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-02-23 12:44:57.712174",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Facebook Lead Form Question",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,27 @@
# 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
mapped_to_crm_field: DF.Autocomplete | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
type: DF.Data | None
# end: auto-generated types
pass

View 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) {
// },
// });

View File

@@ -0,0 +1,95 @@
{
"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": "2026-02-23 12:45:31.760295",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Facebook Page",
"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
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales 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
}

View 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

View File

@@ -0,0 +1,20 @@
# 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

View File

@@ -0,0 +1,22 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Failed Lead Sync Log", {
refresh(frm) {
const btn = frm.add_custom_button(__("Retry Sync"), () => {
frm
.call({
doc: frm.doc,
method: "retry_sync",
btn,
})
.then(({ message }) => {
frappe.show_alert(
__("Sync Successful, CRM Lead: {0}!", [
frappe.utils.get_form_link("CRM Lead", message.name, true),
]),
);
});
});
},
});

View File

@@ -0,0 +1,102 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-10-19 17:29:10.261307",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"type",
"column_break_dhay",
"source",
"section_break_fhot",
"lead_data",
"section_break_knec",
"traceback"
],
"fields": [
{
"default": "Failure",
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
"options": "Duplicate\nFailure\nSynced",
"read_only": 1
},
{
"fieldname": "lead_data",
"fieldtype": "Code",
"label": "Lead Data",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "column_break_dhay",
"fieldtype": "Column Break"
},
{
"fieldname": "source",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Source",
"options": "Lead Sync Source",
"read_only": 1
},
{
"fieldname": "section_break_fhot",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_knec",
"fieldtype": "Section Break"
},
{
"fieldname": "traceback",
"fieldtype": "Code",
"label": "Traceback",
"read_only": 1
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-02-23 12:45:12.218952",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Failed Lead Sync Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,40 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from crm.lead_syncing.doctype.lead_sync_source.facebook import FacebookSyncSource
class FailedLeadSyncLog(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
lead_data: DF.Code | None
source: DF.Link | None
traceback: DF.Code | None
type: DF.Literal["Duplicate", "Failure", "Synced"]
# end: auto-generated types
@frappe.whitelist()
def retry_sync(self):
if not self.source:
frappe.throw(frappe._("Can't retry sync for this without source!"))
source = frappe.get_cached_doc("Lead Sync Source", self.source)
if source.type != "Facebook":
frappe.throw(frappe._("Not implemented yet!"))
crm_lead = FacebookSyncSource(
source.get_password("access_token"), source.facebook_lead_form
).sync_single_lead(frappe.parse_json(self.lead_data), raise_exception=True)
self.type = "Synced"
self.save()
return crm_lead

View File

@@ -0,0 +1,20 @@
# 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 IntegrationTestFailedLeadSyncLog(IntegrationTestCase):
"""
Integration tests for FailedLeadSyncLog.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,226 @@
import frappe
from frappe.exceptions import ValidationError
from frappe.integrations.utils import make_get_request
FB_GRAPH_API_BASE = "https://graph.facebook.com"
FB_GRAPH_API_VERSION = "v23.0"
class DuplicateLeadError(ValidationError):
pass
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 FacebookSyncSource:
def __init__(
self,
access_token: str,
form_id: str,
source_name: str | None = None,
):
self.access_token = access_token
self.form_id = form_id
self.source_name = source_name
self.form_questions_mapping = None
def get_api_url(self, endpoint: str) -> str:
return get_fb_graph_api_url(endpoint)
def sync(self):
leads = self.fetch_leads()
for lead in leads:
self.sync_single_lead(lead)
self.update_last_synced_at()
def sync_single_lead(self, lead, raise_exception=False):
question_to_field_map = self.get_form_questions_mapping()
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
crm_lead_data = {
question_to_field_map.get(k): v for k, v in lead_data.items() if k in question_to_field_map
}
crm_lead_data["source"] = "Facebook"
crm_lead_data["facebook_lead_id"] = lead["id"]
crm_lead_data["facebook_form_id"] = self.form_id
try:
self.validate_duplicate_lead(crm_lead_data, question_to_field_map)
return frappe.get_doc(
{
"doctype": "CRM Lead",
**crm_lead_data,
}
).insert(ignore_permissions=True)
except (frappe.UniqueValidationError, DuplicateLeadError):
self.create_failure_log(lead, "Duplicate")
if raise_exception:
raise
except Exception:
self.create_failure_log(lead, traceback=frappe.get_traceback(with_context=True))
if raise_exception:
raise
def fetch_leads(self):
url = self.get_api_url(f"/{self.form_id}/leads")
params = {
"access_token": self.access_token,
"fields": "id,created_time,field_data",
"limit": 100000, # TODO: pagination
}
filtering = []
if self.last_synced_at:
timestamp = frappe.utils.data.get_timestamp(self.last_synced_at)
filtering.append({"field": "time_created", "operator": "GREATER_THAN", "value": timestamp})
params["filtering"] = frappe.as_json(filtering)
return make_get_request(
url,
params=params,
).get("data", [])
def get_form_questions_mapping(self):
if self.form_questions_mapping:
return self.form_questions_mapping
form_questions = frappe.db.get_all(
"Facebook Lead Form Question",
filters={"parent": self.form_id},
fields=["key", "mapped_to_crm_field"],
)
self.form_questions_mapping = {
q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]
}
return self.form_questions_mapping
@property
def last_synced_at(self):
return frappe.db.get_value(
"Lead Sync Source", self.source_name or {"facebook_lead_form": self.form_id}, "last_synced_at"
)
def create_failure_log(
self, lead_data: dict | None = None, type: str = "Failure", traceback: str | None = None
):
return frappe.get_doc(
{
"doctype": "Failed Lead Sync Log",
"type": type,
"lead_data": frappe.as_json(lead_data),
"source": self.get_source_name(),
"traceback": traceback,
}
).insert(ignore_permissions=True)
def update_last_synced_at(self):
frappe.db.set_value(
"Lead Sync Source",
self.source_name or {"facebook_lead_form": self.form_id},
"last_synced_at",
frappe.utils.now(),
)
def get_source_name(self):
if self.source_name:
return self.source_name
return frappe.db.get_value("Lead Sync Source", {"facebook_lead_form": self.form_id}, "name")
def validate_duplicate_lead(self, lead_data: dict, field_mapping: dict):
validation_filters = {crm_field: lead_data[crm_field] for crm_field in field_mapping.values()}
validation_filters["facebook_form_id"] = lead_data["facebook_form_id"] # only for this campaign
if frappe.db.exists("CRM Lead", validation_filters):
raise DuplicateLeadError
@frappe.whitelist()
def fetch_and_store_pages_from_facebook(access_token: str) -> list[dict]:
if not access_token:
frappe.throw(frappe._("Access token is required"))
account_details = get_fb_account_details(access_token)
if not account_details.get("id"):
frappe.throw(frappe._("Invalid access token provided for Facebook."))
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)
forms = fetch_and_store_leadgen_forms_from_facebook(page_id, page["access_token"])
page["forms"] = forms
return pages
def get_fb_account_details(access_token: str) -> dict:
url = get_fb_graph_api_url("me")
try:
response = make_get_request(url, params={"access_token": access_token})
except Exception as _:
frappe.throw(frappe._("Please check your access token"))
return response
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) -> list[dict]:
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)
return forms
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"],
}
)
form_doc.insert(ignore_permissions=True)
@frappe.whitelist()
def get_pages_with_forms() -> list[dict]:
pages = frappe.db.get_all("Facebook Page", fields=["id", "name"])
for page in pages:
forms = frappe.db.get_all("Facebook Lead Form", filters={"page": page["id"]}, fields=["id", "name"])
page["forms"] = forms
return pages

View File

@@ -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."));
});
});
},
});

View File

@@ -0,0 +1,130 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "prompt",
"creation": "2025-09-26 18:51:41.145560",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"type",
"access_token",
"column_break_lwcw",
"last_synced_at",
"enabled",
"background_sync_frequency",
"facebook_section",
"facebook_page",
"column_break_zukm",
"facebook_lead_form"
],
"fields": [
{
"default": "Facebook",
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Facebook",
"reqd": 1
},
{
"fieldname": "column_break_lwcw",
"fieldtype": "Column Break"
},
{
"fieldname": "last_synced_at",
"fieldtype": "Datetime",
"label": "Last Synced At",
"read_only": 1
},
{
"fieldname": "access_token",
"fieldtype": "Password",
"label": "Access Token",
"length": 500,
"reqd": 1
},
{
"fieldname": "facebook_page",
"fieldtype": "Link",
"label": "Facebook Page",
"options": "Facebook Page"
},
{
"fieldname": "facebook_lead_form",
"fieldtype": "Link",
"label": "Facebook Lead Form",
"options": "Facebook Lead Form",
"unique": 1
},
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled?"
},
{
"depends_on": "eval:doc.type===\"Facebook\"",
"fieldname": "facebook_section",
"fieldtype": "Section Break",
"label": "Facebook"
},
{
"fieldname": "column_break_zukm",
"fieldtype": "Column Break"
},
{
"default": "Hourly",
"fieldname": "background_sync_frequency",
"fieldtype": "Select",
"label": "Background Sync Frequency",
"options": "Every 5 Minutes\nEvery 10 Minutes\nEvery 15 Minutes\nHourly\nDaily\nMonthly",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Failed Lead Sync Log",
"link_fieldname": "source"
}
],
"modified": "2026-02-23 12:45:55.637376",
"modified_by": "Administrator",
"module": "Lead Syncing",
"name": "Lead Sync Source",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,69 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from crm.lead_syncing.doctype.lead_sync_source.facebook import (
FacebookSyncSource,
fetch_and_store_pages_from_facebook,
)
class LeadSyncSource(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.Password
background_sync_frequency: DF.Literal[
"Every 5 Minutes", "Every 10 Minutes", "Every 15 Minutes", "Hourly", "Daily", "Monthly"
]
enabled: DF.Check
facebook_lead_form: DF.Link | None
facebook_page: DF.Link | None
last_synced_at: DF.Datetime | None
type: DF.Literal["Facebook"]
# end: auto-generated types
def validate(self):
self.validate_same_fb_form_active()
def validate_same_fb_form_active(self):
if not self.enabled:
return
if not self.facebook_lead_form:
return
already_active = frappe.db.exists(
"Lead Sync Source",
{"enabled": 1, "facebook_lead_form": self.facebook_lead_form, "name": ["!=", self.name]},
)
if already_active:
frappe.throw(frappe._("A lead sync source is already enabled for this Facebook Lead Form!"))
def before_insert(self):
if self.type == "Facebook" and self.access_token:
fetch_and_store_pages_from_facebook(self.access_token)
# rest of the source types can be added here
@frappe.whitelist()
def sync_leads(self):
if frappe.conf.developer_mode:
self._sync_leads()
return
frappe.enqueue_doc(self.doctype, self.name, "_sync_leads", queue="long")
def _sync_leads(self):
if self.type == "Facebook" and self.access_token:
if not self.facebook_lead_form:
frappe.throw(frappe._("Please select a lead gen form before syncing!"))
FacebookSyncSource(self.get_password("access_token"), self.facebook_lead_form).sync()

View File

@@ -0,0 +1,20 @@
# 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