From fe6300403c94f7ce042de332c9b1c68212ddc915 Mon Sep 17 00:00:00 2001 From: dpk404 Date: Tue, 19 May 2026 12:25:59 +0000 Subject: [PATCH 1/2] fix: backfill utm_source from legacy `source` column on Lead/Opportunity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports https://github.com/frappe/erpnext/pull/55022 so next_crm sites can recover orphaned source values without waiting on the upstream patch to land. The v15 `migrate_to_utm_analytics` patch renamed `source -> utm_source` on Lead and Opportunity via the `oldfieldname` schema hint. That hint is documentation-only — Frappe's DocField sync never issues a RENAME COLUMN based on it (only the desk-driven Custom Field rename flow does). So on every upgraded site, `utm_source` was added as an empty column and every row's data stayed on the legacy `source` column, where get_meta()-aware code can't see it. This patch: - Skips cleanly if either column is absent (clean rename or fresh site). - Copies legacy `source` -> `utm_source` for rows where utm is empty. - Leaves rows with both columns populated to *different* values untouched and reports a count for manual triage. - Idempotent — a second run is a no-op. Reports per-doctype row counts on stdout during `bench migrate`. --- next_crm/patches.txt | 1 + .../backfill_utm_source_from_legacy_source.py | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 next_crm/patches/v1_0/backfill_utm_source_from_legacy_source.py 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..d4c5bd9289 --- /dev/null +++ b/next_crm/patches/v1_0/backfill_utm_source_from_legacy_source.py @@ -0,0 +1,68 @@ +"""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", + ) From dcf9a564cbb8be081938cbe92be30aa07ce4810d Mon Sep 17 00:00:00 2001 From: dpk404 Date: Fri, 22 May 2026 08:25:04 +0000 Subject: [PATCH 2/2] style: black format backfill patch --- .../v1_0/backfill_utm_source_from_legacy_source.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 index d4c5bd9289..701e0cc35b 100644 --- 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 @@ -48,13 +48,20 @@ def _backfill_one(doctype: str) -> None: 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] + 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() + 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() + 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",