diff --git a/next_crm/patches.txt b/next_crm/patches.txt index c0ec8ad91c..d26d4b95d6 100644 --- a/next_crm/patches.txt +++ b/next_crm/patches.txt @@ -17,3 +17,4 @@ next_crm.patches.v1_0.update_won_date next_crm.patches.v1_0.update_crm_views_filters next_crm.patches.v1_0.add_contracts_documents_section_to_customers next_crm.patches.v0_1.remove_stale_field_order_property_setters +next_crm.patches.v1_0.backfill_utm_source_from_legacy_source diff --git a/next_crm/patches/v1_0/backfill_utm_source_from_legacy_source.py b/next_crm/patches/v1_0/backfill_utm_source_from_legacy_source.py new file mode 100644 index 0000000000..701e0cc35b --- /dev/null +++ b/next_crm/patches/v1_0/backfill_utm_source_from_legacy_source.py @@ -0,0 +1,75 @@ +"""Backfill `utm_source` from the legacy `source` column on Lead and Opportunity. + +Mirrors https://github.com/frappe/erpnext/pull/55022 — shipped here so sites +running next_crm don't have to wait on the upstream patch landing. + +The v15 patch `erpnext.patches.v15_0.migrate_to_utm_analytics` migrated the +`Lead Source` lookup doctype's rows into `UTM Source`, then renamed the +per-row `source` field on Lead/Opportunity to `utm_source` via the +`oldfieldname` schema hint. + +That hint is documentation-only in Frappe — the only code path that actually +issues a `RENAME COLUMN` is the desk-driven Custom Field rename flow. +Standard DocField schema-sync only adds new columns and modifies existing +ones; it never renames or drops based on JSON changes. So on every site +that ran the v15 patch, `utm_source` was added empty alongside the legacy +`source` column, and every row's source value was orphaned on the now +meta-less `source` column. + +This patch promotes those values: where `utm_source` is empty and the +legacy `source` column still exists with a value, copy it forward. Rows +where both columns are set to *different* values are left untouched and +surfaced as a count for the operator to triage. Idempotent re-run is a +no-op. +""" + +import click +import frappe +from frappe.query_builder.functions import Count + + +def execute(): + for doctype in ("Lead", "Opportunity"): + if not frappe.db.exists("DocType", doctype): + continue + columns = frappe.db.get_table_columns(doctype) + if "source" not in columns or "utm_source" not in columns: + # Either the legacy column was already dropped (clean rename + # completed) or the new column never landed — nothing to do. + continue + + _backfill_one(doctype) + + +def _backfill_one(doctype: str) -> None: + table = frappe.qb.DocType(doctype) + + empty_utm = table.utm_source.isnull() | (table.utm_source == "") + utm_set = table.utm_source.isnotnull() & (table.utm_source != "") + source_set = table.source.isnotnull() & (table.source != "") + + to_fill = ( + frappe.qb.from_(table).select(Count("*")).where(empty_utm & source_set).run() + )[0][0] + conflicts = ( + frappe.qb.from_(table) + .select(Count("*")) + .where(utm_set & source_set & (table.utm_source != table.source)) + .run() + )[0][0] + + if to_fill: + frappe.qb.update(table).set(table.utm_source, table.source).where( + empty_utm & source_set + ).run() + click.secho( + f" {doctype}: copied legacy `source` → `utm_source` on {to_fill} rows", + fg="green", + ) + + if conflicts: + click.secho( + f" {doctype}: {conflicts} rows have both `source` and `utm_source` populated " + f"with DIFFERENT values; left untouched — review manually", + fg="yellow", + )