Merge pull request #56118 from frappe/mergify/bp/develop/pr-56065

fix(stock): propagate renamed attribute values to variant items (backport #56065)
This commit is contained in:
Mihir Kandoi
2026-06-19 07:42:50 +05:30
committed by GitHub
3 changed files with 134 additions and 0 deletions

View File

@@ -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"

View File

@@ -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)

View File

@@ -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()