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
398 lines
9.7 KiB
Python
398 lines
9.7 KiB
Python
# 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)
|