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
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
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 1s
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
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
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 1s
This commit is contained in:
397
crm/domain_enrichment/install.py
Normal file
397
crm/domain_enrichment/install.py
Normal file
@@ -0,0 +1,397 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
"""One-time code-to-data migration of the enrichment knowledge base.
|
||||
|
||||
The POC baked its keyword / signature / mapping tables into Python constants.
|
||||
This seeder translates them into editable desk records (CRM Enrichment Rule +
|
||||
Pattern, CRM Enrichment Field Mapping) so a sales-ops admin owns the knowledge.
|
||||
|
||||
Everything here is idempotent (skip-if-exists) and wired into both ``after_install``
|
||||
and ``after_migrate`` so a fresh install and every migrate land the defaults
|
||||
without clobbering admin edits.
|
||||
"""
|
||||
|
||||
import frappe
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Rule knowledge (recovered from the POC's extractors.py constant tables)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
# industry -> keywords (matched against headline text, weight 1 each)
|
||||
INDUSTRY_KEYWORDS = {
|
||||
"ERP": [
|
||||
"erp",
|
||||
"enterprise resource planning",
|
||||
"procurement",
|
||||
"supply chain management",
|
||||
"inventory management",
|
||||
],
|
||||
"CRM": [
|
||||
"crm",
|
||||
"customer relationship management",
|
||||
"sales pipeline",
|
||||
"lead management",
|
||||
"contact management",
|
||||
],
|
||||
"Ecommerce": [
|
||||
"ecommerce",
|
||||
"e-commerce",
|
||||
"online shopping",
|
||||
"online store",
|
||||
"marketplace",
|
||||
"shopping cart",
|
||||
"add to cart",
|
||||
"add to bag",
|
||||
"storefront",
|
||||
"checkout",
|
||||
"shop",
|
||||
"free shipping",
|
||||
"free returns",
|
||||
"new arrivals",
|
||||
"best sellers",
|
||||
"bestsellers",
|
||||
"size guide",
|
||||
"apparel",
|
||||
"clothing brand",
|
||||
"direct-to-consumer",
|
||||
"quick commerce",
|
||||
"q-commerce",
|
||||
"online grocery",
|
||||
"online groceries",
|
||||
"grocery delivery",
|
||||
"grocery app",
|
||||
"delivery app",
|
||||
"online shop",
|
||||
],
|
||||
"SaaS": ["saas", "software as a service", "subscription software", "cloud platform", "free trial"],
|
||||
"Technology": [
|
||||
"software",
|
||||
"developer",
|
||||
"developers",
|
||||
"api",
|
||||
"open source",
|
||||
"cloud computing",
|
||||
"it services",
|
||||
"information technology",
|
||||
"artificial intelligence",
|
||||
"machine learning",
|
||||
],
|
||||
"Semiconductors": [
|
||||
"semiconductor",
|
||||
"gpu",
|
||||
"graphics card",
|
||||
"processor",
|
||||
"chipset",
|
||||
"supercomputer",
|
||||
"microprocessor",
|
||||
],
|
||||
"Manufacturing": [
|
||||
"manufacturing",
|
||||
"factory",
|
||||
"production line",
|
||||
"assembly line",
|
||||
"fabrication",
|
||||
"industrial equipment",
|
||||
"3d printing",
|
||||
"additive manufacturing",
|
||||
"cnc machining",
|
||||
"rapid prototyping",
|
||||
"injection molding",
|
||||
"prototyping",
|
||||
],
|
||||
"Healthcare": [
|
||||
"healthcare",
|
||||
"health care",
|
||||
"clinic",
|
||||
"hospital",
|
||||
"patient care",
|
||||
"medical",
|
||||
"telehealth",
|
||||
"pharmaceutical",
|
||||
"pharma",
|
||||
],
|
||||
"Education": [
|
||||
"education",
|
||||
"e-learning",
|
||||
"students",
|
||||
"curriculum",
|
||||
"university",
|
||||
"online courses",
|
||||
"courses",
|
||||
"course",
|
||||
"edtech",
|
||||
"learning platform",
|
||||
],
|
||||
"Consulting": [
|
||||
"consulting",
|
||||
"advisory services",
|
||||
"consultants",
|
||||
"professional services",
|
||||
"strategy consulting",
|
||||
],
|
||||
"Marketing": [
|
||||
"marketing agency",
|
||||
"digital marketing",
|
||||
"advertising agency",
|
||||
"seo services",
|
||||
"ad campaigns",
|
||||
"media buying",
|
||||
],
|
||||
"Finance": [
|
||||
"fintech",
|
||||
"banking",
|
||||
"payments",
|
||||
"lending",
|
||||
"insurance",
|
||||
"financial services",
|
||||
"trading",
|
||||
"wealth management",
|
||||
],
|
||||
"Logistics": [
|
||||
"logistics",
|
||||
"freight",
|
||||
"warehousing",
|
||||
"fulfillment",
|
||||
"shipping carrier",
|
||||
"last-mile delivery",
|
||||
],
|
||||
"Food & Beverage": [
|
||||
"food and beverage",
|
||||
"beverage",
|
||||
"beverages",
|
||||
"snacks",
|
||||
"snack",
|
||||
"packaged food",
|
||||
"fmcg",
|
||||
"consumer goods",
|
||||
"sparkling water",
|
||||
"iced tea",
|
||||
"energy drink",
|
||||
"soft drink",
|
||||
"drink",
|
||||
"drinks",
|
||||
"soda",
|
||||
"kombucha",
|
||||
"cereal",
|
||||
"coffee",
|
||||
"chocolate",
|
||||
"protein bar",
|
||||
],
|
||||
"Retail": ["retailer", "department store", "brick and mortar"],
|
||||
"Media": [
|
||||
"entertainment",
|
||||
"streaming service",
|
||||
"video game",
|
||||
"game studio",
|
||||
"publishing house",
|
||||
"news media",
|
||||
"broadcasting",
|
||||
"journalism",
|
||||
"journalists",
|
||||
"media company",
|
||||
"newsroom",
|
||||
"newsletter",
|
||||
"reported articles",
|
||||
"investigative",
|
||||
"editorial",
|
||||
"magazine",
|
||||
"podcast",
|
||||
"reporting on",
|
||||
],
|
||||
"Real Estate": [
|
||||
"real estate",
|
||||
"property management",
|
||||
"commercial property",
|
||||
"interior design",
|
||||
"home design",
|
||||
"home improvement",
|
||||
"remodeling",
|
||||
"decorating",
|
||||
"home renovation",
|
||||
],
|
||||
"Travel": ["hospitality", "tourism", "flight booking", "travel agency"],
|
||||
"Automotive": ["automotive", "electric vehicle", "car manufacturer"],
|
||||
"Telecom": ["telecommunications", "broadband", "network operator"],
|
||||
"Energy": ["renewable energy", "solar power", "oil and gas"],
|
||||
"Legal": [
|
||||
"law firm",
|
||||
"legal services",
|
||||
"litigation",
|
||||
"legal help",
|
||||
"legal documents",
|
||||
"legal advice",
|
||||
"attorney",
|
||||
"attorneys",
|
||||
"lawyer",
|
||||
"lawyers",
|
||||
],
|
||||
"Nonprofit": ["nonprofit", "non-profit", "ngo", "charity"],
|
||||
}
|
||||
|
||||
# social network -> regex (matched against HTML; stored as regex patterns)
|
||||
SOCIAL_PATTERNS = {
|
||||
"linkedin": [r"linkedin\.com/(company|in|school)/"],
|
||||
"twitter": [r"(twitter\.com|x\.com)/[A-Za-z0-9_]+"],
|
||||
"github": [r"github\.com/[A-Za-z0-9_.-]+"],
|
||||
"facebook": [r"facebook\.com/[A-Za-z0-9_.\-/]+"],
|
||||
"instagram": [r"instagram\.com/[A-Za-z0-9_.]+"],
|
||||
"youtube": [r"youtube\.com/(channel/|c/|user/|@)[A-Za-z0-9_.\-]+"],
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Field-mapping knowledge (recovered from the POC's api/website_intelligence.py)
|
||||
# Translates FIELD_MAP / OVERRIDABLE_DEFAULTS / ALWAYS_REFRESH into records.
|
||||
# (source_key, target_doctype, target_fieldname, write_policy, default_values, create_missing_link)
|
||||
# --------------------------------------------------------------------------- #
|
||||
_LEAD = "CRM Lead"
|
||||
_DEAL = "CRM Deal"
|
||||
_ORG = "CRM Organization"
|
||||
|
||||
# Per-doctype the org "name" lands on a different field: Lead.organization,
|
||||
# Deal.organization_name (free text), Org.organization_name.
|
||||
FIELD_MAPPINGS = [
|
||||
# company name
|
||||
("company_name", _LEAD, "organization", "Fill if empty", None, 0),
|
||||
("company_name", _DEAL, "organization_name", "Fill if empty", None, 0),
|
||||
("company_name", _ORG, "organization_name", "Fill if empty", None, 0),
|
||||
# logo / description (shared)
|
||||
*[
|
||||
(k, dt, fn, "Fill if empty", None, 0)
|
||||
for dt in (_LEAD, _DEAL, _ORG)
|
||||
for k, fn in (("logo", "organization_logo"), ("description", "company_description"))
|
||||
],
|
||||
# industry — link, opt-in auto-create of missing CRM Industry
|
||||
*[("industry", dt, "industry", "Fill if empty", None, 1) for dt in (_LEAD, _DEAL, _ORG)],
|
||||
# social links (shared, fill-empty)
|
||||
*[
|
||||
(k, dt, fn, "Fill if empty", None, 0)
|
||||
for dt in (_LEAD, _DEAL, _ORG)
|
||||
for k, fn in (("linkedin", "linkedin"), ("twitter", "twitter"), ("facebook", "facebook"))
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
def seed_default_rules_and_mappings():
|
||||
"""Idempotently create the default enrichment rules + field mappings.
|
||||
|
||||
Safe to run repeatedly (after_install + after_migrate): each record is keyed
|
||||
on a stable name and skipped if it already exists, so admin edits are never
|
||||
clobbered.
|
||||
"""
|
||||
_seed_industry_rules()
|
||||
_seed_social_rules()
|
||||
_seed_field_mappings()
|
||||
_seed_settings_defaults()
|
||||
|
||||
|
||||
def _seed_settings_defaults():
|
||||
"""Embed the default Link Priority Order rows into Settings.
|
||||
|
||||
Idempotent: only populated when the table is empty, so admin edits/removals are
|
||||
never clobbered. The default list lives in ``config.DEFAULT_LINK_PRIORITY``.
|
||||
"""
|
||||
from crm.domain_enrichment.config import DEFAULT_LINK_PRIORITY
|
||||
|
||||
settings = frappe.get_single("CRM Enrichment Settings")
|
||||
if settings.link_priority_order:
|
||||
return
|
||||
for keyword, weight in DEFAULT_LINK_PRIORITY:
|
||||
settings.append("link_priority_order", {"keyword": keyword, "weight": weight})
|
||||
settings.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def _ensure_industry(industry: str):
|
||||
if not frappe.db.exists("CRM Industry", industry):
|
||||
frappe.get_doc({"doctype": "CRM Industry", "industry": industry}).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def _make_rule(
|
||||
rule_name,
|
||||
rule_type,
|
||||
patterns,
|
||||
*,
|
||||
target_value=None,
|
||||
industry=None,
|
||||
weight=1,
|
||||
match_scope="Full Text",
|
||||
is_regex=False,
|
||||
):
|
||||
# Match on the rule_name field, not the document name: the autoname
|
||||
# ("{rule_type}.-.####") means name != rule_name, so a bare-string exists()
|
||||
# never matches a seeded rule and re-seeding would collide with the unique
|
||||
# rule_name constraint (breaking bench migrate). rule_name stays out of the
|
||||
# name because it is free text (colons/spaces) that a naming series rejects.
|
||||
if frappe.db.exists("CRM Enrichment Rule", {"rule_name": rule_name}):
|
||||
return
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Enrichment Rule",
|
||||
"rule_name": rule_name,
|
||||
"rule_type": rule_type,
|
||||
"target_value": target_value,
|
||||
"industry": industry,
|
||||
"weight": weight,
|
||||
"match_scope": match_scope,
|
||||
"enabled": 1,
|
||||
"patterns": [{"pattern": p, "is_regex": 1 if is_regex else 0} for p in patterns],
|
||||
}
|
||||
)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def _seed_industry_rules():
|
||||
for industry, keywords in INDUSTRY_KEYWORDS.items():
|
||||
_ensure_industry(industry)
|
||||
_make_rule(
|
||||
f"Industry: {industry}",
|
||||
"Industry",
|
||||
keywords,
|
||||
industry=industry,
|
||||
match_scope="Headline",
|
||||
)
|
||||
|
||||
|
||||
def _seed_social_rules():
|
||||
for network, patterns in SOCIAL_PATTERNS.items():
|
||||
_make_rule(
|
||||
f"Social: {network}",
|
||||
"Social",
|
||||
patterns,
|
||||
target_value=network,
|
||||
match_scope="HTML",
|
||||
is_regex=True,
|
||||
)
|
||||
|
||||
|
||||
def _seed_field_mappings():
|
||||
for (
|
||||
source_key,
|
||||
target_doctype,
|
||||
target_fieldname,
|
||||
write_policy,
|
||||
default_values,
|
||||
create_link,
|
||||
) in FIELD_MAPPINGS:
|
||||
exists = frappe.db.exists(
|
||||
"CRM Enrichment Field Mapping",
|
||||
{
|
||||
"source_key": source_key,
|
||||
"target_doctype": target_doctype,
|
||||
"target_fieldname": target_fieldname,
|
||||
},
|
||||
)
|
||||
if exists:
|
||||
continue
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Enrichment Field Mapping",
|
||||
"enabled": 1,
|
||||
"source_key": source_key,
|
||||
"target_doctype": target_doctype,
|
||||
"target_fieldname": target_fieldname,
|
||||
"write_policy": write_policy,
|
||||
"default_values": default_values,
|
||||
"create_missing_link": create_link,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
Reference in New Issue
Block a user