From 4806b82add4333e627c79c808e760ea88c05c3c2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:04:58 +0200 Subject: [PATCH 1/4] fix(stock): propagate renamed attribute values to variant items (cherry picked from commit 27d574dad50d2e44e58a949effbc7700bde11e99) # Conflicts: # erpnext/stock/doctype/item_attribute/item_attribute.py --- erpnext/controllers/item_variant.py | 43 +++++++++++++++++++ erpnext/stock/doctype/item/test_item.py | 24 +++++++++++ .../doctype/item_attribute/item_attribute.py | 6 +++ 3 files changed, 73 insertions(+) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 7c5db01c835..ed01ca5a78d 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -135,6 +135,49 @@ def validate_is_incremental(numeric_attribute, attribute, value, item): ) +def get_attribute_value_renames(item_attribute): + """Return old to new attribute value mappings for renamed Item Attribute Value rows.""" + if item_attribute.numeric_values: + return {} + + db_value = item_attribute.get_doc_before_save() + if not db_value: + return {} + + old_values = {d.name: d.attribute_value for d in db_value.item_attribute_values} + renames = {} + + for row in item_attribute.item_attribute_values: + if row.name in old_values and old_values[row.name] != row.attribute_value: + renames[old_values[row.name]] = row.attribute_value + + return renames + + +def update_variant_attribute_values(item_attribute): + """Propagate renamed Item Attribute Values to Item Variant Attribute on variant items.""" + value_map = get_attribute_value_renames(item_attribute) + if not value_map: + return + + item_variant_table = frappe.qb.DocType("Item Variant Attribute") + item_table = frappe.qb.DocType("Item") + + for old_value, new_value in value_map.items(): + ( + frappe.qb.update(item_variant_table) + .join(item_table) + .on(item_table.name == item_variant_table.parent) + .set(item_variant_table.attribute_value, new_value) + .where(item_table.variant_of.isnotnull()) + .where(item_table.variant_of != "") + .where(item_variant_table.attribute == item_attribute.name) + .where(item_variant_table.attribute_value == old_value) + ).run() + + frappe.flags.attribute_values = None + + def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True): allow_rename_attribute_value = frappe.db.get_single_value( "Item Variant Settings", "allow_rename_attribute_value" diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index eb67a776f1d..f7f4a444ea2 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -411,6 +411,30 @@ class TestItem(ERPNextTestSuite): self.assertRaises(InvalidItemAttributeValueError, attribute.save) + def test_rename_attribute_value_updates_variants(self): + frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) + + variant = create_variant("_Test Variant Item", {"Test Size": "Large"}) + variant.save() + + attribute = frappe.get_doc("Item Attribute", "Test Size") + for row in attribute.item_attribute_values: + if row.attribute_value == "Large": + row.attribute_value = "Larger" + break + + frappe.flags.attribute_values = None + attribute.save() + + self.assertEqual( + frappe.db.get_value( + "Item Variant Attribute", + {"parent": variant.name, "attribute": "Test Size"}, + "attribute_value", + ), + "Larger", + ) + def test_make_item_variant(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 2878a19de48..3d328e17168 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -8,6 +8,11 @@ from frappe.model.document import Document from frappe.utils import flt from erpnext.controllers.item_variant import ( +<<<<<<< HEAD +======= + InvalidItemAttributeValueError, + update_variant_attribute_values, +>>>>>>> 27d574dad5 (fix(stock): propagate renamed attribute values to variant items) validate_is_incremental, validate_item_attribute_value, ) @@ -43,6 +48,7 @@ class ItemAttribute(Document): self.validate_duplication() def on_update(self): + update_variant_attribute_values(self) self.validate_exising_items() self.set_enabled_disabled_in_items() From 4c29d5630d1bbd37ebcb673626164cf9fd4d9554 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:34:42 +0200 Subject: [PATCH 2/4] test(stock): add cleanup for item attribute value changes in tests (cherry picked from commit 60f5de7ab80926afb710d7350185ce52e90a883c) --- erpnext/stock/doctype/item/test_item.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index f7f4a444ea2..b4e18373cce 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -423,6 +423,17 @@ class TestItem(ERPNextTestSuite): row.attribute_value = "Larger" break + def restore_test_size_large(): + doc = frappe.get_doc("Item Attribute", "Test Size") + for row in doc.item_attribute_values: + if row.attribute_value == "Larger": + row.attribute_value = "Large" + break + frappe.flags.attribute_values = None + doc.save() + + self.addCleanup(restore_test_size_large) + frappe.flags.attribute_values = None attribute.save() From 0e244dd83ad64760d4467b61af619987bb5f16b7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:55:27 +0200 Subject: [PATCH 3/4] fix(stock): update variant attributes on value rename (cherry picked from commit c7acd887425e42e15460d588a1735a7122e48736) --- erpnext/controllers/item_variant.py | 25 +++++++------ erpnext/stock/doctype/item/test_item.py | 48 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index ed01ca5a78d..b125b15fe55 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _ +from frappe.query_builder import Case from frappe.utils import cstr, flt from erpnext.utilities.product import get_item_codes_by_attributes @@ -162,18 +163,22 @@ def update_variant_attribute_values(item_attribute): item_variant_table = frappe.qb.DocType("Item Variant Attribute") item_table = frappe.qb.DocType("Item") + attribute_value = item_variant_table.attribute_value + attribute_value_case = Case() for old_value, new_value in value_map.items(): - ( - frappe.qb.update(item_variant_table) - .join(item_table) - .on(item_table.name == item_variant_table.parent) - .set(item_variant_table.attribute_value, new_value) - .where(item_table.variant_of.isnotnull()) - .where(item_table.variant_of != "") - .where(item_variant_table.attribute == item_attribute.name) - .where(item_variant_table.attribute_value == old_value) - ).run() + attribute_value_case = attribute_value_case.when(attribute_value == old_value, new_value) + + ( + frappe.qb.update(item_variant_table) + .join(item_table) + .on(item_table.name == item_variant_table.parent) + .set(attribute_value, attribute_value_case.else_(attribute_value)) + .where(item_table.variant_of.isnotnull()) + .where(item_table.variant_of != "") + .where(item_variant_table.attribute == item_attribute.name) + .where(attribute_value.isin(list(value_map))) + ).run() frappe.flags.attribute_values = None diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index b4e18373cce..fcfa5d50075 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -446,6 +446,54 @@ class TestItem(ERPNextTestSuite): "Larger", ) + def test_swapped_attribute_value_renames_update_variants(self): + frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) + frappe.delete_doc_if_exists("Item", "_Test Variant Item-S", force=1) + + large_variant = create_variant("_Test Variant Item", {"Test Size": "Large"}) + large_variant.save() + + small_variant = create_variant("_Test Variant Item", {"Test Size": "Small"}) + small_variant.save() + + attribute = frappe.get_doc("Item Attribute", "Test Size") + original_values = {row.name: row.attribute_value for row in attribute.item_attribute_values} + + def restore_test_size_values(): + doc = frappe.get_doc("Item Attribute", "Test Size") + for row in doc.item_attribute_values: + row.attribute_value = original_values[row.name] + frappe.flags.attribute_values = None + doc.save() + + self.addCleanup(restore_test_size_values) + + for row in attribute.item_attribute_values: + if row.attribute_value == "Large": + row.attribute_value = "Small" + elif row.attribute_value == "Small": + row.attribute_value = "Large" + + frappe.flags.attribute_values = None + attribute.save() + + self.assertEqual( + frappe.db.get_value( + "Item Variant Attribute", + {"parent": large_variant.name, "attribute": "Test Size"}, + "attribute_value", + ), + "Small", + ) + self.assertEqual( + frappe.db.get_value( + "Item Variant Attribute", + {"parent": small_variant.name, "attribute": "Test Size"}, + "attribute_value", + ), + "Large", + ) + def test_make_item_variant(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) From d3c0d9b2837213c6d3d46450f32795a026e64903 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 18 Jun 2026 22:38:23 +0530 Subject: [PATCH 4/4] fix: resolve item attribute backport conflict --- erpnext/stock/doctype/item_attribute/item_attribute.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 3d328e17168..2e50479b409 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -8,11 +8,8 @@ from frappe.model.document import Document from frappe.utils import flt from erpnext.controllers.item_variant import ( -<<<<<<< HEAD -======= InvalidItemAttributeValueError, update_variant_attribute_values, ->>>>>>> 27d574dad5 (fix(stock): propagate renamed attribute values to variant items) validate_is_incremental, validate_item_attribute_value, )