diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 7c5db01c835..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 @@ -135,6 +136,53 @@ 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") + attribute_value = item_variant_table.attribute_value + attribute_value_case = Case() + + for old_value, new_value in value_map.items(): + 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 + + 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..fcfa5d50075 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -411,6 +411,89 @@ 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 + + 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() + + self.assertEqual( + frappe.db.get_value( + "Item Variant Attribute", + {"parent": variant.name, "attribute": "Test Size"}, + "attribute_value", + ), + "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) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 2878a19de48..2e50479b409 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -8,6 +8,8 @@ from frappe.model.document import Document from frappe.utils import flt from erpnext.controllers.item_variant import ( + InvalidItemAttributeValueError, + update_variant_attribute_values, validate_is_incremental, validate_item_attribute_value, ) @@ -43,6 +45,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()