From 27d574dad50d2e44e58a949effbc7700bde11e99 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/3] fix(stock): propagate renamed attribute values to variant items --- erpnext/controllers/item_variant.py | 43 +++++++++++++++++++ erpnext/stock/doctype/item/test_item.py | 24 +++++++++++ .../doctype/item_attribute/item_attribute.py | 2 + 3 files changed, 69 insertions(+) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 74b7dfb5c24..42692b7528c 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -129,6 +129,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 d5f13e62a5c..91fa8bece72 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -360,6 +360,30 @@ class TestItem(FrappeTestCase): self.assertRaises(InvalidItemAttributeValueError, attribute.save) frappe.db.rollback() + 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 3b9bcf93288..14d2c6a4f12 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -9,6 +9,7 @@ from frappe.utils import flt from erpnext.controllers.item_variant import ( InvalidItemAttributeValueError, + update_variant_attribute_values, validate_is_incremental, validate_item_attribute_value, ) @@ -47,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 60f5de7ab80926afb710d7350185ce52e90a883c 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/3] test(stock): add cleanup for item attribute value changes in tests --- 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 91fa8bece72..b34ce5bd894 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -372,6 +372,17 @@ class TestItem(FrappeTestCase): 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 c7acd887425e42e15460d588a1735a7122e48736 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/3] fix(stock): update variant attributes on value rename --- 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 42692b7528c..c2c620950af 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 @@ -156,18 +157,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 b34ce5bd894..8072437a173 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -395,6 +395,54 @@ class TestItem(FrappeTestCase): "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)